Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adding "Critical Region"/"Option" and "Break" blocks to sequence diagram #3063

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
36 changes: 36 additions & 0 deletions cypress/integration/rendering/sequencediagram.spec.js
Expand Up @@ -452,6 +452,42 @@ context('Sequence diagram', () => {
{}
);
});
it('should render rect around and inside criticals', () => {
imgSnapshotTest(
`
sequenceDiagram
A ->> B: 1
rect rgb(204, 0, 102)
critical yes
C ->> C: 1
option no
rect rgb(0, 204, 204)
C ->> C: 0
end
end
end
B ->> A: Return
`,
{}
);
});
it('should render rect around and inside breaks', () => {
imgSnapshotTest(
`
sequenceDiagram
A ->> B: 1
rect rgb(204, 0, 102)
break yes
rect rgb(0, 204, 204)
C ->> C: 0
end
end
end
B ->> A: Return
`,
{}
);
});
it('should render autonumber when configured with such', () => {
imgSnapshotTest(
`
Expand Down
64 changes: 64 additions & 0 deletions docs/sequenceDiagram.md
Expand Up @@ -230,6 +230,70 @@ sequenceDiagram
end
```

## Critical Region

It is possible to show actions that must happen automatically with conditional handling of circumstances.

This is done by the notation

```
critical [Action that must be performed]
... statements ...
option [Circumstance A]
... statements ...
option [Circumstance B]
... statements ...
end
```

See the example below:

```mermaid-example
sequenceDiagram
critical Establish a connection to the DB
Service-->DB: connect
option Network timeout
Service-->Service: Log error
option Credentials rejected
Service-->Service: Log different error
end
```

It is also possible to have no options at all

```mermaid-example
sequenceDiagram
critical Establish a connection to the DB
Service-->DB: connect
end
```

This critical block can also be nested, equivalently to the `par` statement as seen above.

## Break

It is possible to indicate a stop of the sequence within the flow (usually used to model exceptions).

This is done by the notation

```
break [something happened]
... statements ...
end
```

See the example below:

```mermaid-example
sequenceDiagram
Consumer-->API: Book something
API-->BookingService: Start booking process
break when the booking process fails
API-->Consumer: show failure
end
API-->BillingService: Start billing process
```

## Background Highlighting

It is possible to highlight flows by providing colored background rects. This is done by the notation
Expand Down
22 changes: 22 additions & 0 deletions src/diagrams/sequence/parser/sequenceDiagram.jison
Expand Up @@ -47,6 +47,9 @@
"else" { this.begin('LINE'); return 'else'; }
"par" { this.begin('LINE'); return 'par'; }
"and" { this.begin('LINE'); return 'and'; }
"critical" { this.begin('LINE'); return 'critical'; }
"option" { this.begin('LINE'); return 'option'; }
"break" { this.begin('LINE'); return 'break'; }
<LINE>(?:[:]?(?:no)?wrap:)?[^#\n;]* { this.popState(); return 'restOfLine'; }
"end" return 'end';
"left of" return 'left_of';
Expand Down Expand Up @@ -172,9 +175,28 @@ statement
// End
$3.push({type: 'parEnd', signalType: yy.LINETYPE.PAR_END});
$$=$3;}
| critical restOfLine option_sections end
{
// critical start
$3.unshift({type: 'criticalStart', criticalText:yy.parseMessage($2), signalType: yy.LINETYPE.CRITICAL_START});
// Content in critical is already in $3
// critical end
$3.push({type: 'criticalEnd', signalType: yy.LINETYPE.CRITICAL_END});
$$=$3;}
| break restOfLine document end
{
$3.unshift({type: 'breakStart', breakText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_START});
$3.push({type: 'breakEnd', optText:yy.parseMessage($2), signalType: yy.LINETYPE.BREAK_END});
$$=$3;}
| directive
;

option_sections
: document
| document option restOfLine option_sections
{ $$ = $1.concat([{type: 'option', optionText:yy.parseMessage($3), signalType: yy.LINETYPE.CRITICAL_OPTION}, $4]); }
;

par_sections
: document
| document and restOfLine par_sections
Expand Down
20 changes: 20 additions & 0 deletions src/diagrams/sequence/sequenceDb.js
Expand Up @@ -188,6 +188,11 @@ export const LINETYPE = {
SOLID_POINT: 24,
DOTTED_POINT: 25,
AUTONUMBER: 26,
CRITICAL_START: 27,
CRITICAL_OPTION: 28,
CRITICAL_END: 29,
BREAK_START: 30,
BREAK_END: 31,
};

export const ARROWTYPE = {
Expand Down Expand Up @@ -429,6 +434,21 @@ export const apply = function (param) {
case 'parEnd':
addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'criticalStart':
addSignal(undefined, undefined, param.criticalText, param.signalType);
break;
case 'option':
addSignal(undefined, undefined, param.optionText, param.signalType);
break;
case 'criticalEnd':
addSignal(undefined, undefined, undefined, param.signalType);
break;
case 'breakStart':
addSignal(undefined, undefined, param.breakText, param.signalType);
break;
case 'breakEnd':
addSignal(undefined, undefined, undefined, param.signalType);
break;
}
}
};
Expand Down
74 changes: 74 additions & 0 deletions src/diagrams/sequence/sequenceDiagram.spec.js
Expand Up @@ -843,6 +843,80 @@ end`;
expect(messages[7].from).toBe('Bob');
expect(messages[8].type).toBe(parser.yy.LINETYPE.ALT_END);
});
it('it should handle critical statements without options', function () {
const str = `
sequenceDiagram
critical Establish a connection to the DB
Service-->DB: connect
end`;

mermaidAPI.parse(str);
const actors = parser.yy.getActors();

expect(actors.Service.description).toBe('Service');
expect(actors.DB.description).toBe('DB');

const messages = parser.yy.getMessages();

expect(messages.length).toBe(3);
expect(messages[0].type).toBe(parser.yy.LINETYPE.CRITICAL_START);
expect(messages[1].from).toBe('Service');
expect(messages[2].type).toBe(parser.yy.LINETYPE.CRITICAL_END);
});
it('it should handle critical statements with options', function () {
const str = `
sequenceDiagram
critical Establish a connection to the DB
Service-->DB: connect
option Network timeout
Service-->Service: Log error
option Credentials rejected
Service-->Service: Log different error
end`;

mermaidAPI.parse(str);
const actors = parser.yy.getActors();

expect(actors.Service.description).toBe('Service');
expect(actors.DB.description).toBe('DB');

const messages = parser.yy.getMessages();

expect(messages.length).toBe(7);
expect(messages[0].type).toBe(parser.yy.LINETYPE.CRITICAL_START);
expect(messages[1].from).toBe('Service');
expect(messages[2].type).toBe(parser.yy.LINETYPE.CRITICAL_OPTION);
expect(messages[3].from).toBe('Service');
expect(messages[4].type).toBe(parser.yy.LINETYPE.CRITICAL_OPTION);
expect(messages[5].from).toBe('Service');
expect(messages[6].type).toBe(parser.yy.LINETYPE.CRITICAL_END);
});
it('it should handle break statements', function () {
const str = `
sequenceDiagram
Consumer-->API: Book something
API-->BookingService: Start booking process
break when the booking process fails
API-->Consumer: show failure
end
API-->BillingService: Start billing process`;

mermaidAPI.parse(str);
const actors = parser.yy.getActors();

expect(actors.Consumer.description).toBe('Consumer');
expect(actors.API.description).toBe('API');

const messages = parser.yy.getMessages();

expect(messages.length).toBe(6);
expect(messages[0].from).toBe('Consumer');
expect(messages[1].from).toBe('API');
expect(messages[2].type).toBe(parser.yy.LINETYPE.BREAK_START);
expect(messages[3].from).toBe('API');
expect(messages[4].type).toBe(parser.yy.LINETYPE.BREAK_END);
expect(messages[5].from).toBe('API');
});
it('it should handle par statements a sequenceDiagram', function () {
const str = `
sequenceDiagram
Expand Down
44 changes: 44 additions & 0 deletions src/diagrams/sequence/sequenceRenderer.js
Expand Up @@ -764,6 +764,45 @@ export const draw = function (text, id) {
if (msg.message.visible) parser.yy.enableSequenceNumbers();
else parser.yy.disableSequenceNumbers();
break;
case parser.yy.LINETYPE.CRITICAL_START:
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin,
conf.boxMargin + conf.boxTextMargin,
(message) => bounds.newLoop(message)
);
break;
case parser.yy.LINETYPE.CRITICAL_OPTION:
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin + conf.boxTextMargin,
conf.boxMargin,
(message) => bounds.addSectionToLoop(message)
);
break;
case parser.yy.LINETYPE.CRITICAL_END:
loopModel = bounds.endLoop();
svgDraw.drawLoop(diagram, loopModel, 'critical', conf);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
bounds.models.addLoop(loopModel);
break;
case parser.yy.LINETYPE.BREAK_START:
adjustLoopHeightForWrap(
loopWidths,
msg,
conf.boxMargin,
conf.boxMargin + conf.boxTextMargin,
(message) => bounds.newLoop(message)
);
break;
case parser.yy.LINETYPE.BREAK_END:
loopModel = bounds.endLoop();
svgDraw.drawLoop(diagram, loopModel, 'break', conf);
bounds.bumpVerticalPos(loopModel.stopy - bounds.getVerticalPos());
bounds.models.addLoop(loopModel);
break;
default:
try {
// lastMsg = msg
Expand Down Expand Up @@ -1166,6 +1205,8 @@ const calculateLoopBounds = function (messages, actors) {
case parser.yy.LINETYPE.ALT_START:
case parser.yy.LINETYPE.OPT_START:
case parser.yy.LINETYPE.PAR_START:
case parser.yy.LINETYPE.CRITICAL_START:
case parser.yy.LINETYPE.BREAK_START:
stack.push({
id: msg.id,
msg: msg.message,
Expand All @@ -1176,6 +1217,7 @@ const calculateLoopBounds = function (messages, actors) {
break;
case parser.yy.LINETYPE.ALT_ELSE:
case parser.yy.LINETYPE.PAR_AND:
case parser.yy.LINETYPE.CRITICAL_OPTION:
if (msg.message) {
current = stack.pop();
loops[current.id] = current;
Expand All @@ -1187,6 +1229,8 @@ const calculateLoopBounds = function (messages, actors) {
case parser.yy.LINETYPE.ALT_END:
case parser.yy.LINETYPE.OPT_END:
case parser.yy.LINETYPE.PAR_END:
case parser.yy.LINETYPE.CRITICAL_END:
case parser.yy.LINETYPE.BREAK_END:
current = stack.pop();
loops[current.id] = current;
break;
Expand Down