Skip to content

Commit

Permalink
Merge pull request #3063 from financelurker/feature/3062_critical_reg…
Browse files Browse the repository at this point in the history
…ion_and_break_in_sequence_diagrams

feat: adding "Critical Region"/"Option" and "Break" blocks to sequence diagram
  • Loading branch information
knsv committed May 31, 2022
2 parents ef943d4 + 077e662 commit ee61a26
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 0 deletions.
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 @@ -190,6 +190,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 @@ -422,6 +427,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 @@ -763,6 +763,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 @@ -1165,6 +1204,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 @@ -1175,6 +1216,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 @@ -1186,6 +1228,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

0 comments on commit ee61a26

Please sign in to comment.