Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

visualizer works with patterns #157

Merged
merged 3 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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