Skip to content

Commit

Permalink
Merge pull request #157 from aidanm3341/visualize-patterns
Browse files Browse the repository at this point in the history
 visualizer works with patterns
  • Loading branch information
aidanm3341 committed Apr 19, 2024
2 parents 629a378 + a60b2c0 commit b8e5c9f
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 52 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions calm/pattern/api-gateway.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,12 @@
"relationship-type": {
"const": {
"connects": {
"source": "api-gateway",
"destination": "idp"
"source": {
"node": "api-gateway"
},
"destination": {
"node": "idp"
}
}
}
},
Expand Down
36 changes: 18 additions & 18 deletions calm/samples/traderx/traderx.json
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "web-gui-process"
"node": "web-gui-process"
},
"destination": {
"nodes": "trade-feed"
"node": "trade-feed"
}
}
},
Expand All @@ -324,10 +324,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "trade-processor"
"node": "trade-processor"
},
"destination": {
"nodes": "trade-feed"
"node": "trade-feed"
}
}
},
Expand All @@ -339,10 +339,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "trade-processor"
"node": "trade-processor"
},
"destination": {
"nodes": "traderx-db"
"node": "traderx-db"
}
}
},
Expand All @@ -354,10 +354,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "web-gui-process"
"node": "web-gui-process"
},
"destination": {
"nodes": "accounts-service"
"node": "accounts-service"
}
}
},
Expand All @@ -369,10 +369,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "web-gui-process"
"node": "web-gui-process"
},
"destination": {
"nodes": "people-service"
"node": "people-service"
}
}
},
Expand All @@ -384,10 +384,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "people-service"
"node": "people-service"
},
"destination": {
"nodes": "user-directory"
"node": "user-directory"
}
}
},
Expand All @@ -399,10 +399,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "trading-services"
"node": "trading-services"
},
"destination": {
"nodes": "reference-data-service"
"node": "reference-data-service"
}
}
},
Expand All @@ -414,10 +414,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "trading-services"
"node": "trading-services"
},
"destination": {
"nodes": "trade-feed"
"node": "trade-feed"
}
}
},
Expand All @@ -429,10 +429,10 @@
"relationship-type": {
"connects": {
"source": {
"nodes": "trading-services"
"node": "trading-services"
},
"destination": {
"nodes": "accounts-service"
"node": "accounts-service"
}
}
},
Expand Down
12 changes: 9 additions & 3 deletions cli/src/commands/generate/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/visualize/calmToDot.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CALMInstantiation } from './Types';
import { CALMInstantiation } from '../../types';
import calmToDot from './calmToDot';

jest.mock('../helper.js', () => {
Expand Down
33 changes: 20 additions & 13 deletions cli/src/commands/visualize/calmToDot.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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 || ''}`
}));
Expand All @@ -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'
}));
Expand All @@ -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);
Expand All @@ -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);
}
}

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;
}
}
57 changes: 52 additions & 5 deletions cli/src/commands/visualize/visualize.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -28,7 +28,7 @@ jest.mock('../helper.js', () => {
};
});

describe('visualize', () => {
describe('visualize instantiation', () => {
beforeEach(() => {
(readFileSync as jest.Mock).mockReturnValue(`
{
Expand All @@ -46,21 +46,68 @@ describe('visualize', () => {
it('reads from the given input file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<svg></svg>');

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('<svg></svg>');

await visualize('./input.json', './output.svg', false);
await visualizeInstantiation('./input.json', './output.svg', false);
expect(writeFileSync).toHaveBeenCalledWith('./output.svg', '<svg></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('<svg></svg>');

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('<svg></svg>');

await visualizePattern('./input.json', './output.svg', false);
expect(writeFileSync).toHaveBeenCalledWith('./output.svg', '<svg></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();
});
});
33 changes: 29 additions & 4 deletions cli/src/commands/visualize/visualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
`);

Expand Down
Loading

0 comments on commit b8e5c9f

Please sign in to comment.