diff --git a/docs/package-lock.json b/docs/package-lock.json index b34fddc9..b4149f83 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -4243,12 +4243,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -4256,7 +4256,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -4424,13 +4424,18 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4934,9 +4939,9 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" }, "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "engines": { "node": ">= 0.6" } @@ -5949,16 +5954,19 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -6319,6 +6327,25 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", @@ -6571,16 +6598,16 @@ } }, "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.1", + "body-parser": "1.20.2", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.5.0", + "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", @@ -6896,9 +6923,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -7131,15 +7158,19 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", "dependencies": { + "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7397,11 +7428,11 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -12771,9 +12802,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -13909,14 +13940,16 @@ } }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -13987,13 +14020,17 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" diff --git a/translator/README.md b/translator/README.md index 10e9bb0c..7cfb52cc 100644 --- a/translator/README.md +++ b/translator/README.md @@ -9,6 +9,6 @@ The OpenAPI Specification is available at ( | +| Format | Endpoint | Description | +|----------------|---------------|--------------------------------------------------------------------------------------------------------------------| +| Structurizr-C4 | /translate/c4 | Produces a Workspace json object with generated default views that can be imported into | diff --git a/translator/src/main/java/org/finos/calmtranslator/controller/Translator.java b/translator/src/main/java/org/finos/calmtranslator/controller/Translator.java index 854ce853..95f73f84 100644 --- a/translator/src/main/java/org/finos/calmtranslator/controller/Translator.java +++ b/translator/src/main/java/org/finos/calmtranslator/controller/Translator.java @@ -1,7 +1,6 @@ package org.finos.calmtranslator.controller; import com.structurizr.Workspace; -import com.structurizr.util.WorkspaceUtils; import org.finos.calmtranslator.calm.Core; import org.finos.calmtranslator.translators.C4ModelTranslator; @@ -24,12 +23,7 @@ public Translator(C4ModelTranslator c4ModelTranslator) { @PostMapping("/c4") @ResponseStatus(HttpStatus.CREATED) - public String c4Translation( - @RequestBody Core calmModel - - ) throws Exception { - // Currently a Structurizr json format - final Workspace workspace = c4ModelTranslator.translate(calmModel); - return WorkspaceUtils.toJson(workspace, true); + public Workspace c4Translation(@RequestBody Core calmModel) { + return c4ModelTranslator.translate(calmModel); } } diff --git a/translator/src/main/java/org/finos/calmtranslator/translators/C4ModelTranslator.java b/translator/src/main/java/org/finos/calmtranslator/translators/C4ModelTranslator.java index 382d8690..f3289025 100644 --- a/translator/src/main/java/org/finos/calmtranslator/translators/C4ModelTranslator.java +++ b/translator/src/main/java/org/finos/calmtranslator/translators/C4ModelTranslator.java @@ -1,17 +1,16 @@ package org.finos.calmtranslator.translators; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; import com.structurizr.Workspace; import com.structurizr.model.Container; import com.structurizr.model.Model; import com.structurizr.model.Person; import com.structurizr.model.SoftwareSystem; +import org.finos.calmtranslator.calm.ComposedOfType; import org.finos.calmtranslator.calm.ConnectsType; import org.finos.calmtranslator.calm.Core; import org.finos.calmtranslator.calm.InteractsType; @@ -23,30 +22,20 @@ import org.springframework.stereotype.Service; /** - * Convert CALM to a C4 Model + * */ @Service public class C4ModelTranslator implements ModelTranslator { private static final Logger LOG = LoggerFactory.getLogger(C4ModelTranslator.class); - private Core calmModel; - private SoftwareSystem softwareSystem; - @Override public Workspace translate(final Core calmModel) { - this.calmModel = calmModel; - final Workspace c4Workspace = new Workspace("calm-to-c4", "calm to c4"); + final Workspace c4Workspace = new Workspace("calm-to-c4", "CALM model to c4 model"); final Model c4Model = c4Workspace.getModel(); - Map nodeNameToC4Id = new HashMap<>(); - final Map> systemMap = this.getSystemNodeRelationships(calmModel); - systemMap.forEach((systemNode, relationshipNodes) -> { - this.softwareSystem = c4Model.addSoftwareSystem(systemNode.getName(), systemNode.getDescription()); - nodeNameToC4Id.put(softwareSystem.getName(), softwareSystem.getId()); - relationshipNodes - .forEach(node -> addNodeAsContainer(node, softwareSystem, nodeNameToC4Id)); - }); + Map nodeNameToC4Id = new HashMap<>(calmModel.getNodes().size()); + // Add all persons calmModel.getNodes().forEach(node -> { if (Objects.requireNonNull(node.getNodeType()) == Node.NodeTypeDefinition.ACTOR) { final Person person = c4Model.addPerson(node.getName(), node.getDescription()); @@ -54,116 +43,121 @@ public Workspace translate(final Core calmModel) { } }); - calmModel.getRelationships() - .forEach(relationship -> { - final RelationshipType relationshipType = relationship.getRelationshipType(); - if (Objects.nonNull(relationshipType.getInteracts())) { - final InteractsType interacts = relationshipType.getInteracts(); - final String actorStr = interacts.getActor(); - final List nodes = interacts.getNodes(); - final Node actorNode = getNodeFromUniqueName(actorStr); - final Person actor = c4Model.getPersonWithName(actorNode.getName()); - for (String node : nodes) { - final String id = nodeNameToC4Id.get(this.getNodeFromUniqueName(node).getName()); - actor.uses((Container) c4Model.getElement(id), relationship.getDescription()); - } - } - else if (Objects.nonNull(relationshipType.getConnects())) { - - final ConnectsType connects = relationshipType.getConnects(); - final Node sourceNode = getNodeFromUniqueName(connects.getSource()); - final Node destinationNode = getNodeFromUniqueName(connects.getDestination()); - - String srcC4Id = nodeNameToC4Id.get(sourceNode.getName()); - if (Objects.isNull(srcC4Id)) { - LOG.warn("Adding new source node that does not exist in composed-of [{}]", sourceNode); - addNodeAsContainer(sourceNode, softwareSystem, nodeNameToC4Id); - srcC4Id = nodeNameToC4Id.get(sourceNode.getName()); - } - String dstC4Id = nodeNameToC4Id.get(destinationNode.getName()); - if (Objects.isNull(dstC4Id)) { - LOG.warn("Adding new destination node that does not exist in composed-of [{}]", destinationNode); - addNodeAsContainer(destinationNode, softwareSystem, nodeNameToC4Id); - dstC4Id = nodeNameToC4Id.get(destinationNode.getName()); - } - final Container sContainer = (Container) c4Model.getElement(srcC4Id); - final Container dContainer = (Container) c4Model.getElement(dstC4Id); - - if (Objects.isNull(relationship.getProtocol())) { - sContainer.uses(dContainer, relationship.getDescription()); - } - else { - sContainer.uses(dContainer, relationship.getDescription(), relationship.getProtocol().value()); - } - } - else if (Objects.nonNull(relationshipType.getComposedOf())) { - // The top level is defined ahead of time - } - else if (Objects.nonNull(relationshipType.getDeployedIn())) { - // We don't use the network boundaries in C4 - } - else { - throw new RuntimeException("Unknown relationship type"); - } - }); - addViewsToWorkspace(c4Workspace); - return c4Workspace; - } - - private void addViewsToWorkspace(final Workspace workspace) { - workspace.getViews().createDefaultViews(); - } - - private static void addNodeAsContainer(final Node node, final SoftwareSystem softwareSystem, final Map nodeNameToC4Id) { - final Container container = softwareSystem.addContainer(node.getName(), node.getDescription(), node.getNodeType().value()); - nodeNameToC4Id.put(container.getName(), container.getId()); - } + // Add all containers to the software systems + for (var relationships : calmModel.getRelationships()) { + final ComposedOfType composedOf = relationships.getRelationshipType().getComposedOf(); + if (composedOf == null) { + continue; + } - private Map> getSystemNodeRelationships(final Core calmModel) { - final List connections = new ArrayList<>(); - final List interacts = new ArrayList<>(); - final List deployedIn = new ArrayList<>(); - final List composedOf = new ArrayList<>(); + final Node containerSystemNode = getNodeFromUniqueName(calmModel, composedOf.getContainer()); + LOG.info("Add new Software system [{}], unique-id [{}]", containerSystemNode.getName(), containerSystemNode.getUniqueId()); + final SoftwareSystem softwareSystem = addNodeAsSoftwareSystem(containerSystemNode, c4Model, nodeNameToC4Id); + final List containedNodes = composedOf.getNodes(); + for (String containedNodeStr : containedNodes) { + final Node containedNode = getNodeFromUniqueName(calmModel, containedNodeStr); + LOG.info("Adding node [{}] with unique-id [{}] to software system [{}]", containedNode.getName(), containedNode.getUniqueId(), containerSystemNode.getName()); + addNodeAsContainer(containedNode, softwareSystem, nodeNameToC4Id); + } + } - calmModel.getRelationships().forEach(relationship -> { + // Add all the relationships + for (var relationship : calmModel.getRelationships()) { final RelationshipType relationshipType = relationship.getRelationshipType(); if (Objects.nonNull(relationshipType.getInteracts())) { - interacts.add(relationship); + addInteractsRelationship(relationship, calmModel, c4Model, nodeNameToC4Id); } else if (Objects.nonNull(relationshipType.getConnects())) { - connections.add(relationship); + addConnectsRelationship(relationship, calmModel, c4Model, nodeNameToC4Id); + } + } + addViewsToWorkspace(c4Workspace); + return c4Workspace; + } + + private void addConnectsRelationship(Relationship relationship, final Core calmModel, final Model c4Model, final Map nodeNameToC4Id) { + final RelationshipType relationshipType = relationship.getRelationshipType(); + final ConnectsType connects = relationshipType.getConnects(); + final Node sourceNode = getNodeFromUniqueName(calmModel, connects.getSource()); + final Node destinationNode = getNodeFromUniqueName(calmModel, connects.getDestination()); + + String srcC4Id = nodeNameToC4Id.get(sourceNode.getName()); + if (Objects.isNull(srcC4Id)) { + LOG.warn("Adding unknown source node as a new Software System [{}]", sourceNode.getName()); + addNodeAsSoftwareSystem(sourceNode, c4Model, nodeNameToC4Id); + srcC4Id = nodeNameToC4Id.get(destinationNode.getName()); + } + String dstC4Id = nodeNameToC4Id.get(destinationNode.getName()); + if (Objects.isNull(dstC4Id)) { + LOG.warn("Adding unknown destination node as a new Software System [{}]", destinationNode.getName()); + addNodeAsSoftwareSystem(destinationNode, c4Model, nodeNameToC4Id); + dstC4Id = nodeNameToC4Id.get(destinationNode.getName()); + } + + //Add relationship + final SoftwareSystem sourceSoftwareSystem = c4Model.getSoftwareSystemWithId(srcC4Id); + final SoftwareSystem destinationSoftwareSystem = c4Model.getSoftwareSystemWithId(dstC4Id); + if (sourceSoftwareSystem == null) { + final Container sContainer = (Container) c4Model.getElement(srcC4Id); + if (destinationSoftwareSystem == null) { + final Container dContainer = (Container) c4Model.getElement(dstC4Id); + sContainer.uses(dContainer, + relationship.getDescription(), + Objects.isNull(relationship.getProtocol()) ? null : relationship.getProtocol().value()); } - else if (Objects.nonNull(relationshipType.getComposedOf())) { - composedOf.add(relationship); + else { + sContainer.uses(destinationSoftwareSystem, + relationship.getDescription(), + Objects.isNull(relationship.getProtocol()) ? null : relationship.getProtocol().value()); } - else if (Objects.nonNull(relationshipType.getDeployedIn())) { - deployedIn.add(relationship); + } + else { + if (destinationSoftwareSystem == null) { + final Container dContainer = (Container) c4Model.getElement(dstC4Id); + sourceSoftwareSystem.uses(dContainer, + relationship.getDescription(), + Objects.isNull(relationship.getProtocol()) ? null : relationship.getProtocol().value()); } else { - throw new RuntimeException("Unknown relationship type"); + sourceSoftwareSystem.uses(destinationSoftwareSystem, + relationship.getDescription(), + Objects.isNull(relationship.getProtocol()) ? null : relationship.getProtocol().value()); } - }); + } + } - return containsSystem(composedOf); + private SoftwareSystem addNodeAsSoftwareSystem(final Node node, Model c4Model, final Map nodeNameToC4Id) { + final SoftwareSystem softwareSystem = c4Model.addSoftwareSystem(node.getName(), node.getDescription()); + nodeNameToC4Id.put(softwareSystem.getName(), softwareSystem.getId()); + return softwareSystem; } - private Map> containsSystem(final List composedOf) { - final Map> systemComposedOf = new HashMap<>(); - composedOf.forEach(relationship -> { - final String systemContainerName = relationship.getRelationshipType().getComposedOf().getContainer(); - final Node systemContainerNode = getNodeFromUniqueName(systemContainerName); - final List containedInNodes = relationship.getRelationshipType().getComposedOf().getNodes().stream() - .map(this::getNodeFromUniqueName) - .collect(Collectors.toList()); - systemComposedOf.put(systemContainerNode, containedInNodes); - }); - return systemComposedOf; + private void addInteractsRelationship(Relationship relationship, final Core calmModel, final Model c4Model, final Map nodeNameToC4Id) { + final InteractsType interacts = relationship.getRelationshipType().getInteracts(); + final String actorStr = interacts.getActor(); + final List nodes = interacts.getNodes(); + final Node actorNode = getNodeFromUniqueName(calmModel, actorStr); + final Person actor = c4Model.getPersonWithName(actorNode.getName()); + for (String node : nodes) { + final String id = nodeNameToC4Id.get(this.getNodeFromUniqueName(calmModel, node).getName()); + actor.uses((Container) c4Model.getElement(id), relationship.getDescription()); + } + } + + private void addViewsToWorkspace(final Workspace workspace) { + workspace.getViews().createDefaultViews(); + } + + private static Container addNodeAsContainer(final Node node, final SoftwareSystem softwareSystem, final Map nodeNameToC4Id) { + final Container container = softwareSystem.addContainer(node.getName(), node.getDescription(), node.getNodeType().value()); + nodeNameToC4Id.put(container.getName(), container.getId()); + return container; } - private Node getNodeFromUniqueName(final String systemContainer) { - LOG.debug("Looking up container [{}]", systemContainer); + private Node getNodeFromUniqueName(final Core calmModel, final String nodeUniqueName) { + LOG.debug("Looking up container [{}]", nodeUniqueName); return calmModel.getNodes().stream() - .filter(node -> systemContainer.equals(node.getUniqueId())) + .filter(node -> nodeUniqueName.equals(node.getUniqueId())) .findFirst().get(); } } diff --git a/translator/src/main/java/org/finos/calmtranslator/translators/ModelTranslator.java b/translator/src/main/java/org/finos/calmtranslator/translators/ModelTranslator.java index 70e7866c..361855f8 100644 --- a/translator/src/main/java/org/finos/calmtranslator/translators/ModelTranslator.java +++ b/translator/src/main/java/org/finos/calmtranslator/translators/ModelTranslator.java @@ -3,8 +3,8 @@ import org.finos.calmtranslator.calm.Core; /** - * An interface to translate from CALM Model - * @param + * An interface to translate from CALM Model to the defined type + * @param The output of type of the translated CALM model */ public interface ModelTranslator { @@ -12,8 +12,8 @@ public interface ModelTranslator { * * Convert the CALM Model to the Model Type * - * @param calmModel - * @return + * @param calmModel CALM model to translate + * @return the translated type */ T translate(Core calmModel); } diff --git a/translator/src/test/java/org/finos/calmtranslator/controller/TestTranslatorShould.java b/translator/src/test/java/org/finos/calmtranslator/controller/TestTranslatorShould.java index b22d3825..cd204c8b 100644 --- a/translator/src/test/java/org/finos/calmtranslator/controller/TestTranslatorShould.java +++ b/translator/src/test/java/org/finos/calmtranslator/controller/TestTranslatorShould.java @@ -51,8 +51,8 @@ void return_c4_json_when_a_valid_calm_model_is_passed() throws Exception { ) .andExpect(status().isCreated()) - .andExpect(content().contentType(MediaType.APPLICATION_JSON)); -// .andExpect(content().json()); + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(content().json("{\"id\":0,\"configuration\":{},\"model\":{},\"documentation\":{},\"views\":{\"configuration\":{\"branding\":{},\"styles\":{},\"terminology\":{}}}}")); } @Test diff --git a/translator/src/test/java/org/finos/calmtranslator/translators/TestC4ModelTranslatorShould.java b/translator/src/test/java/org/finos/calmtranslator/translators/TestC4ModelTranslatorShould.java index 21d0016a..0cc6633f 100644 --- a/translator/src/test/java/org/finos/calmtranslator/translators/TestC4ModelTranslatorShould.java +++ b/translator/src/test/java/org/finos/calmtranslator/translators/TestC4ModelTranslatorShould.java @@ -19,11 +19,14 @@ class TestC4ModelTranslatorShould { private C4ModelTranslator translator; private Core traderxCalmModel; + private Core crossCommunicationModel; @BeforeEach void setUp() throws IOException { this.translator = new C4ModelTranslator(); this.traderxCalmModel = new ObjectMapper().readValue(this.getClass().getClassLoader().getResourceAsStream("traderx-calm.json"), Core.class); + this.crossCommunicationModel = new ObjectMapper().readValue(this.getClass().getClassLoader() + .getResourceAsStream("cross-communication.json"), Core.class); } @Test @@ -42,9 +45,10 @@ void addCalmActorAsC4Person() { void addCalmSystemAsC4System() { final Workspace workspace = translator.translate(traderxCalmModel); assertThat(workspace.getModel().getSoftwareSystems()) - .singleElement() - .matches(softwareSystem -> softwareSystem.getDescription().equals("Simple Trading System") && - softwareSystem.getName().equals("TraderX"), "System compare") + .anyMatch( + softwareSystem -> softwareSystem.getDescription().equals("Simple Trading System") && + softwareSystem.getName().equals("TraderX")) + ; } @@ -80,7 +84,6 @@ void addCalmRelationshipConnectionsAsC4Interaction() { final Container tradeProcessor = expectedSoftwareSystem.addContainer("Trade Processor", "Process incoming trade requests, settle and persist"); tradeProcessor.uses(tradeFeed, "Processes incoming trade requests, persist and publish updates.", "SocketIO"); - final Iterable expectedRelationship = model.getRelationships(); final Workspace workspace = translator.translate(traderxCalmModel); assertThat(workspace.getModel().getRelationships()) @@ -99,7 +102,26 @@ void addCalmRelationshipsInteractionAsC4Interaction() { final Iterable expectedRelationship = model.getRelationships(); final Workspace workspace = translator.translate(traderxCalmModel); assertThat(workspace.getModel().getRelationships()) - .usingRecursiveFieldByFieldElementComparatorOnFields("description","source.name", "destination.name", "source.description", "destination.description") + .usingRecursiveFieldByFieldElementComparatorOnFields("description", "source.name", "destination.name", "source.description", "destination.description") .containsAll(expectedRelationship); } + + @Test + void processMultipleSystems() { + final Model model = new Workspace("", "").getModel(); + final SoftwareSystem system1 = model.addSoftwareSystem("System1"); + final Container service1 = system1.addContainer("Service1", "service1", "service"); + final SoftwareSystem system2 = model.addSoftwareSystem("System2"); + final Container service2 = system2.addContainer("Service2", "service2", "service"); + final Container service3 = system2.addContainer("Service3", "service3", "service"); + + service1.uses(service2, "Service 1 to Service 2"); + service2.uses(service3, "Service 2 to Service 3"); + final Iterable expectedRelationship = model.getRelationships(); + final Workspace workspace = translator.translate(crossCommunicationModel); + assertThat(workspace.getModel().getRelationships()) + .usingRecursiveFieldByFieldElementComparatorOnFields("description", "source.name", "destination.name", "source.description", "destination.description") + .containsAll(expectedRelationship); + } + } diff --git a/translator/src/test/resources/cross-communication.json b/translator/src/test/resources/cross-communication.json new file mode 100644 index 00000000..c78f9697 --- /dev/null +++ b/translator/src/test/resources/cross-communication.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://raw.githubusercontent.com/finos-labs/architecture-as-code/main/calm/draft/2024-03/meta/calm.json", + "nodes": [ + { + "unique-id": "system1", + "node-type": "system", + "name": "System1", + "description": "System1" + }, + { + "unique-id": "system2", + "node-type": "system", + "name": "System2", + "description": "System2" + }, + { + "unique-id": "service1", + "node-type": "service", + "name": "Service1", + "description": "service1", + "data-classification": "Confidential", + "run-as": "systemId" + }, + { + "unique-id": "service2", + "node-type": "service", + "name": "Service2", + "description": "service2", + "data-classification": "Confidential", + "run-as": "systemId" + }, + { + "unique-id": "service3", + "node-type": "service", + "name": "Service3", + "description": "service3", + "data-classification": "Confidential", + "run-as": "systemId" + } + ], + "relationships": [ + { + "unique-id": "system1-composed-of", + "relationship-type": { + "composed-of": { + "container": "system1", + "nodes": [ + "service1" + ] + } + } + }, + { + "unique-id": "system2-composed-of", + "relationship-type": { + "composed-of": { + "container": "system2", + "nodes": [ + "service2", + "service3" + ] + } + } + }, + { + "unique-id": "service1-to-service2", + "description": "Service 1 to Service 2", + "relationship-type": { + "connects": { + "source": "service1", + "destination": "service2" + } + } + }, + { + "unique-id": "service2-to-service3", + "description": "Service 2 to Service 3", + "relationship-type": { + "connects": { + "source": "service2", + "destination": "service3" + } + } + } + ] +}