Skip to content

Commit

Permalink
Merge pull request #193 from aidanm3341/visualizer-fix
Browse files Browse the repository at this point in the history
Visualizer fix
  • Loading branch information
aidanm3341 authored May 15, 2024
2 parents f22ad75 + 6b1086b commit 032a928
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 87 deletions.
15 changes: 10 additions & 5 deletions .github/workflows/cli-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ name: Build CLI

on:
pull_request:
branches:
branches:
- "main"
paths:
paths:
- "cli/**"
push:
branches:
branches:
- "main"
paths:
paths:
- "cli/**"

defaults:
Expand All @@ -28,4 +28,9 @@ jobs:
node-version: v20
- run: npm ci
- run: npm run test
- run: npm run lint
- run: npm run lint
- run: npm run build
# Try running each command
- run: npx calm validate -p ../calm/pattern/api-gateway.json -i ../calm/samples/api-gateway-implementation.json
- run: npx calm generate -p ../calm/pattern/api-gateway.json -s ../calm/draft/2024-04/meta
- run: npx calm visualize -p ../calm/pattern/api-gateway.json
20 changes: 12 additions & 8 deletions cli/src/commands/generate/components/property.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

export function getStringPlaceholder(name: string): string {
return '{{ ' + name.toUpperCase().replaceAll('-', '_') + ' }}';
}
Expand All @@ -8,16 +6,22 @@ export function getRefPlaceholder(name: string): string {
return '{{ REF_' + name.toUpperCase().replaceAll('-', '_') + ' }}';
}

export function getPropertyValue(keyName: string, detail: any): any {
interface Detail {
const?: string | object,
type?: 'string' | 'integer' | 'number' | 'array',
$ref?: string
}

export function getPropertyValue(keyName: string, detail: Detail): string | string[] | number | object {
// TODO follow refs here
// should be able to instantiate not just a simple enum type but also a whole sub-object
// if both const and type are defined, prefer const
if ('const' in detail) {
return detail['const'];
if (detail.const) {
return detail.const;
}

if ('type' in detail) {
const propertyType = detail['type'];
if (detail.type) {
const propertyType = detail.type;

if (propertyType === 'string') {
return getStringPlaceholder(keyName);
Expand All @@ -32,7 +36,7 @@ export function getPropertyValue(keyName: string, detail: any): any {
}
}

if ('$ref' in detail) {
if (detail.$ref) {
return getRefPlaceholder(keyName);
}
}
2 changes: 2 additions & 0 deletions cli/src/commands/visualize/calmToDot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function(calm: CALMInstantiation, debug: boolean = false): string

calm.nodes.forEach(node => {
logger.debug(`Creating a node with ID [${node['unique-id']}]`);
logger.debug(`Using the following node [${JSON.stringify(node)}]`);
addNode(G, node);
});
calm.relationships.forEach(relationship => {
Expand Down Expand Up @@ -50,6 +51,7 @@ function capitalizeFirstLetter(s: string): string {
}

function addRelationship(g: Digraph, relationship: CALMRelationship): void {
logger.debug(`Using the following relationship [${JSON.stringify(relationship)}]`);
if ('connects' in relationship['relationship-type']) {
addConnectsRelationship(g, relationship as CALMConnectsRelationship);
} else if ('interacts' in relationship['relationship-type']) {
Expand Down
156 changes: 89 additions & 67 deletions cli/src/commands/visualize/visualize.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,86 +28,108 @@ jest.mock('../helper.js', () => {
};
});

describe('visualize instantiation', () => {
beforeEach(() => {
(readFileSync as jest.Mock).mockReturnValue(`
{
"nodes": [],
"relationships": []
}
`);
});


afterEach(() => {
jest.resetAllMocks();
});
describe('visualizer', () => {
let mockExit;

it('reads from the given input file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<svg></svg>');

await visualizeInstantiation('./input.json', './output.svg', false);
expect(readFileSync).toHaveBeenCalledWith('./input.json', 'utf-8');
beforeEach(() => {
mockExit = jest.spyOn(process, 'exit')
.mockImplementation((code) => {
if (code != 0) {
throw new Error();
}
return undefined as never;
});
});

it('writes to the given output file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<svg></svg>');

await visualizeInstantiation('./input.json', './output.svg', false);
expect(writeFileSync).toHaveBeenCalledWith('./output.svg', '<svg></svg>');
});
describe('visualize instantiation', () => {

it('doesnt write if an error is thrown', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockRejectedValue(new Error());
beforeEach(() => {
(readFileSync as jest.Mock).mockReturnValue(`
{
"nodes": [],
"relationships": []
}
`);
});

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();
mockExit.mockRestore();
});

it('reads from the given input file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<svg></svg>');

afterEach(() => {
jest.resetAllMocks();
});
await visualizeInstantiation('./input.json', './output.svg', false);
expect(readFileSync).toHaveBeenCalledWith('./input.json', 'utf-8');
});

it('reads from the given input file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<svg></svg>');
it('writes to the given output file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<svg></svg>');

await visualizePattern('./input.json', './output.svg', false);
expect(readFileSync).toHaveBeenCalledWith('./input.json', 'utf-8');
});
await visualizeInstantiation('./input.json', './output.svg', false);
expect(writeFileSync).toHaveBeenCalledWith('./output.svg', '<svg></svg>');
});

it('writes to the given output file', async () => {
jest.spyOn(graphviz, 'renderGraphFromSource').mockResolvedValue('<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).toHaveBeenCalledWith('./output.svg', '<svg></svg>');
await expect(visualizeInstantiation('./input.json', './output.svg', false))
.rejects
.toThrow();
expect(writeFileSync).not.toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(1);
});
});

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();
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 expect(visualizePattern('./input.json', './output.svg', false))
.rejects
.toThrow();
expect(writeFileSync).not.toHaveBeenCalled();
expect(mockExit).toHaveBeenCalledWith(1);
});
});
});
8 changes: 3 additions & 5 deletions cli/src/commands/visualize/visualize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,9 @@ export async function visualizeInstantiation(instantiationPath: string, output:

logger.info(`Outputting file at [${output}]`);
fs.writeFileSync(output, svg);
return;
} catch (err) {
logger.error(err);
return;
process.exit(1);
}
}

