fix: Support @contentType for multiline values (#6217)

* fix: Support @contentType for multiline values

Fixes the issue where the @contentType annotation broke the parsing of multiline values.

* chore: add dotall flag to fileExtractContentType

Not strictly needed since body:file uses single-line values in practice,
but doesn't hurt and matches what multipartExtractContentType does.

---------

Co-authored-by: Márk Dániel Seres <markdaniel.seres@tesco.com>
This commit is contained in:
Dániel Seres
2025-12-08 14:09:25 +01:00
committed by GitHub
parent a66be21523
commit cf969dfcd6
3 changed files with 51 additions and 26 deletions

View File

@@ -52,7 +52,8 @@ const grammar = ohm.grammar(`Bru {
// Multiline text block surrounded by '''
multilinetextblockdelimiter = "'''"
multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter st* contenttypeannotation?
contenttypeannotation = "@contentType(" (~")" any)* ")"
// Dictionary Blocks
dictionary = st* "{" pairlist? tagend
@@ -65,7 +66,8 @@ const grammar = ohm.grammar(`Bru {
quoted_key_char = ~(quote_char | esc_quote_char | nl) any
quoted_key = disable_char? quote_char (esc_quote_char | quoted_key_char)* quote_char
key = keychar*
value = list | multilinetextblock | valuechar*
value = list | multilinetextblock | singlelinevalue
singlelinevalue = valuechar*
// Dictionary for Assert Block
assertdictionary = st* "{" assertpairlist? tagend
@@ -211,7 +213,7 @@ const mapRequestParams = (pairList = [], type) => {
const multipartExtractContentType = (pair) => {
if (_.isString(pair.value)) {
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/s);
if (match != null && match.length > 2) {
pair.value = match[1];
pair.contentType = match[2];
@@ -223,7 +225,7 @@ const multipartExtractContentType = (pair) => {
const fileExtractContentType = (pair) => {
if (_.isString(pair.value)) {
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/s);
if (match && match.length > 2) {
pair.value = match[1].trim();
pair.contentType = match[2].trim();
@@ -369,25 +371,6 @@ const sem = grammar.createSemantics().addAttribute('ast', {
key(chars) {
return chars.sourceString ? chars.sourceString.trim() : '';
},
value(chars) {
if (chars.ctorName === 'list') {
return chars.ast;
}
try {
let isMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
if (isMultiline) {
const multilineString = chars.sourceString?.replace(/^'''|'''$/g, '');
return multilineString
.split('\n')
.map((line) => line.slice(4))
.join('\n');
}
return chars.sourceString ? chars.sourceString.trim() : '';
} catch (err) {
console.error(err);
}
return chars.sourceString ? chars.sourceString.trim() : '';
},
assertdictionary(_1, _2, pairlist, _3) {
return pairlist.ast;
},
@@ -435,9 +418,19 @@ const sem = grammar.createSemantics().addAttribute('ast', {
multilinetextblockdelimiter(_) {
return '';
},
multilinetextblock(_1, content, _2) {
// Join all the content between the triple quotes and trim it
return content.sourceString.trim();
multilinetextblock(_1, content, _2, _3, contentType) {
const multilineString = content.sourceString
.split('\n')
.map((line) => line.slice(4))
.join('\n');
if (!contentType.sourceString) {
return multilineString;
}
return `${multilineString} ${contentType.sourceString}`;
},
singlelinevalue(chars) {
return chars.sourceString?.trim() || '';
},
_iter(...elements) {
return elements.map((e) => e.ast);

View File

@@ -175,5 +175,33 @@ vars:pre-request {
const output = parser(input);
expect(output).toEqual(expected);
});
it('parses multiline body parts with content type annotation', () => {
const input = `
body:multipart-form {
filePart: '''
Line1
Line2
''' @contentType(text/plain)
}
`;
const expected = {
body: {
multipartForm: [
{
name: 'filePart',
value: 'Line1\nLine2',
enabled: true,
type: 'text',
contentType: 'text/plain'
}
]
}
};
const output = parser(input);
expect(output).toEqual(expected);
});
});
});

View File

@@ -12,6 +12,9 @@ post {
body:multipart-form {
foo: {"bar":"baz"} @contentType(application/json--test)
multiline: '''
"multiline-test"
''' @contentType(application/json--multiline--test)
form-data-key: {{form-data-key}}
form-data-stringified-object: {{form-data-stringified-object}}
file: @file(bruno.png)
@@ -21,6 +24,7 @@ assert {
res.body: contains form-data-value
res.body: contains {"foo":123}
res.body: contains Content-Type: application/json--test
res.body: contains Content-Type: application/json--multiline--test
}
script:pre-request {