mirror of
https://github.com/usebruno/bruno.git
synced 2026-07-07 22:18:33 +00:00
fix: Consistent multipart form handling and @contentType support in examples (#6325)
* fix: multiline multipart items within multipart within response example * change multiline editor to single line fot contentType --------- Co-authored-by: Bijin A B <bijin@usebruno.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from 'providers/ReduxStore/slices/collections';
|
||||
import { browseFiles } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import { sendRequest, saveRequest } from 'providers/ReduxStore/slices/collections/actions';
|
||||
import EditableTable from 'components/EditableTable';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
@@ -178,7 +179,7 @@ const MultipartFormParams = ({ item, collection }) => {
|
||||
placeholder: 'Auto',
|
||||
width: '20%',
|
||||
render: ({ row, value, onChange, isLastEmptyRow }) => (
|
||||
<MultiLineEditor
|
||||
<SingleLineEditor
|
||||
onSave={onSave}
|
||||
theme={storedTheme}
|
||||
placeholder={isLastEmptyRow ? 'Auto' : ''}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { updateResponseExampleMultipartFormParams } from 'providers/ReduxStore/s
|
||||
import mime from 'mime-types';
|
||||
import path from 'utils/common/path';
|
||||
import MultiLineEditor from 'components/MultiLineEditor';
|
||||
import SingleLineEditor from 'components/SingleLineEditor';
|
||||
import StyledWrapper from './StyledWrapper';
|
||||
import FilePickerEditor from 'components/FilePickerEditor';
|
||||
import Table from 'components/Table-v2';
|
||||
@@ -206,7 +207,7 @@ const ResponseExampleMultipartFormParams = ({ item, collection, exampleUid, edit
|
||||
</td>
|
||||
<td>
|
||||
<div className="flex items-center justify-center pl-4">
|
||||
<MultiLineEditor
|
||||
<SingleLineEditor
|
||||
onSave={() => {}}
|
||||
theme={storedTheme}
|
||||
placeholder="Auto"
|
||||
|
||||
@@ -11,6 +11,10 @@ const astBaseAttribute = {
|
||||
},
|
||||
pair(_1, key, _2, _3, _4, value, _5) {
|
||||
let res = {};
|
||||
if (Array.isArray(value.ast)) {
|
||||
res[key.ast] = value.ast;
|
||||
return res;
|
||||
}
|
||||
res[key.ast] = value.ast ? value.ast.trim() : '';
|
||||
return res;
|
||||
},
|
||||
@@ -23,33 +27,6 @@ const astBaseAttribute = {
|
||||
key(chars) {
|
||||
return chars.sourceString ? chars.sourceString.trim() : '';
|
||||
},
|
||||
value(chars) {
|
||||
if (chars.ctorName === 'list') {
|
||||
return chars.ast;
|
||||
}
|
||||
try {
|
||||
let isBacktickMultiline = chars.sourceString?.startsWith('```') && chars.sourceString?.endsWith('```');
|
||||
if (isBacktickMultiline) {
|
||||
const multilineString = chars.sourceString?.replace(/^```|```$/g, '');
|
||||
return multilineString
|
||||
.split('\n')
|
||||
.map((line) => line.slice(4))
|
||||
.join('\n');
|
||||
}
|
||||
let isQuoteMultiline = chars.sourceString?.startsWith(`'''`) && chars.sourceString?.endsWith(`'''`);
|
||||
if (isQuoteMultiline) {
|
||||
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() : '';
|
||||
},
|
||||
textblock(line, _1, rest) {
|
||||
return [line.ast, ...rest.ast].join('\n');
|
||||
},
|
||||
@@ -74,8 +51,19 @@ const astBaseAttribute = {
|
||||
multilinetextblockdelimiter(_) {
|
||||
return '';
|
||||
},
|
||||
multilinetextblock(_1, content, _2) {
|
||||
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);
|
||||
|
||||
@@ -69,7 +69,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];
|
||||
@@ -85,7 +85,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();
|
||||
|
||||
@@ -25,7 +25,8 @@ const exampleGrammar = ohm.grammar(`Example {
|
||||
|
||||
// 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
|
||||
@@ -38,7 +39,8 @@ const exampleGrammar = ohm.grammar(`Example {
|
||||
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 = multilinetextblock | valuechar*
|
||||
value = list | multilinetextblock | singlelinevalue
|
||||
singlelinevalue = valuechar*
|
||||
|
||||
// List
|
||||
list = st* "[" nl+ listitems? st* nl+ st* "]"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { indentString } = require('../utils');
|
||||
const { indentString, getValueString } = require('../utils');
|
||||
|
||||
// remove the last line if two new lines are found
|
||||
const stripLastLine = (text) => {
|
||||
@@ -138,7 +138,9 @@ const jsonToExampleBru = (json) => {
|
||||
= item.contentType && item.contentType !== '' ? ' @contentType(' + item.contentType + ')' : '';
|
||||
|
||||
if (item.type === 'text') {
|
||||
return `${enabled}${quoteKey(item.name)}: ${item.value}${contentType}`;
|
||||
// Use getValueString to wrap multiline values with triple quotes
|
||||
const valueString = getValueString(item.value);
|
||||
return `${enabled}${quoteKey(item.name)}: ${valueString}${contentType}`;
|
||||
}
|
||||
|
||||
if (item.type === 'file') {
|
||||
|
||||
@@ -29,7 +29,8 @@ const requestGrammar = ohm.grammar(`Request {
|
||||
|
||||
// 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
|
||||
@@ -42,7 +43,8 @@ const requestGrammar = ohm.grammar(`Request {
|
||||
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*
|
||||
|
||||
// List
|
||||
list = st* "[" nl+ listitems? st* nl+ st* "]"
|
||||
|
||||
@@ -23,7 +23,8 @@ const responseGrammar = ohm.grammar(`Response {
|
||||
|
||||
// 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
|
||||
@@ -36,7 +37,8 @@ const responseGrammar = ohm.grammar(`Response {
|
||||
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*
|
||||
|
||||
// List
|
||||
list = st* "[" nl+ listitems? st* nl+ st* "]"
|
||||
|
||||
@@ -308,4 +308,70 @@ example {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Examples with multiline strings and contentType', () => {
|
||||
it('should parse examples with multiline strings and @contentType annotations', () => {
|
||||
const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');
|
||||
const expected = require('./fixtures/json/examples-multiline-contenttype.json');
|
||||
const output = bruToJson(input);
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should correctly extract contentType from multiline values', () => {
|
||||
const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');
|
||||
const output = bruToJson(input);
|
||||
|
||||
const example = output.examples[0];
|
||||
const multipartForm = example.request.body.multipartForm;
|
||||
|
||||
// Check that multiline values with @contentType are parsed correctly
|
||||
const testField = multipartForm.find((f) => f.name === 'test');
|
||||
expect(testField).toBeDefined();
|
||||
expect(testField.value).toContain('"hello"');
|
||||
expect(testField.contentType).toBe('application/json');
|
||||
|
||||
// Check single-line value with @contentType
|
||||
const simpleField = multipartForm.find((f) => f.name === 'simple');
|
||||
expect(simpleField).toBeDefined();
|
||||
expect(simpleField.value).toBe('cat and mouse');
|
||||
expect(simpleField.contentType).toBe('text/plain');
|
||||
|
||||
// Check multiline value without @contentType
|
||||
const arrayField = multipartForm.find((f) => f.name === 'array');
|
||||
expect(arrayField).toBeDefined();
|
||||
expect(arrayField.value).toContain('"coolade"');
|
||||
expect(arrayField.contentType).toBe('');
|
||||
|
||||
// Check complex multiline JSON with @contentType
|
||||
const jsonValueField = multipartForm.find((f) => f.name === 'jsonValue');
|
||||
expect(jsonValueField).toBeDefined();
|
||||
expect(jsonValueField.value).toContain('"key": "value"');
|
||||
expect(jsonValueField.contentType).toBe('application/json');
|
||||
});
|
||||
|
||||
it('should handle round-trip conversion for multiline strings with contentType', () => {
|
||||
const originalBru = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-contenttype.bru'), 'utf8');
|
||||
const jsonFromBru = bruToJson(originalBru);
|
||||
const bruFromJson = jsonToBru(jsonFromBru);
|
||||
const jsonFromBruAgain = bruToJson(bruFromJson);
|
||||
|
||||
// The examples should be preserved through the round-trip
|
||||
expect(jsonFromBruAgain.examples).toBeDefined();
|
||||
expect(Array.isArray(jsonFromBruAgain.examples)).toBe(true);
|
||||
expect(jsonFromBruAgain.examples).toHaveLength(1);
|
||||
|
||||
const example = jsonFromBruAgain.examples[0];
|
||||
const multipartForm = example.request.body.multipartForm;
|
||||
|
||||
// Verify contentType is preserved
|
||||
const testField = multipartForm.find((f) => f.name === 'test');
|
||||
expect(testField.contentType).toBe('application/json');
|
||||
expect(testField.value).toContain('"hello"');
|
||||
|
||||
const jsonValueField = multipartForm.find((f) => f.name === 'jsonValue');
|
||||
expect(jsonValueField.contentType).toBe('application/json');
|
||||
expect(jsonValueField.value).toContain('"key": "value"');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
meta {
|
||||
name: Multiline ContentType Test
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{host}}/test
|
||||
body: multipartForm
|
||||
}
|
||||
|
||||
example {
|
||||
name: Example with multiline strings and contentType
|
||||
|
||||
request: {
|
||||
url: {{host}}/test
|
||||
method: POST
|
||||
mode: multipartForm
|
||||
body:multipart-form: {
|
||||
test: '''
|
||||
{
|
||||
"hello" : "there"
|
||||
}
|
||||
''' @contentType(application/json)
|
||||
simple: cat and mouse @contentType(text/plain)
|
||||
array: '''
|
||||
[
|
||||
"coolade",
|
||||
"blast"
|
||||
]
|
||||
'''
|
||||
jsonValue: '''
|
||||
{
|
||||
"key": "value",
|
||||
"nested": {
|
||||
"data": 123
|
||||
}
|
||||
}
|
||||
''' @contentType(application/json)
|
||||
textValue: '''
|
||||
This is a
|
||||
multiline text
|
||||
value
|
||||
''' @contentType(text/plain)
|
||||
}
|
||||
}
|
||||
|
||||
response: {
|
||||
status: {
|
||||
code: 200
|
||||
text: OK
|
||||
}
|
||||
|
||||
body: {
|
||||
type: json
|
||||
content: '''
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Data received"
|
||||
}
|
||||
'''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Multiline ContentType Test",
|
||||
"type": "http",
|
||||
"seq": "1"
|
||||
},
|
||||
"http": {
|
||||
"method": "post",
|
||||
"url": "{{host}}/test",
|
||||
"body": "multipartForm"
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Example with multiline strings and contentType",
|
||||
"request": {
|
||||
"url": "{{host}}/test",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"mode": "multipartForm",
|
||||
"multipartForm": [
|
||||
{
|
||||
"name": "test",
|
||||
"value": "{\n\"hello\" : \"there\"\n}",
|
||||
"enabled": true,
|
||||
"type": "text",
|
||||
"contentType": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "simple",
|
||||
"value": "cat and mouse",
|
||||
"enabled": true,
|
||||
"type": "text",
|
||||
"contentType": "text/plain"
|
||||
},
|
||||
{
|
||||
"name": "array",
|
||||
"value": "[\n\"coolade\", \n\"blast\"\n]",
|
||||
"enabled": true,
|
||||
"type": "text",
|
||||
"contentType": ""
|
||||
},
|
||||
{
|
||||
"name": "jsonValue",
|
||||
"value": "{\n \"key\": \"value\",\n \"nested\": {\n \"data\": 123\n }\n}",
|
||||
"enabled": true,
|
||||
"type": "text",
|
||||
"contentType": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "textValue",
|
||||
"value": "This is a\nmultiline text\nvalue",
|
||||
"enabled": true,
|
||||
"type": "text",
|
||||
"contentType": "text/plain"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"status": "200",
|
||||
"statusText": "OK",
|
||||
"body": {
|
||||
"type": "json",
|
||||
"content": "{\n \"status\": \"success\",\n \"message\": \"Data received\"\n}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user