Expand All @@ -38,7 +37,7 @@ export async function visualizePattern(patternPath: string, output: string, debu

// TODO add a path to load schemas and generate intelligently
const schemaDir: SchemaDirectory = new SchemaDirectory(patternPath);
const instantiation = generate(patternPath, schemaDir, debug, false);
const instantiation = generate(patternPath, schemaDir, debug, true);

logger.info('Generating an SVG from input');

Expand All @@ -52,9 +51,8 @@ export async function visualizePattern(patternPath: string, output: string, debu

logger.info(`Outputting file at [${output}]`);
fs.writeFileSync(output, svg);
return;
} catch (err) {
logger.error(err);
return;
process.exit(1);
}
}
4 changes: 2 additions & 2 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ program
.command('generate')
.description('Generate an instantiation from a CALM pattern file.')
.requiredOption('-p, --pattern <source>', 'Path to the pattern file to use. May be a file path or a URL.')
.requiredOption('-o, --output <output>', 'Path location at which to output the generated file.')
.requiredOption('-o, --output <output>', 'Path location at which to output the generated file.', 'instantiation.json')
.option('-s, --schemaDirectory <path>', 'Path to directory containing schemas to use in instantiation')
.option('-v, --verbose', 'Enable verbose logging.', false)
.option('-a, --instantiateAll', 'Instantiate all properties, ignoring the "required" field.', false)
Expand All @@ -42,7 +42,7 @@ program
.command('validate')
.requiredOption('-p, --pattern <pattern>', 'Path to the pattern file to use. May be a file path or a URL.')
.requiredOption('-i, --instantiation <instantiation>', 'Path to the pattern instantiation file to use. May be a file path or a URL.')
.option('-m, --metaSchemasLocation <metaSchemaLocation>', 'The location of the directory of the meta schemas to be loaded', '../calm/draft/2024-03/meta')
.option('-m, --metaSchemasLocation <metaSchemaLocation>', 'The location of the directory of the meta schemas to be loaded', '../calm/draft/2024-04/meta')
.addOption(
new Option('-f, --format <format>', 'The format of the output')
.choices(['json', 'junit'])
Expand Down

0 comments on commit 032a928

Please sign in to comment.