diff --git a/README.md b/README.md
index 7daebb6c..00c2a5f7 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,6 @@ your project.
| [CLI](./cli) | @aidanm3341, @lbulanti-ms, @willosborne | [![Build CLI](https://github.com/finos-labs/architecture-as-code/actions/workflows/cli-tests.yml/badge.svg)](https://github.com/finos-labs/architecture-as-code/actions/workflows/cli-tests.yml) |
| [Spectral](./spectral) | @willosborne, @lbulanti-ms | [![Validation of CALM Samples](https://github.com/finos-labs/architecture-as-code/actions/workflows/spectral-validation.yml/badge.svg)](https://github.com/finos-labs/architecture-as-code/actions/workflows/spectral-validation.yml) |
| [Translators](./translator) | @Budlee @matthewgardner @jpgough-ms | [![Build Translators](https://github.com/finos-labs/architecture-as-code/actions/workflows/translator.yml/badge.svg)](https://github.com/finos-labs/architecture-as-code/actions/workflows/translator.yml) |
-| [Visualizer](./cli/visualizer) | @aidanm3341, @Budlee, @willosborne | |
## Getting Involved
diff --git a/calm/pattern/api-gateway.json b/calm/pattern/api-gateway.json
index 68ea9868..c4ddb47c 100644
--- a/calm/pattern/api-gateway.json
+++ b/calm/pattern/api-gateway.json
@@ -157,8 +157,12 @@
"relationship-type": {
"const": {
"connects": {
- "source": "api-gateway",
- "destination": "idp"
+ "source": {
+ "node": "api-gateway"
+ },
+ "destination": {
+ "node": "idp"
+ }
}
}
},
diff --git a/calm/samples/traderx/traderx.json b/calm/samples/traderx/traderx.json
index ba6d7ca8..3e69514d 100644
--- a/calm/samples/traderx/traderx.json
+++ b/calm/samples/traderx/traderx.json
@@ -309,10 +309,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "web-gui-process"
+ "node": "web-gui-process"
},
"destination": {
- "nodes": "trade-feed"
+ "node": "trade-feed"
}
}
},
@@ -324,10 +324,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "trade-processor"
+ "node": "trade-processor"
},
"destination": {
- "nodes": "trade-feed"
+ "node": "trade-feed"
}
}
},
@@ -339,10 +339,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "trade-processor"
+ "node": "trade-processor"
},
"destination": {
- "nodes": "traderx-db"
+ "node": "traderx-db"
}
}
},
@@ -354,10 +354,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "web-gui-process"
+ "node": "web-gui-process"
},
"destination": {
- "nodes": "accounts-service"
+ "node": "accounts-service"
}
}
},
@@ -369,10 +369,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "web-gui-process"
+ "node": "web-gui-process"
},
"destination": {
- "nodes": "people-service"
+ "node": "people-service"
}
}
},
@@ -384,10 +384,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "people-service"
+ "node": "people-service"
},
"destination": {
- "nodes": "user-directory"
+ "node": "user-directory"
}
}
},
@@ -399,10 +399,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "trading-services"
+ "node": "trading-services"
},
"destination": {
- "nodes": "reference-data-service"
+ "node": "reference-data-service"
}
}
},
@@ -414,10 +414,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "trading-services"
+ "node": "trading-services"
},
"destination": {
- "nodes": "trade-feed"
+ "node": "trade-feed"
}
}
},
@@ -429,10 +429,10 @@
"relationship-type": {
"connects": {
"source": {
- "nodes": "trading-services"
+ "node": "trading-services"
},
"destination": {
- "nodes": "accounts-service"
+ "node": "accounts-service"
}
}
},
diff --git a/cli/src/commands/generate/generate.ts b/cli/src/commands/generate/generate.ts
index ff2617ab..39680117 100644
--- a/cli/src/commands/generate/generate.ts
+++ b/cli/src/commands/generate/generate.ts
@@ -6,12 +6,13 @@ import { mkdirp } from 'mkdirp';
import * as winston from 'winston';
import { initLogger } from '../helper.js';
+import { CALMInstantiation } from '../../types.js';
let logger: winston.Logger; // defined later at startup
function loadFile(path: string): any {
logger.info('Loading pattern from file: ' + path);
- const raw = fs.readFileSync(path, { encoding: 'utf8' });
+ const raw = fs.readFileSync(path, 'utf-8');
logger.debug('Attempting to load pattern file: ' + raw);
const pattern = JSON.parse(raw);
@@ -138,9 +139,8 @@ export const exportedForTesting = {
instantiateNodeInterfaces
};
-export function runGenerate (patternPath: string, outputPath: string, debug: boolean): void {
+export function generate(patternPath: string, debug: boolean): CALMInstantiation {
logger = initLogger(debug);
-
const pattern = loadFile(patternPath);
const outputNodes = instantiateNodes(pattern);
const relationshipNodes = instantiateRelationships(pattern);
@@ -150,6 +150,12 @@ export function runGenerate (patternPath: string, outputPath: string, debug: boo
'relationships': relationshipNodes,
};
+ return final;
+}
+
+export function runGenerate(patternPath: string, outputPath: string, debug: boolean): void {
+ const final = generate(patternPath, debug);
+
const output = JSON.stringify(final, null, 2);
logger.debug('Generated instantiation: ' + output);
diff --git a/cli/src/commands/visualize/calmToDot.spec.ts b/cli/src/commands/visualize/calmToDot.spec.ts
index 939e64a6..cb0d6f2a 100644
--- a/cli/src/commands/visualize/calmToDot.spec.ts
+++ b/cli/src/commands/visualize/calmToDot.spec.ts
@@ -1,4 +1,4 @@
-import { CALMInstantiation } from './Types';
+import { CALMInstantiation } from '../../types';
import calmToDot from './calmToDot';
jest.mock('../helper.js', () => {
diff --git a/cli/src/commands/visualize/calmToDot.ts b/cli/src/commands/visualize/calmToDot.ts
index 929002ff..7b8ccafe 100644
--- a/cli/src/commands/visualize/calmToDot.ts
+++ b/cli/src/commands/visualize/calmToDot.ts
@@ -1,5 +1,5 @@
import { Digraph, Subgraph, Node, Edge, toDot, attribute } from 'ts-graphviz';
-import { CALMInstantiation, CALMComposedOfRelationship, CALMConnectsRelationship, CALMDeployedInRelationship, CALMInteractsRelationship, CALMRelationship, CALMNode } from './Types';
+import { CALMInstantiation, CALMComposedOfRelationship, CALMConnectsRelationship, CALMDeployedInRelationship, CALMInteractsRelationship, CALMRelationship, CALMNode } from '../../types';
import { initLogger } from '../helper.js';
import winston from 'winston';
@@ -68,10 +68,10 @@ function addConnectsRelationship(g: Digraph, relationship: CALMConnectsRelations
const r = relationship['relationship-type'];
logger.debug(`${JSON.stringify(r)}`);
logger.debug(`Creating a connects relationship from [${sourceId}] to [${destinationId}]`);
-
+
g.addEdge(new Edge([
- idToNode[sourceId],
- idToNode[destinationId]
+ getNode(sourceId),
+ getNode(destinationId)
], {
label: `connects ${relationship.protocol || ''} ${relationship.authentication || ''}`
}));
@@ -84,8 +84,8 @@ function addInteractsRelationship(g: Digraph, relationship: CALMInteractsRelatio
const targetId = maybeId;
g.addEdge(new Edge([
- idToNode[sourceId],
- idToNode[targetId]
+ getNode(sourceId),
+ getNode(targetId)
], {
label: 'interacts'
}));
@@ -100,11 +100,10 @@ function addDeployedInRelationship(g: Digraph, relationship: CALMDeployedInRelat
label: containerId
});
- targetIds.forEach(maybeId => {
- const targetId = maybeId;
+ targetIds.forEach(targetId => {
logger.debug(`Creating a deployed-in relationship from [${containerId}] to [${targetId}]`);
- subgraph.addNode(idToNode[targetId]);
+ subgraph.addNode(getNode(targetId));
});
g.addSubgraph(subgraph);
@@ -118,11 +117,19 @@ function addComposedOfRelationship(g: Digraph, relationship: CALMComposedOfRelat
label: containerId
});
- targetIds.forEach(maybeId => {
- const targetId = maybeId;
+ targetIds.forEach(targetId => {
logger.debug(`Creating a composed-of relationship from [${containerId}] to [${targetId}]`);
- subgraph.addNode(idToNode[targetId]);
+ subgraph.addNode(getNode(targetId));
});
g.addSubgraph(subgraph);
-}
\ No newline at end of file
+}
+
+function getNode(id: string) {
+ const node = idToNode[id];
+ if (!node) {
+ throw new Error(`There does not exist a node with ID [${id}]`);
+ } else {
+ return node;
+ }
+}
\ No newline at end of file
diff --git a/cli/src/commands/visualize/visualize.spec.ts b/cli/src/commands/visualize/visualize.spec.ts
index 95e1b626..1de3db3b 100644
--- a/cli/src/commands/visualize/visualize.spec.ts
+++ b/cli/src/commands/visualize/visualize.spec.ts
@@ -1,6 +1,6 @@
import { readFileSync, writeFileSync } from 'node:fs';
-import visualize from './visualize';
import * as graphviz from 'graphviz-cli';
+import { visualizeInstantiation, visualizePattern } from './visualize';
jest.mock('node:fs', () => {
return {
@@ -28,7 +28,7 @@ jest.mock('../helper.js', () => {
};
});
-describe('visualize', () => {
+describe('visualize instantiation', () => {
beforeEach(() => {
(readFileSync as jest.Mock).mockReturnValue(`
{
@@ -46,21 +46,68 @@ describe('visualize', () => {
it('reads from the given input file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('');
- await visualize('./input.json', './output.svg', false);
+ await visualizeInstantiation('./input.json', './output.svg', false);
expect(readFileSync).toHaveBeenCalledWith('./input.json', 'utf-8');
});
it('writes to the given output file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('');
- await visualize('./input.json', './output.svg', false);
+ await visualizeInstantiation('./input.json', './output.svg', false);
expect(writeFileSync).toHaveBeenCalledWith('./output.svg', '');
});
it('doesnt write if an error is thrown', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockRejectedValue(new Error());
- await visualize('./input.json', './output.svg', false);
+ await visualizeInstantiation('./input.json', './output.svg', false);
+ expect(writeFileSync).not.toHaveBeenCalled();
+ });
+});
+
+describe('visualize pattern', () => {
+ beforeEach(() => {
+ (readFileSync as jest.Mock).mockReturnValue(`
+ {
+ "properties": {
+ "nodes": {
+ "prefixItems": []
+ },
+ "relationships": {
+ "prefixItems": []
+ }
+ },
+ "required": [
+ "nodes",
+ "relationships"
+ ]
+ }
+ `);
+ });
+
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('reads from the given input file', async () => {
+ jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('');
+
+ await visualizePattern('./input.json', './output.svg', false);
+ expect(readFileSync).toHaveBeenCalledWith('./input.json', 'utf-8');
+ });
+
+ it('writes to the given output file', async () => {
+ jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('');
+
+ await visualizePattern('./input.json', './output.svg', false);
+ expect(writeFileSync).toHaveBeenCalledWith('./output.svg', '');
+ });
+
+ it('doesnt write if an error is thrown', async () => {
+ jest.spyOn(graphviz, 'renderGraphFromSource').mockRejectedValue(new Error());
+
+ await visualizePattern('./input.json', './output.svg', false);
expect(writeFileSync).not.toHaveBeenCalled();
});
});
\ No newline at end of file
diff --git a/cli/src/commands/visualize/visualize.ts b/cli/src/commands/visualize/visualize.ts
index 4ea6753a..cca21d46 100644
--- a/cli/src/commands/visualize/visualize.ts
+++ b/cli/src/commands/visualize/visualize.ts
@@ -3,20 +3,45 @@ import * as winston from 'winston';
import { initLogger } from '../helper.js';
import calmToDot from './calmToDot.js';
import { renderGraphFromSource } from 'graphviz-cli';
+import { generate } from '../generate/generate.js';
let logger: winston.Logger;
-export default async function(input: string, output: string, debug: boolean) {
+export async function visualizeInstantiation(instantiationPath: string, output: string, debug: boolean) {
logger = initLogger(debug);
- logger.info(`Reading CALM file from [${input}]`);
- const calm = fs.readFileSync(input, 'utf-8');
+ logger.info(`Reading CALM file from [${instantiationPath}]`);
+ const calm = fs.readFileSync(instantiationPath, 'utf-8');
logger.info('Generating an SVG from input');
try {
const dot = calmToDot(JSON.parse(calm), debug);
- logger.debug(`Creating the following dot:
+ logger.debug(`Generated the following dot:
+ ${dot}
+ `);
+
+ const svg = await renderGraphFromSource({ input: dot }, { format: 'svg', engine: 'dot' });
+
+ logger.info(`Outputting file at [${output}]`);
+ fs.writeFileSync(output, svg);
+ return;
+ } catch (err) {
+ logger.error(err);
+ return;
+ }
+}
+
+export async function visualizePattern(patternPath: string, output: string, debug: boolean) {
+ logger = initLogger(debug);
+
+ const instantiation = generate(patternPath, debug);
+
+ logger.info('Generating an SVG from input');
+
+ try {
+ const dot = calmToDot(instantiation, debug);
+ logger.debug(`Generated the following dot:
${dot}
`);
diff --git a/cli/src/index.ts b/cli/src/index.ts
index 98fe5c05..86b70309 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -1,7 +1,7 @@
#! /usr/bin/env node
-import { program } from 'commander';
-import visualize from './commands/visualize/visualize.js';
+import { Option, program } from 'commander';
+import { visualizeInstantiation, visualizePattern } from './commands/visualize/visualize.js';
import { runGenerate } from './commands/generate/generate.js';
import validate from './commands/validate/validate.js';
@@ -12,11 +12,18 @@ program
program
.command('visualize')
.description('Produces an SVG file representing a visualization of the CALM Specification.')
- .requiredOption('-i, --instantiation ', 'Path to an instantiation of a CALM pattern.')
+ .addOption(new Option('-i, --instantiation ', 'Path to an instantiation of a CALM pattern.').conflicts('pattern'))
+ .addOption(new Option('-p, --pattern ', 'Path to a CALM pattern.').conflicts('instantiation'))
.requiredOption('-o, --output ', 'Path location at which to output the SVG.', 'calm-visualization.svg')
.option('-v, --verbose', 'Enable verbose logging.', false)
- .action((options) => {
- visualize(options.instantiation, options.output, !!options.verbose);
+ .action(async (options) => {
+ if (!options.instantiation && ! options.pattern) {
+ throw new Error('You must provide either a pattern or an instantiation');
+ } else if (options.instantiation) {
+ await visualizeInstantiation(options.instantiation, options.output, !!options.verbose);
+ } else if (options.pattern) {
+ await visualizePattern(options.pattern, options.output, !!options.verbose);
+ }
});
program
diff --git a/cli/src/commands/visualize/Types.d.ts b/cli/src/types.d.ts
similarity index 100%
rename from cli/src/commands/visualize/Types.d.ts
rename to cli/src/types.d.ts