mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
fix: support multiline descriptions in example blocks (#6879)
* fix: support multiline descriptions in example blocks * refactor: use outdentString for example multiline text block parsing * test: add test case for examples without description field * test: add jsonToBru conversion test for multiline descriptions * refactor: generalize descriptionvalue to textvalue in example grammar
This commit is contained in:
@@ -27,7 +27,7 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
// Check if this example is the active tab
|
||||
const activeTabUid = useSelector((state) => state.tabs?.activeTabUid);
|
||||
const isExampleActive = activeTabUid === example.uid;
|
||||
const [editName, setEditName] = useState(example.name);
|
||||
const [editName, setEditName] = useState(example.name || '');
|
||||
const [showRenameModal, setShowRenameModal] = useState(false);
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [generateCodeItemModalOpen, setGenerateCodeItemModalOpen] = useState(false);
|
||||
@@ -230,7 +230,7 @@ const ExampleItem = ({ example, item, collection }) => {
|
||||
handleConfirm={() => handleRenameConfirm(editName)}
|
||||
confirmText="Rename"
|
||||
cancelText="Cancel"
|
||||
confirmDisabled={!editName.trim()}
|
||||
confirmDisabled={!editName || !editName.trim()}
|
||||
>
|
||||
<div>
|
||||
<label htmlFor="renameExampleName" className="block font-medium">
|
||||
|
||||
@@ -12,6 +12,7 @@ const astBaseAttribute = require('../common/attributes');
|
||||
* No meta block - everything is at root level: name, description, type, url, etc.
|
||||
* Supports all body types from request side but response body stays as simple text.
|
||||
*/
|
||||
|
||||
const exampleGrammar = ohm.grammar(`Example {
|
||||
ExampleFile = (name | description | request | response)*
|
||||
|
||||
@@ -41,20 +42,21 @@ const exampleGrammar = ohm.grammar(`Example {
|
||||
key = keychar*
|
||||
value = list | multilinetextblock | singlelinevalue
|
||||
singlelinevalue = valuechar*
|
||||
|
||||
|
||||
// List
|
||||
list = st* "[" nl+ listitems? st* nl+ st* "]"
|
||||
listitems = listitem (nl+ listitem)*
|
||||
listitem = st+ (alnum | "_" | "-")+ st*
|
||||
|
||||
|
||||
// Text Blocks
|
||||
textblock = textline (~tagend nl textline)*
|
||||
textline = textchar*
|
||||
textchar = ~nl any
|
||||
textvalue = multilinetextblock | singlelinevalue
|
||||
|
||||
// Root level properties
|
||||
name = "name" st* ":" st* valuechar* st*
|
||||
description = "description" st* ":" st* valuechar* st*
|
||||
description = "description" st* ":" st* textvalue st*
|
||||
|
||||
// Request block
|
||||
request = nl* "request" st* ":" st* "{" nl* requestcontent+ nl* "}" nl*
|
||||
@@ -85,9 +87,20 @@ const astExampleAttribute = {
|
||||
},
|
||||
description(_1, _2, _3, _4, value, _6) {
|
||||
return {
|
||||
description: value.sourceString ? value.sourceString.trim() : ''
|
||||
description: value.ast ? value.ast.trim() : ''
|
||||
};
|
||||
},
|
||||
textvalue(content) {
|
||||
return content.ast;
|
||||
},
|
||||
multilinetextblock(_1, content, _2, _3, contentType) {
|
||||
const multilineString = outdentString(content.sourceString);
|
||||
|
||||
if (!contentType.sourceString) {
|
||||
return multilineString;
|
||||
}
|
||||
return `${multilineString} ${contentType.sourceString}`;
|
||||
},
|
||||
request(_1, _2, _3, _4, _5, _6, _7, requestcontent, _8, _9, _10) {
|
||||
if (!requestcontent || !requestcontent.ast || !requestcontent.ast.length) {
|
||||
return {};
|
||||
|
||||
@@ -38,7 +38,8 @@ const jsonToExampleBru = (json) => {
|
||||
}
|
||||
|
||||
if (description) {
|
||||
bru += `description: ${description}\n`;
|
||||
const descriptionValue = getValueString(description);
|
||||
bru += `description: ${descriptionValue}\n`;
|
||||
}
|
||||
|
||||
// Request block
|
||||
|
||||
@@ -243,6 +243,51 @@ example {
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should handle examples without description', () => {
|
||||
const jsonInput = {
|
||||
meta: {
|
||||
name: 'Test API',
|
||||
type: 'http'
|
||||
},
|
||||
http: {
|
||||
url: 'https://api.example.com/test',
|
||||
method: 'get'
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
name: 'Example Request',
|
||||
request: {
|
||||
url: 'https://api.example.com/example',
|
||||
method: 'get'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const expected = `meta {
|
||||
name: Test API
|
||||
type: http
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://api.example.com/test
|
||||
}
|
||||
|
||||
example {
|
||||
name: Example Request
|
||||
|
||||
request: {
|
||||
url: https://api.example.com/example
|
||||
method: get
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = jsonToBru(jsonInput);
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex examples with auth', () => {
|
||||
@@ -309,6 +354,48 @@ example {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Examples with multiline descriptions', () => {
|
||||
it('should parse examples with multiline descriptions', () => {
|
||||
const input = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-description.bru'), 'utf8');
|
||||
const expected = require('./fixtures/json/examples-multiline-description.json');
|
||||
const output = bruToJson(input);
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should convert examples with multiline descriptions to BRU format', () => {
|
||||
const jsonInput = require('./fixtures/json/examples-multiline-description.json');
|
||||
const expected = fs.readFileSync(path.join(__dirname, 'fixtures', 'bru', 'examples-multiline-description.bru'), 'utf8');
|
||||
const output = jsonToBru(jsonInput);
|
||||
|
||||
expect(output).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should parse example without description field', () => {
|
||||
const bruInput = `meta {
|
||||
name: Test API
|
||||
type: http
|
||||
}
|
||||
|
||||
example {
|
||||
name: Example Request
|
||||
|
||||
request: {
|
||||
url: https://api.example.com/example
|
||||
method: get
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const output = bruToJson(bruInput);
|
||||
|
||||
expect(output.examples).toBeDefined();
|
||||
expect(output.examples).toHaveLength(1);
|
||||
expect(output.examples[0].name).toBe('Example Request');
|
||||
expect(output.examples[0].description).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
meta {
|
||||
name: Multiline Description Test
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: https://api.example.com/test
|
||||
}
|
||||
|
||||
example {
|
||||
name: Test Example
|
||||
description: '''
|
||||
This is a multiline description.
|
||||
It spans multiple lines.
|
||||
And should be parsed correctly.
|
||||
'''
|
||||
|
||||
request: {
|
||||
url: https://api.example.com/test
|
||||
method: get
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"meta": {
|
||||
"name": "Multiline Description Test",
|
||||
"type": "http",
|
||||
"seq": "1"
|
||||
},
|
||||
"http": {
|
||||
"method": "get",
|
||||
"url": "https://api.example.com/test"
|
||||
},
|
||||
"examples": [
|
||||
{
|
||||
"name": "Test Example",
|
||||
"description": "This is a multiline description.\nIt spans multiple lines.\nAnd should be parsed correctly.",
|
||||
"request": {
|
||||
"url": "https://api.example.com/test",
|
||||
"method": "get"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user