From 672c120896073966894e7e30648a9d5ebd578228 Mon Sep 17 00:00:00 2001 From: Juan Garcia Losada Date: Wed, 25 Oct 2023 14:31:38 +0200 Subject: [PATCH] OPT-1032 - Implement DiagramDataflowLoader --- .../load/diagram_dataflow_loader.py | 21 ++++++- .../slp_drawio/load/drawio_dict_utils.py | 21 +++++-- .../slp_drawio/objects/diagram_objects.py | 16 +++++- .../aws_two_component_multiple_dataflows.json | 57 ++++++++++++------- .../aws_two_component_multiple_dataflows.xml | 24 ++++---- .../tests/resources/test_resource_paths.py | 2 +- .../unit/load/test_diagram_dataflow_loader.py | 38 +++++++++++++ .../tests/unit/load/test_drawio_dict_utils.py | 28 ++++++++- 8 files changed, 165 insertions(+), 42 deletions(-) create mode 100644 slp_drawio/tests/unit/load/test_diagram_dataflow_loader.py diff --git a/slp_drawio/slp_drawio/load/diagram_dataflow_loader.py b/slp_drawio/slp_drawio/load/diagram_dataflow_loader.py index 642932be..86eb0a82 100644 --- a/slp_drawio/slp_drawio/load/diagram_dataflow_loader.py +++ b/slp_drawio/slp_drawio/load/diagram_dataflow_loader.py @@ -1,10 +1,27 @@ +from typing import List + +from slp_drawio.slp_drawio.load.drawio_dict_utils import get_mx_cell_dataflows, get_dataflow_tags from slp_drawio.slp_drawio.objects.diagram_objects import DiagramDataflow class DiagramDataflowLoader: def __init__(self, source: dict): - self.source: dict = source + self._source: dict = source def load(self) -> [DiagramDataflow]: - return [] + + result: List[DiagramDataflow] = [] + + mx_cell_dataflows = get_mx_cell_dataflows(self._source) + for mx_cell in mx_cell_dataflows: + if all(key in mx_cell for key in ['source', 'target']): + result.append(DiagramDataflow( + dataflow_id=mx_cell.get('id'), + name=f'{mx_cell.get("id")}-dataflow', + source_node=mx_cell.get('source'), + destination_node=mx_cell.get('target'), + tags=get_dataflow_tags(mx_cell.get('id'), self._source) + )) + + return result diff --git a/slp_drawio/slp_drawio/load/drawio_dict_utils.py b/slp_drawio/slp_drawio/load/drawio_dict_utils.py index 5fc07ec9..31808f46 100644 --- a/slp_drawio/slp_drawio/load/drawio_dict_utils.py +++ b/slp_drawio/slp_drawio/load/drawio_dict_utils.py @@ -22,21 +22,32 @@ def __is_mx_cell_dataflow(mx_cell): def __is_mx_cell_component(mx_cell: Dict): - if 'mxGeometry' not in mx_cell: + mx_geometry = mx_cell.get('mxGeometry', {}) + if not all(key in mx_geometry for key in ['height', 'width']): return False return not __is_mx_cell_dataflow(mx_cell) -def __get_mx_cell_from_source(source): +def __get_mx_cells(source) -> List[Dict]: return source.get("mxfile", {}).get("diagram", {}).get("mxGraphModel", {}).get("root", {}).get("mxCell", []) def get_mx_cell_components(source) -> List[Dict]: - return list(filter(lambda c: __is_mx_cell_component(c), __get_mx_cell_from_source(source))) + return list(filter(lambda c: __is_mx_cell_component(c), __get_mx_cells(source))) + + +def get_mx_cell_dataflows(source) -> List[Dict]: + return list(filter(lambda c: __is_mx_cell_dataflow(c), __get_mx_cells(source))) + + +def get_dataflow_tags(dataflow_id: str, source) -> List[str]: + tags: List[str] = [] + for mx_cell in __get_mx_cells(source): + if dataflow_id == mx_cell.get('parent') and 'value' in mx_cell: + tags.append(mx_cell['value']) -def get_mxcell_dataflows(source) -> List[Dict]: - return list(filter(lambda c: __is_mx_cell_dataflow(c), __get_mx_cell_from_source(source))) + return tags def is_multiple_pages(source): diff --git a/slp_drawio/slp_drawio/objects/diagram_objects.py b/slp_drawio/slp_drawio/objects/diagram_objects.py index bb67430e..d59eb092 100644 --- a/slp_drawio/slp_drawio/objects/diagram_objects.py +++ b/slp_drawio/slp_drawio/objects/diagram_objects.py @@ -39,8 +39,20 @@ def __str__(self) -> str: @auto_repr class DiagramDataflow: - def __init__(self, id: str): - self.otm = Dataflow(dataflow_id=id, name='', source_node=None, destination_node=None) + def __init__(self, + dataflow_id: str, + name: str = '', + source_node: str = None, + destination_node: str = None, + tags: List[str] = None + ): + self.otm = Dataflow( + dataflow_id=dataflow_id, + name=name, + source_node=source_node, + destination_node=destination_node, + tags=tags + ) @auto_repr diff --git a/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.json b/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.json index 3d92941f..f9770c6c 100644 --- a/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.json +++ b/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.json @@ -23,9 +23,9 @@ }, "id":"pt2kyrPXSm7H56EBWWGj-2", "style":"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;", - "edge":"1", "parent":"1", - "source":"pt2kyrPXSm7H56EBWWGj-1" + "source":"pt2kyrPXSm7H56EBWWGj-1", + "edge":"1" }, { "mxGeometry":{ @@ -46,8 +46,8 @@ }, "id":"pt2kyrPXSm7H56EBWWGj-4", "style":"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;", - "edge":"1", - "parent":"1" + "parent":"1", + "edge":"1" }, { "mxGeometry":{ @@ -60,8 +60,8 @@ "id":"pt2kyrPXSm7H56EBWWGj-1", "value":"", "style":"outlineConnect=0;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;shape=mxgraph.aws3.ec2;fillColor=#F58534;gradientColor=none;", - "vertex":"1", - "parent":"1" + "parent":"1", + "vertex":"1" }, { "mxGeometry":{ @@ -88,9 +88,9 @@ }, "id":"pt2kyrPXSm7H56EBWWGj-5", "style":"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;", - "edge":"1", "parent":"1", - "target":"pt2kyrPXSm7H56EBWWGj-1" + "target":"pt2kyrPXSm7H56EBWWGj-1", + "edge":"1" }, { "mxGeometry":{ @@ -99,10 +99,28 @@ }, "id":"pt2kyrPXSm7H56EBWWGj-6", "style":"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;entryPerimeter=0;", - "edge":"1", "parent":"1", "source":"pt2kyrPXSm7H56EBWWGj-1", - "target":"pt2kyrPXSm7H56EBWWGj-7" + "target":"pt2kyrPXSm7H56EBWWGj-7", + "edge":"1" + }, + { + "mxGeometry":{ + "mxPoint":{ + "y":"1", + "as":"offset" + }, + "x":"-0.0044", + "y":"1", + "relative":"1", + "as":"geometry" + }, + "id":"m2HWG3bc47D113dpYQm--1", + "value":"Dataflow Info", + "style":"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];", + "vertex":"1", + "connectable":"0", + "parent":"pt2kyrPXSm7H56EBWWGj-6" }, { "mxGeometry":{ @@ -115,8 +133,8 @@ "id":"pt2kyrPXSm7H56EBWWGj-7", "value":"", "style":"outlineConnect=0;dashed=0;verticalLabelPosition=bottom;verticalAlign=top;align=center;html=1;shape=mxgraph.aws3.ec2;fillColor=#F58534;gradientColor=none;", - "vertex":"1", - "parent":"1" + "parent":"1", + "vertex":"1" }, { "mxGeometry":{ @@ -125,15 +143,15 @@ }, "id":"pt2kyrPXSm7H56EBWWGj-8", "style":"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;", - "edge":"1", "parent":"1", "source":"pt2kyrPXSm7H56EBWWGj-7", - "target":"pt2kyrPXSm7H56EBWWGj-7" + "target":"pt2kyrPXSm7H56EBWWGj-7", + "edge":"1" } ] }, "dx":"1364", - "dy":"771", + "dy":"759", "grid":"1", "gridSize":"10", "guides":"1", @@ -152,9 +170,10 @@ "id":"KqMttW68mEOmiwTKMwjx" }, "host":"app.diagrams.net", - "modified":"2023-10-19T16:09:41.010Z", - "agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36", - "etag":"53oyWIo9RTndIs_Ustab", - "version":"22.0.5" + "modified":"2023-10-25T06:52:34.609Z", + "agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36", + "etag":"gHENJeu3QhVxARD-wiXo", + "version":"22.0.7", + "type":"device" } } \ No newline at end of file diff --git a/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.xml b/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.xml index d0e7d46d..95b4b22a 100644 --- a/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.xml +++ b/slp_drawio/tests/resources/drawio/aws_two_component_multiple_dataflows.xml @@ -1,25 +1,24 @@ - - + - + - + - + - + - + @@ -28,13 +27,18 @@ - + - + + + + + + - + diff --git a/slp_drawio/tests/resources/test_resource_paths.py b/slp_drawio/tests/resources/test_resource_paths.py index 4bdf0a45..f93999f3 100644 --- a/slp_drawio/tests/resources/test_resource_paths.py +++ b/slp_drawio/tests/resources/test_resource_paths.py @@ -9,7 +9,7 @@ aws_minimal_drawio_as_json = f'{drawio}/aws_minimal_source.json' aws_multiple_pages_drawio = f'{drawio}/aws_multiple_pages.drawio' aws_multiple_pages_drawio_as_json = f'{drawio}/aws_multiple_pages.json' -aws_two_component_multiple_dataflows = f'{drawio}/aws_two_component_multiple_dataflows.json' +aws_two_component_multiple_dataflows_as_json = f'{drawio}/aws_two_component_multiple_dataflows.json' wrong_mxcell_drawio = f'{drawio}/wrong_mxcell.drawio' wrong_mxfile_drawio = f'{drawio}/wrong_mxfile.drawio' wrong_mxgraphmodel_drawio = f'{drawio}/wrong_mxgraphmodel.drawio' diff --git a/slp_drawio/tests/unit/load/test_diagram_dataflow_loader.py b/slp_drawio/tests/unit/load/test_diagram_dataflow_loader.py new file mode 100644 index 00000000..bbfae0f9 --- /dev/null +++ b/slp_drawio/tests/unit/load/test_diagram_dataflow_loader.py @@ -0,0 +1,38 @@ +import json +from unittest.mock import patch + +from sl_util.sl_util.file_utils import get_byte_data +from slp_drawio.slp_drawio.load.diagram_dataflow_loader import DiagramDataflowLoader +from slp_drawio.slp_drawio.load.drawio_dict_utils import get_dataflow_tags +from slp_drawio.tests.resources import test_resource_paths + + +class TestDiagramDataflowLoader: + + @patch('slp_drawio.slp_drawio.load.diagram_dataflow_loader.get_dataflow_tags', wraps=get_dataflow_tags) + def test_load(self, get_dataflow_tags_wrapper): + # GIVEN a DrawIO + source = json.loads(get_byte_data(test_resource_paths.aws_two_component_multiple_dataflows_as_json)) + + # WHEN DiagramDataflowLoader::load + diagram_dataflows = DiagramDataflowLoader(source).load() + + # THEN diagram dataflows has length of 2 + assert len(diagram_dataflows) == 2 + # AND elements has the following information + assert diagram_dataflows[0].otm.id == 'pt2kyrPXSm7H56EBWWGj-6' + assert diagram_dataflows[0].otm.name == 'pt2kyrPXSm7H56EBWWGj-6-dataflow' + assert diagram_dataflows[0].otm.source_node == 'pt2kyrPXSm7H56EBWWGj-1' + assert diagram_dataflows[0].otm.destination_node == 'pt2kyrPXSm7H56EBWWGj-7' + assert len(diagram_dataflows[0].otm.tags) == 1 + assert diagram_dataflows[0].otm.tags[0] == 'Dataflow Info' + + # AND self reference dataflow is also mapped + assert diagram_dataflows[1].otm.id == 'pt2kyrPXSm7H56EBWWGj-8' + assert diagram_dataflows[1].otm.name == 'pt2kyrPXSm7H56EBWWGj-8-dataflow' + assert diagram_dataflows[1].otm.source_node == 'pt2kyrPXSm7H56EBWWGj-7' + assert diagram_dataflows[1].otm.destination_node == 'pt2kyrPXSm7H56EBWWGj-7' + assert len(diagram_dataflows[1].otm.tags) == 0 + + # AND the method get_dataflow_tags has been called once for each dataflow + assert get_dataflow_tags_wrapper.call_count == len(diagram_dataflows) diff --git a/slp_drawio/tests/unit/load/test_drawio_dict_utils.py b/slp_drawio/tests/unit/load/test_drawio_dict_utils.py index 76300912..442bc29c 100644 --- a/slp_drawio/tests/unit/load/test_drawio_dict_utils.py +++ b/slp_drawio/tests/unit/load/test_drawio_dict_utils.py @@ -1,6 +1,7 @@ # class TestDrawioDictUtils: import json +from unittest.mock import patch import pytest @@ -48,7 +49,7 @@ def test_is_multiple_pages(source, expected): def test_get_mx_cell_components(): # GIVEN a DrawIO with one component and Dataflow with multiple configurations - source = json.loads(get_byte_data(test_resource_paths.aws_two_component_multiple_dataflows)) + source = json.loads(get_byte_data(test_resource_paths.aws_two_component_multiple_dataflows_as_json)) # WHEN drawio_dict_utils::get_components_from_source components = drawio_dict_utils.get_mx_cell_components(source) @@ -59,15 +60,36 @@ def test_get_mx_cell_components(): def test_get_mx_cell_dataflows(): # GIVEN a DrawIO with one component and Dataflow with multiple configurations - source = json.loads(get_byte_data(test_resource_paths.aws_two_component_multiple_dataflows)) + source = json.loads(get_byte_data(test_resource_paths.aws_two_component_multiple_dataflows_as_json)) # WHEN drawio_dict_utils::get_components_from_source - dataflows = drawio_dict_utils.get_mxcell_dataflows(source) + dataflows = drawio_dict_utils.get_mx_cell_dataflows(source) # THEN it returns 5 dataflows assert len(dataflows) == 5 +@patch('slp_drawio.slp_drawio.load.drawio_dict_utils.__get_mx_cells') +@pytest.mark.parametrize('dataflow_id, source, expected', [ + pytest.param('1', [], [], id="tags are empty"), + pytest.param('1', [{'parent': '2'}, {'parent': '1', 'value': 'tag'}], ['tag'], id="tags has one value"), + pytest.param('1', [{'parent': '1', 'value': 'tag'}, {'parent': '1', 'value': 'tag2'}], ['tag', 'tag2'], + id="tags has two values"), + pytest.param('1', [{'parent': '2', 'value': 'tag'}], [], id="tags is empty as not child exists"), + pytest.param('1', [{'parent': '1'}], [], id="tags is empty as not value exists"), +]) +def test_get_dataflow_tags(__get_mx_cells_mock, dataflow_id: str, source, expected): + # GIVEN the dataflow_id + # AND the get_mx_cells returning the given source + __get_mx_cells_mock.return_value = source + + # WHEN drawio_dict_utils::get_dataflow_tags + tags = drawio_dict_utils.get_dataflow_tags(dataflow_id, {}) + + # THEN the tags are as expected + assert tags == expected + + @pytest.mark.parametrize('mx_geometry, expected', [ pytest.param({}, {'x': 0, 'y': 0}, id="without position"), pytest.param({'x': '10'}, {'x': 10, 'y': 0}, id="only with x value"),