Skip to content

Commit

Permalink
fix: rendering of apikit referenced flows #256 (#297)
Browse files Browse the repository at this point in the history
  • Loading branch information
manikmagar committed Oct 29, 2022
1 parent b64563b commit 06d27f3
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 11 deletions.
30 changes: 23 additions & 7 deletions src/main/java/com/javastreets/mulefd/drawings/GraphDiagram.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,39 @@ public boolean draw(DrawingContext drawingContext) {
List<Component> flows = drawingContext.getComponents();
Path singleFlowDirPath = Paths.get(targetDirectory.getAbsolutePath(), "single-flow-diagrams",
DateUtil.now("ddMMyyyy-HHmmss"));

// Some non-flow type components have a valid reference to other flows.
// Eg. APIKit router can call another flow. It is possible for that flow to not reference any
// other flow.
// In such cases, APIKit router should be treated as root Graph Node to find usable links.
// Accumulate all such components to be added as a root node.
// TODO: Find alternate way to traverse node links
List<MutableNode> additionalRootNodes = new ArrayList<>();

if (drawingContext.getFlowName() != null) {
Component component = flows.stream()
.filter(component1 -> component1.getName().equalsIgnoreCase(drawingContext.getFlowName()))
.findFirst().orElseThrow(() -> new DrawingException(
"Target flow not found - " + drawingContext.getFlowName()));
MutableNode flowNode = processComponent(component, drawingContext, flowRefs, mappedFlowKinds);
MutableNode flowNode = processComponent(component, drawingContext, flowRefs, mappedFlowKinds,
additionalRootNodes);
flowNode.addTo(appGraph);
}

for (Component component : flows) {
if (drawingContext.getFlowName() == null
|| mappedFlowKinds.contains(component.qualifiedName())) {

MutableNode flowNode =
processComponent(component, drawingContext, flowRefs, mappedFlowKinds);
MutableNode flowNode = processComponent(component, drawingContext, flowRefs,
mappedFlowKinds, additionalRootNodes);

if (drawingContext.isGenerateSingles() && component.isaFlow()) {
MutableGraph flowRootGraph = initNewGraph(getDiagramHeaderLines());
flowNode.addTo(flowRootGraph);
for (Component component2 : flows) {
if (mappedFlowKinds.contains(component2.qualifiedName())) {
MutableNode flowNode3 =
processComponent(component2, drawingContext, flowRefs, mappedFlowKinds);
MutableNode flowNode3 = processComponent(component2, drawingContext, flowRefs,
mappedFlowKinds, additionalRootNodes);
flowNode3.addTo(flowRootGraph);
}
}
Expand All @@ -77,6 +87,9 @@ public boolean draw(DrawingContext drawingContext) {
flowNode.addTo(appGraph);
}
}
// any flows that are referenced by just the non-flow like nodes e.g APIKIT
// this adds those component nodes to the root graph so links are visible via node lookup
additionalRootNodes.forEach(node -> node.addTo(appGraph));
if (drawingContext.getFlowName() == null) {
checkUnusedNodes(appGraph);
}
Expand Down Expand Up @@ -201,7 +214,8 @@ MutableNode asSourceNode(MutableNode node) {
}

MutableNode processComponent(Component component, DrawingContext drawingContext,
Map<String, Component> flowRefs, List<String> mappedFlowKinds) {
Map<String, Component> flowRefs, List<String> mappedFlowKinds,
List<MutableNode> additionalRootNodes) {
log.debug("Processing flow - {}", component.qualifiedName());
FlowContainer flow = (FlowContainer) component;
MutableNode flowNode = mutNode(flow.qualifiedName()).add(Label.markdown(getNodeLabel(flow)));
Expand Down Expand Up @@ -229,7 +243,8 @@ MutableNode processComponent(Component component, DrawingContext drawingContext,
} else {
name = refComponent.qualifiedName();
if (!mappedFlowKinds.contains(name)) {
processComponent(refComponent, drawingContext, flowRefs, mappedFlowKinds);
processComponent(refComponent, drawingContext, flowRefs, mappedFlowKinds,
additionalRootNodes);
}
}
}
Expand Down Expand Up @@ -258,6 +273,7 @@ MutableNode processComponent(Component component, DrawingContext drawingContext,
asFlow(node);
apiKitNode.addLink(to(node).with(Style.SOLID));
}
additionalRootNodes.add(apiKitNode);
flowNode.addLink(callSequenceLink(componentIdx - 1, apiKitNode));
} else {
addSubNodes(flowNode, hasSource ? componentIdx - 1 : componentIdx, muleComponent, name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ void draw() {
}

@Test
@DisplayName("Validate generated graph when generated as JSON.")
@DisplayName("Validate generated graph when generated as DOT.")
void drawToValidateGraph() throws Exception {

File output = new File(".", "output.png");
Expand Down Expand Up @@ -129,7 +129,39 @@ void drawToValidateGraph() throws Exception {


@Test
@DisplayName("Validate generated graph for APIKIT flows when generated as JSON.")
@DisplayName("When APIKIT generated flow excludes flow-refs")
void github_issue_256() throws Exception {

List flows =
DiagramRendererTestUtil.getFlows(Paths.get("src/test/resources/gh-issues/iss-256.xml"));
File output = new File(".", "output.png");
DrawingContext context = new DrawingContext();
context.setDiagramType(DiagramType.GRAPH);
context.setOutputFile(output);
context.setComponents(flows);

ComponentItem item = new ComponentItem();
item.setPrefix("apikit");
item.setOperation("*");
item.setSource(false);
item.setConfigAttributeName("config-ref");
item.setPathAttributeName("config-ref");
context.setKnownComponents(Collections.singletonMap(item.qualifiedName(), item));

GraphDiagram graphDiagram = Mockito.spy(new GraphDiagram());
when(graphDiagram.getDiagramHeaderLines()).thenReturn(new String[] {"Test Diagram"});
graphDiagram.draw(context);
ArgumentCaptor<MutableGraph> graphArgumentCaptor = ArgumentCaptor.forClass(MutableGraph.class);
verify(graphDiagram).writGraphToFile(any(File.class), graphArgumentCaptor.capture());
MutableGraph generatedGraph = graphArgumentCaptor.getValue();
String generated = GraphvizEngineHelper.generate(generatedGraph, Format.DOT);
String ref =
new String(Files.readAllBytes(Paths.get("src/test/resources/gh-issues/iss-256.dot")));
assertThat(generated).as("DOT Graph").isEqualToNormalizingNewlines(ref);
}

@Test
@DisplayName("Validate generated graph for APIKIT flows when generated as DOT.")
void drawToValidateGraph_APIKIT() throws Exception {

List flows = DiagramRendererTestUtil.getFlows(Paths.get("src/test/resources/test-api.xml"));
Expand Down Expand Up @@ -160,7 +192,7 @@ void drawToValidateGraph_APIKIT() throws Exception {
}

@Test
@DisplayName("Validate generated graph for Single flow when generated as JSON.")
@DisplayName("Validate generated graph for Single flow when generated as DOT.")
void drawToValidateGraph_SingleFlow() throws Exception {

List flows = DiagramRendererTestUtil
Expand Down Expand Up @@ -274,7 +306,7 @@ void drawASingleFlow() {
assertThat(output).exists();
ArgumentCaptor<Component> compArg = ArgumentCaptor.forClass(Component.class);
verify(graphDiagram, Mockito.times(3)).processComponent(compArg.capture(), eq(context),
anyMap(), anyList());
anyMap(), anyList(), anyList());
assertThat(compArg.getAllValues()).containsExactly(flowContainer2, subflow, subflow);
logs.assertContains(
"Detected a possible self loop in sub-flow test-sub-flow. Skipping flow-ref processing.");
Expand Down Expand Up @@ -359,4 +391,6 @@ void writeFlowGraphWithSubFlow() {
boolean written = graphDiagram.writeFlowGraph(subflow, outputFilePath, graph);
assertThat(written).isFalse();
}


}
49 changes: 49 additions & 0 deletions src/test/resources/gh-issues/iss-256.dot
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
digraph "mule" {
edge ["dir"="forward"]
graph ["rankdir"="LR","splines"="spline","pad"="1.0,0.5","dpi"="150","label"=<Test Diagram<br/>>,"labelloc"="t"]
edge ["arrowhead"="vee","dir"="forward"]
subgraph "cluster_legend" {
edge ["dir"="forward"]
graph ["label"=<<b>Legend</b>>,"style"="dashed"]
"flow" ["fixedsize"="true","width"="1.0","height"="0.25","shape"="rectangle","color"="blue"]
"sub-flow" ["fixedsize"="true","width"="1.0","height"="0.25","color"="black","shape"="ellipse"]
"connector:operation" ["shape"="component"]
"Unused sub/-flow" ["fixedsize"="true","width"="2.0","height"="0.25","color"="gray","style"="filled"]
"Flow A" ["fixedsize"="true","width"="1.0","height"="0.25"]
"sub-flow-1" ["fixedsize"="true","width"="1.25","height"="0.25"]
"Flow C" ["fixedsize"="true","width"="1.0","height"="0.25"]
"sub-flow-C1" ["fixedsize"="true","width"="1.25","height"="0.25"]
"flow source" ["fixedsize"="true","width"="1.5","height"="0.25","shape"="hexagon","style"="filled","color"="cyan","sourceNode"="true"]
"flow self-call" ["fixedsize"="true","width"="1.25","height"="0.25","shape"="rectangle","color"="blue"]
"sub-flow self-call" ["fixedsize"="true","width"="2.0","height"="0.25","color"="black","shape"="ellipse"]
"flow" -> "sub-flow" ["style"="invis"]
"sub-flow" -> "Unused sub/-flow" ["style"="invis"]
"Flow A" -> "sub-flow-1" ["style"="solid","label"="(1)","taillabel"="Call Sequence\n","labelangle"="-5.0","labeldistance"="8.0"]
"Flow C" -> "sub-flow-C1" ["style"="dashed,bold","xlabel"="(1) Async","color"="lightblue3","taillabel"="Asynchronous call\n","labelangle"="-5.0","labeldistance"="8.0"]
"flow source" -> "flow self-call" ["style"="invis"]
"flow self-call" -> "flow self-call"
"flow self-call" -> "sub-flow self-call" ["style"="invis"]
"sub-flow self-call" -> "sub-flow self-call"
}
subgraph "cluster_legend-space" {
edge ["dir"="none"]
graph ["label"="","style"="invis"]
"" ["shape"="none","width"="2.0","height"="1.0"]
}
subgraph "cluster_mule" {
edge ["dir"="forward"]
graph ["rankdir"="LR","splines"="spline","pad"="1.0,0.5","dpi"="150","label"=<Application graph<br/>>,"labelloc"="t","style"="invis"]
edge ["arrowhead"="vee","dir"="forward"]
"http:listener" ["shape"="hexagon","style"="filled","color"="cyan","sourceNode"="true","label"=<<b>http: listener</b><br/>/api/*<br/>>]
"flow:test-api-main" ["label"=<<b>flow</b>: test-api-main>,"shape"="rectangle","color"="blue"]
"apikittest-api-config" ["shape"="doublecircle","color"="cyan","style"="filled","label"=<<b>apikit</b><br/>test-api-config<br/>>]
"flow:put:\users\(userId):test-api-config" ["label"=<<b>flow</b>: put:\users\(userId):test-api-config>,"shape"="rectangle","color"="blue"]
"flow:get:\users:test-api-config" ["label"=<<b>flow</b>: get:\users:test-api-config>,"shape"="rectangle","color"="blue"]
"sub-flow:test-sub-flow" ["label"=<<b>sub-flow</b>: test-sub-flow>,"color"="black","shape"="ellipse"]
"http:listener" -> "flow:test-api-main" ["style"="bold"]
"flow:test-api-main" -> "apikittest-api-config" ["style"="solid","label"="(1)"]
"apikittest-api-config" -> "flow:put:\users\(userId):test-api-config" ["style"="solid"]
"apikittest-api-config" -> "flow:get:\users:test-api-config" ["style"="solid"]
"flow:get:\users:test-api-config" -> "sub-flow:test-sub-flow" ["style"="solid","label"="(1)"]
}
}
115 changes: 115 additions & 0 deletions src/test/resources/gh-issues/iss-256.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<mule xmlns="http://www.mulesoft.org/schema/mule/core" xmlns:apikit="http://www.mulesoft.org/schema/mule/mule-apikit" xmlns:http="http://www.mulesoft.org/schema/mule/http" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/core http://www.mulesoft.org/schema/mule/core/current/mule.xsd http://www.mulesoft.org/schema/mule/http http://www.mulesoft.org/schema/mule/http/current/mule-http.xsd http://www.mulesoft.org/schema/mule/mule-apikit http://www.mulesoft.org/schema/mule/mule-apikit/current/mule-apikit.xsd ">
<http:listener-config name="test-api-httpListenerConfig">
<http:listener-connection host="0.0.0.0" port="8081" />
</http:listener-config>
<apikit:config name="test-api-config" api="test-api.raml" outboundHeadersMapName="outboundHeaders" httpStatusVarName="httpStatus" />
<flow name="test-api-main">
<http:listener config-ref="test-api-httpListenerConfig" path="/api/*">
<http:response statusCode="#[vars.httpStatus default 200]">
<http:headers>#[vars.outboundHeaders default {}]</http:headers>
</http:response>
<http:error-response statusCode="#[vars.httpStatus default 500]">
<http:body>#[payload]</http:body>
<http:headers>#[vars.outboundHeaders default {}]</http:headers>
</http:error-response>
</http:listener>
<apikit:router config-ref="test-api-config" />
<error-handler>
<on-error-propagate type="APIKIT:BAD_REQUEST">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{message: "Bad request"}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">400</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APIKIT:NOT_FOUND">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{message: "Resource not found"}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">404</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APIKIT:METHOD_NOT_ALLOWED">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{message: "Method not allowed"}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">405</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APIKIT:NOT_ACCEPTABLE">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{message: "Not acceptable"}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">406</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APIKIT:UNSUPPORTED_MEDIA_TYPE">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{message: "Unsupported media type"}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">415</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
<on-error-propagate type="APIKIT:NOT_IMPLEMENTED">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core" xsi:schemaLocation="http://www.mulesoft.org/schema/mule/ee/core http://www.mulesoft.org/schema/mule/ee/core/current/mule-ee.xsd">
<ee:message>
<ee:set-payload><![CDATA[%dw 2.0
output application/json
---
{message: "Not Implemented"}]]></ee:set-payload>
</ee:message>
<ee:variables>
<ee:set-variable variableName="httpStatus">501</ee:set-variable>
</ee:variables>
</ee:transform>
</on-error-propagate>
</error-handler>
</flow>
<flow name="put:\users\(userId):test-api-config">
<ee:transform xmlns:ee="http://www.mulesoft.org/schema/mule/ee/core">
<ee:variables>
<ee:set-variable variableName="userId">attributes.uriParams.'userId'</ee:set-variable>
</ee:variables>
</ee:transform>
<logger level="INFO" message="put:\users\(userId):test-api-config" />
<!-- This does not reference any other flow -->
</flow>
<flow name="get:\users:test-api-config">
<logger level="INFO" message="get:\users:test-api-config" />
<flow-ref name="test-sub-flow"/>
</flow>
<sub-flow name="test-sub-flow">
<logger level="INFO" message="test-sub-flow" />
</sub-flow>
</mule>

0 comments on commit 06d27f3

Please sign in to comment.