Connection Descriptions
diff --git a/calm-visualizer/src/components/sidebar/Sidebar.tsx b/calm-visualizer/src/components/sidebar/Sidebar.tsx
index 6956bff2..53eeba28 100644
--- a/calm-visualizer/src/components/sidebar/Sidebar.tsx
+++ b/calm-visualizer/src/components/sidebar/Sidebar.tsx
@@ -75,7 +75,7 @@ function Sidebar({ selectedData, closeSidebar }: SidebarProps) {
source:
- {selectedData.label}
+ {selectedData.source}
diff --git a/calm-visualizer/src/components/zoom-context.provider.tsx b/calm-visualizer/src/components/zoom-context.provider.tsx
new file mode 100644
index 00000000..d3de268d
--- /dev/null
+++ b/calm-visualizer/src/components/zoom-context.provider.tsx
@@ -0,0 +1,28 @@
+import React, { createContext, useState } from 'react';
+
+type ZoomContextProps = {
+ zoomLevel: number,
+ updateZoom: (newZoomLevel: number) => void
+}
+
+type ZoomProviderProps = { children: React.ReactNode; };
+
+const ZoomContext = createContext({
+ zoomLevel: 1, updateZoom: () => {}
+});
+
+const ZoomProvider = ({children}: ZoomProviderProps) => {
+ const [zoomLevel, setZoomLevel] = useState(1);
+
+ const updateZoom = (newZoomLevel: number) => {
+ setZoomLevel(newZoomLevel);
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { ZoomContext, ZoomProvider };
\ No newline at end of file
diff --git a/calm-visualizer/src/tests/Sidebar.test.tsx b/calm-visualizer/src/tests/Sidebar.test.tsx
index 54d0db95..5004b7e8 100644
--- a/calm-visualizer/src/tests/Sidebar.test.tsx
+++ b/calm-visualizer/src/tests/Sidebar.test.tsx
@@ -24,20 +24,19 @@ describe('Sidebar Component', () => {
render();
expect(screen.getByText('Node Details')).toBeInTheDocument();
- expect(screen.getByText('unique-id: node-1')).toBeInTheDocument();
- expect(screen.getByText('name: Node 1')).toBeInTheDocument();
- expect(screen.getByText('node-type: type-1')).toBeInTheDocument();
- expect(screen.getByText('description: Mock Node')).toBeInTheDocument();
+ expect(screen.getByText('node-1')).toBeInTheDocument();
+ expect(screen.getByText('Node 1')).toBeInTheDocument();
+ expect(screen.getByText('type-1')).toBeInTheDocument();
+ expect(screen.getByText('Mock Node')).toBeInTheDocument();
});
it('should render edge details correctly', () => {
render();
expect(screen.getByText('Edge Details')).toBeInTheDocument();
- expect(screen.getByText('unique-id: edge-1')).toBeInTheDocument();
- expect(screen.getByText('description: Edge 1')).toBeInTheDocument();
- expect(screen.getByText('source: node-1')).toBeInTheDocument();
- expect(screen.getByText('target: node-2')).toBeInTheDocument();
+ expect(screen.getByText('edge-1')).toBeInTheDocument();
+ expect(screen.getByText('node-1')).toBeInTheDocument();
+ expect(screen.getByText('node-2')).toBeInTheDocument();
});
it('should call closeSidebar when close button is clicked', () => {
diff --git a/cli/src/cli.spec.ts b/cli/src/cli.spec.ts
index 37949c61..207dcbc6 100644
--- a/cli/src/cli.spec.ts
+++ b/cli/src/cli.spec.ts
@@ -161,6 +161,27 @@ describe('CLI Integration Tests', () => {
done();
});
});
+
+ test('example validate command - fails when neither an architecture or a pattern is provided', (done) => {
+ const calmValidateCommand = 'calm validate';
+ exec(calmValidateCommand, (error, _stdout, stderr) => {
+ expect(error).not.toBeNull();
+ expect(stderr).toContain('error: one of the required options \'-p, --pattern \' or \'-a, --architecture \' was not specified');
+ done();
+ });
+ });
+
+ test('example validate command - validates an architecture only', (done) => {
+ const calmValidateArchitectureOnlyCommand = 'calm validate -a ../calm/samples/api-gateway-architecture.json';
+ exec(calmValidateArchitectureOnlyCommand, (error, stdout, _stderr) => {
+ const expectedFilePath = path.join(__dirname, '../test_fixtures/validate_architecture_only_output.json');
+ const expectedOutput = fs.readFileSync(expectedFilePath, 'utf-8');
+ expect(error).toBeNull();
+ expect(stdout).toContain(expectedOutput);
+ done();
+ });
+ });
+
});
diff --git a/cli/src/index.ts b/cli/src/index.ts
index 5f128c66..6c01a7cf 100644
--- a/cli/src/index.ts
+++ b/cli/src/index.ts
@@ -53,7 +53,7 @@ program
program
.command('validate')
.description('Validate that an architecture conforms to a given CALM pattern.')
- .requiredOption(PATTERN_OPTION, 'Path to the pattern file to use. May be a file path or a URL.')
+ .option(PATTERN_OPTION, 'Path to the pattern file to use. May be a file path or a URL.')
.option(ARCHITECTURE_OPTION, 'Path to the architecture file to use. May be a file path or a URL.')
.option(SCHEMAS_OPTION, 'Path to the directory containing the meta schemas to use.', CALM_META_SCHEMA_DIRECTORY)
.option(STRICT_OPTION, 'When run in strict mode, the CLI will fail if any warnings are reported.', false)
@@ -65,6 +65,9 @@ program
.option(OUTPUT_OPTION, 'Path location at which to output the generated file.')
.option(VERBOSE_OPTION, 'Enable verbose logging.', false)
.action(async (options) => {
+ if(!options.pattern && !options.architecture) {
+ program.error(`error: one of the required options '${PATTERN_OPTION}' or '${ARCHITECTURE_OPTION}' was not specified`);
+ }
const outcome = await validate(options.architecture, options.pattern, options.schemaDirectory, options.verbose);
const content = getFormattedOutput(outcome, options.format);
writeOutputFile(options.output, content);
diff --git a/cli/test_fixtures/validate_architecture_only_output.json b/cli/test_fixtures/validate_architecture_only_output.json
new file mode 100644
index 00000000..b0e6704f
--- /dev/null
+++ b/cli/test_fixtures/validate_architecture_only_output.json
@@ -0,0 +1,18 @@
+{
+ "jsonSchemaValidationOutputs": [],
+ "spectralSchemaValidationOutputs": [
+ {
+ "code": "architecture-has-no-placeholder-properties-numerical",
+ "severity": "warning",
+ "message": "Numerical placeholder (-1) detected in architecture.",
+ "path": "/nodes/2/interfaces/0/port",
+ "schemaPath": "",
+ "line_start": 32,
+ "line_end": 32,
+ "character_start": 18,
+ "character_end": 20
+ }
+ ],
+ "hasErrors": false,
+ "hasWarnings": true
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 7d9c8089..3a0a86a4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,8 +14,10 @@
"calm-visualizer"
],
"devDependencies": {
+ "jsdom": "^25.0.1",
"link": "^2.1.1",
- "npm-run-all2": "^5.0.0"
+ "npm-run-all2": "^5.0.0",
+ "vitest": "^2.1.8"
}
},
"calm-visualizer": {
@@ -23,12 +25,9 @@
"dependencies": {
"cytoscape": "^3.30.3",
"cytoscape-cola": "^2.5.1",
- "cytoscape-cose-bilkent": "^4.1.0",
"cytoscape-dagre": "^2.5.0",
"cytoscape-expand-collapse": "^4.1.1",
- "cytoscape-fcose": "^2.2.0",
"cytoscape-node-edge-html-label": "^1.0.6",
- "cytoscape-node-html-label": "^1.2.2",
"file-saver": "^2.0.5",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -41,7 +40,6 @@
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/cytoscape": "^3.21.8",
- "@types/cytoscape-fcose": "^2.2.4",
"@types/file-saver": "^2.0.7",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
@@ -59,204 +57,10 @@
"tailwindcss": "^3.4.14",
"typescript": "^5.5.3",
"typescript-eslint": "^8.14.0",
- "vite": "^5.4.8",
+ "vite": "^5.4.12",
"vitest": "^2.1.8"
}
},
- "calm-visualizer/node_modules/canvas": {
- "version": "2.11.2",
- "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz",
- "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@mapbox/node-pre-gyp": "^1.0.0",
- "nan": "^2.17.0",
- "simple-get": "^3.0.3"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "calm-visualizer/node_modules/jsdom": {
- "version": "25.0.1",
- "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
- "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssstyle": "^4.1.0",
- "data-urls": "^5.0.0",
- "decimal.js": "^10.4.3",
- "form-data": "^4.0.0",
- "html-encoding-sniffer": "^4.0.0",
- "http-proxy-agent": "^7.0.2",
- "https-proxy-agent": "^7.0.5",
- "is-potential-custom-element-name": "^1.0.1",
- "nwsapi": "^2.2.12",
- "parse5": "^7.1.2",
- "rrweb-cssom": "^0.7.1",
- "saxes": "^6.0.0",
- "symbol-tree": "^3.2.4",
- "tough-cookie": "^5.0.0",
- "w3c-xmlserializer": "^5.0.0",
- "webidl-conversions": "^7.0.0",
- "whatwg-encoding": "^3.1.1",
- "whatwg-mimetype": "^4.0.0",
- "whatwg-url": "^14.0.0",
- "ws": "^8.18.0",
- "xml-name-validator": "^5.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "canvas": "^2.11.2"
- },
- "peerDependenciesMeta": {
- "canvas": {
- "optional": true
- }
- }
- },
- "calm-visualizer/node_modules/magic-string": {
- "version": "0.30.17",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
- "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0"
- }
- },
- "calm-visualizer/node_modules/tr46": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
- "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "punycode": "^2.3.1"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "calm-visualizer/node_modules/vitest": {
- "version": "2.1.8",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
- "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "2.1.8",
- "@vitest/mocker": "2.1.8",
- "@vitest/pretty-format": "^2.1.8",
- "@vitest/runner": "2.1.8",
- "@vitest/snapshot": "2.1.8",
- "@vitest/spy": "2.1.8",
- "@vitest/utils": "2.1.8",
- "chai": "^5.1.2",
- "debug": "^4.3.7",
- "expect-type": "^1.1.0",
- "magic-string": "^0.30.12",
- "pathe": "^1.1.2",
- "std-env": "^3.8.0",
- "tinybench": "^2.9.0",
- "tinyexec": "^0.3.1",
- "tinypool": "^1.0.1",
- "tinyrainbow": "^1.2.0",
- "vite": "^5.0.0",
- "vite-node": "2.1.8",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^18.0.0 || >=20.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "2.1.8",
- "@vitest/ui": "2.1.8",
- "happy-dom": "*",
- "jsdom": "*"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- }
- }
- },
- "calm-visualizer/node_modules/webidl-conversions": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
- "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- }
- },
- "calm-visualizer/node_modules/whatwg-url": {
- "version": "14.1.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
- "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tr46": "^5.0.0",
- "webidl-conversions": "^7.0.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "calm-visualizer/node_modules/ws": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
- "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10.0.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": ">=5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
"cli": {
"name": "@finos/calm-cli",
"version": "0.3.0",
@@ -292,6 +96,21 @@
"xml2js": "^0.6.2"
}
},
+ "cli/node_modules/canvas": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.1.tgz",
+ "integrity": "sha512-PcpVF4f8RubAeN/jCQQ/UymDKzOiLmRPph8fOTzDnlsUihkO/AUlxuhaa7wGRc3vMcCbV1fzuvyu5cWZlIcn1w==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "node-addon-api": "^7.0.0",
+ "prebuild-install": "^7.1.1",
+ "simple-get": "^3.0.3"
+ },
+ "engines": {
+ "node": "^18.12.0 || >= 20.9.0"
+ }
+ },
"docs": {
"version": "0.0.0",
"license": "Apache-2.0",
@@ -8332,16 +8151,6 @@
"devOptional": true,
"license": "MIT"
},
- "node_modules/@types/cytoscape-fcose": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/@types/cytoscape-fcose/-/cytoscape-fcose-2.2.4.tgz",
- "integrity": "sha512-QwWtnT8HI9h+DHhG5krGc1ZY0Ex+cn85MvX96ZNAjSxuXiZDnjIZW/ypVkvvubTjIY4rSdkJY1D/Nsn8NDpmAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/cytoscape": "*"
- }
- },
"node_modules/@types/debug": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -10819,18 +10628,21 @@
"license": "CC-BY-4.0"
},
"node_modules/canvas": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.0.1.tgz",
- "integrity": "sha512-PcpVF4f8RubAeN/jCQQ/UymDKzOiLmRPph8fOTzDnlsUihkO/AUlxuhaa7wGRc3vMcCbV1fzuvyu5cWZlIcn1w==",
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz",
+ "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
- "node-addon-api": "^7.0.0",
- "prebuild-install": "^7.1.1",
+ "@mapbox/node-pre-gyp": "^1.0.0",
+ "nan": "^2.17.0",
"simple-get": "^3.0.3"
},
"engines": {
- "node": "^18.12.0 || >= 20.9.0"
+ "node": ">=6"
}
},
"node_modules/ccount": {
@@ -11632,15 +11444,6 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
- "node_modules/cose-base": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz",
- "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==",
- "license": "MIT",
- "dependencies": {
- "layout-base": "^1.0.0"
- }
- },
"node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
@@ -12225,18 +12028,6 @@
"cytoscape": "^3.2.0"
}
},
- "node_modules/cytoscape-cose-bilkent": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz",
- "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==",
- "license": "MIT",
- "dependencies": {
- "cose-base": "^1.0.0"
- },
- "peerDependencies": {
- "cytoscape": "^3.2.0"
- }
- },
"node_modules/cytoscape-dagre": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/cytoscape-dagre/-/cytoscape-dagre-2.5.0.tgz",
@@ -12258,33 +12049,6 @@
"cytoscape": "^3.3.0"
}
},
- "node_modules/cytoscape-fcose": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz",
- "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==",
- "license": "MIT",
- "dependencies": {
- "cose-base": "^2.2.0"
- },
- "peerDependencies": {
- "cytoscape": "^3.2.0"
- }
- },
- "node_modules/cytoscape-fcose/node_modules/cose-base": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz",
- "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==",
- "license": "MIT",
- "dependencies": {
- "layout-base": "^2.0.0"
- }
- },
- "node_modules/cytoscape-fcose/node_modules/layout-base": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz",
- "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==",
- "license": "MIT"
- },
"node_modules/cytoscape-node-edge-html-label": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/cytoscape-node-edge-html-label/-/cytoscape-node-edge-html-label-1.0.6.tgz",
@@ -12300,21 +12064,6 @@
}
}
},
- "node_modules/cytoscape-node-html-label": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/cytoscape-node-html-label/-/cytoscape-node-html-label-1.2.2.tgz",
- "integrity": "sha512-oUVwrlsIlaJJ8QrQFSMdv3uXVXPg6tMH/Tfofr8JuZIovqI4fPqBi6sQgCMcVpS6k9Td0TTjowBsNRw32CESWg==",
- "license": "MIT",
- "peerDependencies": {
- "@types/cytoscape": "^3.1.0",
- "cytoscape": "^3.0.0"
- },
- "peerDependenciesMeta": {
- "@types/cytoscape": {
- "optional": true
- }
- }
- },
"node_modules/d3-dispatch": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
@@ -17994,6 +17743,106 @@
"js-yaml": "bin/js-yaml.js"
}
},
+ "node_modules/jsdom": {
+ "version": "25.0.1",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
+ "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cssstyle": "^4.1.0",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.4.3",
+ "form-data": "^4.0.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.5",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.12",
+ "parse5": "^7.1.2",
+ "rrweb-cssom": "^0.7.1",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.0.0",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^2.11.2"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
+ "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
+ "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.0.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsep": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
@@ -18194,12 +18043,6 @@
"shell-quote": "^1.8.1"
}
},
- "node_modules/layout-base": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz",
- "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==",
- "license": "MIT"
- },
"node_modules/leven": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@@ -21130,9 +20973,9 @@
}
},
"node_modules/node-abi": {
- "version": "3.71.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.71.0.tgz",
- "integrity": "sha512-SZ40vRiy/+wRTf21hxkkEjPJZpARzUMVcJoQse2EF8qkUWbbO2z7vd5oA/H6bVH6SZQ5STGcu0KRDS7biNRfxw==",
+ "version": "3.73.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.73.0.tgz",
+ "integrity": "sha512-z8iYzQGBu35ZkTQ9mtR8RqugJZ9RCLn8fv3d7LsgDBzOijGQP3RdKTX4LA7LXw03ZhU5z0l4xfhIMgSES31+cg==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
@@ -26963,9 +26806,9 @@
}
},
"node_modules/tar-fs": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz",
- "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
+ "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
@@ -28532,9 +28375,9 @@
}
},
"node_modules/vite": {
- "version": "5.4.11",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
- "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
+ "version": "5.4.12",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
+ "integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -29044,6 +28887,82 @@
"@esbuild/win32-x64": "0.21.5"
}
},
+ "node_modules/vitest": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.8.tgz",
+ "integrity": "sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "2.1.8",
+ "@vitest/mocker": "2.1.8",
+ "@vitest/pretty-format": "^2.1.8",
+ "@vitest/runner": "2.1.8",
+ "@vitest/snapshot": "2.1.8",
+ "@vitest/spy": "2.1.8",
+ "@vitest/utils": "2.1.8",
+ "chai": "^5.1.2",
+ "debug": "^4.3.7",
+ "expect-type": "^1.1.0",
+ "magic-string": "^0.30.12",
+ "pathe": "^1.1.2",
+ "std-env": "^3.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.1",
+ "tinypool": "^1.0.1",
+ "tinyrainbow": "^1.2.0",
+ "vite": "^5.0.0",
+ "vite-node": "2.1.8",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "@vitest/browser": "2.1.8",
+ "@vitest/ui": "2.1.8",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/package.json b/package.json
index 8b437cf6..162a9b3d 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,8 @@
},
"devDependencies": {
"link": "^2.1.1",
- "npm-run-all2": "^5.0.0"
+ "npm-run-all2": "^5.0.0",
+ "vitest": "^2.1.8",
+ "jsdom": "^25.0.1"
}
}
diff --git a/shared/src/commands/validate/validate.spec.ts b/shared/src/commands/validate/validate.spec.ts
index 84358190..d501293d 100644
--- a/shared/src/commands/validate/validate.spec.ts
+++ b/shared/src/commands/validate/validate.spec.ts
@@ -57,13 +57,18 @@ describe('validate-all', () => {
fetchMock.restore();
});
+ it('returns error when the the Pattern and the Architecture are undefined or an empty string', async () => {
+ await expect(validate('', undefined, metaSchemaLocation, debugDisabled))
+ .rejects
+ .toThrow();
+ expect(mockExit).toHaveBeenCalledWith(1);
+ });
it('returns validation error when the JSON Schema pattern cannot be found in the input path', async () => {
await expect(validate('../test_fixtures/api-gateway-implementation.json', 'thisFolderDoesNotExist/api-gateway.json', metaSchemaLocation, debugDisabled))
.rejects
.toThrow();
expect(mockExit).toHaveBeenCalledWith(1);
-
});
it('returns validation error when the architecture file cannot be found in the input path', async () => {
@@ -495,6 +500,75 @@ describe('validate-all', () => {
.toHaveBeenCalledWith(1);
});
});
+
+ describe('validate - architecture only', () => {
+
+ let mockExit;
+
+ beforeEach(() => {
+ mockRunFunction.mockReturnValue([]);
+ mockExit = jest.spyOn(process, 'exit')
+ .mockImplementation((code) => {
+ if (code != 0) {
+ throw new Error('Expected successful run, code was nonzero: ' + code);
+ }
+ return undefined as never;
+ });
+ });
+
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ it('exits with non zero exit code when the architecture cannot be found', async () => {
+ fetchMock.mock('http://exist/api-gateway-implementation.json', 404);
+
+ await expect(validateAndExitConditionally('http://exist/api-gateway-implementation.json', '', metaSchemaLocation, debugDisabled))
+ .rejects
+ .toThrow();
+
+ expect(mockExit)
+ .toHaveBeenCalledWith(1);
+ });
+
+ it('exits with non zero exit code when the architecture does not pass all the spectral validations ', async () => {
+ const expectedSpectralOutput: ISpectralDiagnostic[] = [
+ {
+ code: 'example-error',
+ message: 'Example error',
+ severity: 0,
+ path: ['/nodes'],
+ range: { start: { line: 1, character: 1 }, end: { line: 2, character: 1 } }
+ }
+ ];
+
+ mockRunFunction.mockReturnValue(expectedSpectralOutput);
+
+ const apiGateway = readFileSync(path.resolve(__dirname, '../../../test_fixtures/api-gateway-implementation.json'), 'utf8');
+ fetchMock.mock('http://exist/api-gateway-implementation.json', apiGateway);
+
+ await expect(validateAndExitConditionally('http://exist/api-gateway-implementation.json', '', metaSchemaLocation, debugDisabled))
+ .rejects
+ .toThrow();
+
+ expect(mockExit)
+ .toHaveBeenCalledWith(1);
+ });
+
+ it('exits with zero exit code when the architecture passes all the spectral validations ', async () => {
+ const expectedSpectralOutput: ISpectralDiagnostic[] = [];
+
+ mockRunFunction.mockReturnValue(expectedSpectralOutput);
+
+ const apiGateway = readFileSync(path.resolve(__dirname, '../../../test_fixtures/api-gateway-implementation.json'), 'utf8');
+ fetchMock.mock('http://exist/api-gateway-implementation.json', apiGateway);
+
+ await expect(validateAndExitConditionally('http://exist/api-gateway-implementation.json', undefined, metaSchemaLocation, debugDisabled));
+
+ expect(mockExit)
+ .toHaveBeenCalledWith(0);
+ });
+ });
});
diff --git a/shared/src/commands/validate/validate.ts b/shared/src/commands/validate/validate.ts
index d84dfcef..e2191199 100644
--- a/shared/src/commands/validate/validate.ts
+++ b/shared/src/commands/validate/validate.ts
@@ -267,71 +267,105 @@ export async function validate(
debug: boolean = false): Promise {
logger = initLogger(debug);
- let errors = false;
- let warnings = false;
try {
- const ajv = buildAjv2020(debug);
-
- await loadMetaSchemas(ajv, metaSchemaPath);
-
- logger.info(`Loading pattern from : ${jsonSchemaLocation}`);
- const jsonSchema = await getFileFromUrlOrPath(jsonSchemaLocation);
-
- const spectralResultForPattern: SpectralResult = await runSpectralValidations(stripRefs(jsonSchema), validationRulesForPattern);
-
- if (jsonSchemaArchitectureLocation === undefined) {
- return validatePatternOnly(spectralResultForPattern, jsonSchema, ajv);
+ if (jsonSchemaArchitectureLocation && jsonSchemaLocation) {
+ return await validateArchitectureAgainstPattern(jsonSchemaArchitectureLocation, jsonSchemaLocation, metaSchemaPath, debug);
+ } else if (jsonSchemaLocation) {
+ return await validatePatternOnly(jsonSchemaLocation, metaSchemaPath, debug);
+ } else if (jsonSchemaArchitectureLocation) {
+ return await validateArchitectureOnly(jsonSchemaArchitectureLocation);
+ } else {
+ logger.debug('You must provide at least an architecture or a pattern');
+ throw new Error('You must provide at least an architecture or a pattern');
}
+ } catch (error) {
+ logger.error('An error occured:', error);
+ process.exit(1);
+ }
+}
- const validateSchema = await ajv.compileAsync(jsonSchema);
+/**
+ * Run the spectral rules for the pattern and the architecture, and then compile the pattern and validate the architecture against it.
+ *
+ * @param jsonSchemaArchitectureLocation - the location of the architecture to validate.
+ * @param jsonSchemaLocation - the location of the pattern to validate against.
+ * @param metaSchemaPath - the path of the meta schemas to use for ajv.
+ * @param debug - the flag to enable debug logging.
+ * @returns the validation outcome with the results of the spectral and json schema validations.
+ */
+async function validateArchitectureAgainstPattern(jsonSchemaArchitectureLocation:string, jsonSchemaLocation:string, metaSchemaPath:string, debug: boolean): Promise{
+ const ajv = buildAjv2020(debug);
+ await loadMetaSchemas(ajv, metaSchemaPath);
- logger.info(`Loading architecture from : ${jsonSchemaArchitectureLocation}`);
- const jsonSchemaArchitecture = await getFileFromUrlOrPath(jsonSchemaArchitectureLocation);
+ logger.info(`Loading pattern from : ${jsonSchemaLocation}`);
+ const jsonSchema = await getFileFromUrlOrPath(jsonSchemaLocation);
+ const spectralResultForPattern: SpectralResult = await runSpectralValidations(stripRefs(jsonSchema), validationRulesForPattern);
+ const validateSchema = await ajv.compileAsync(jsonSchema);
- const spectralResultForArchitecture: SpectralResult = await runSpectralValidations(jsonSchemaArchitecture, validationRulesForArchitecture);
+ logger.info(`Loading architecture from : ${jsonSchemaArchitectureLocation}`);
+ const jsonSchemaArchitecture = await getFileFromUrlOrPath(jsonSchemaArchitectureLocation);
- const spectralResult = mergeSpectralResults(spectralResultForPattern, spectralResultForArchitecture);
+ const spectralResultForArchitecture: SpectralResult = await runSpectralValidations(jsonSchemaArchitecture, validationRulesForArchitecture);
- errors = spectralResult.errors;
- warnings = spectralResult.warnings;
+ const spectralResult = mergeSpectralResults(spectralResultForPattern, spectralResultForArchitecture);
- let jsonSchemaValidations = [];
- if (!validateSchema(jsonSchemaArchitecture)) {
- logger.debug(`JSON Schema validation raw output: ${prettifyJson(validateSchema.errors)}`);
- errors = true;
- jsonSchemaValidations = convertJsonSchemaIssuesToValidationOutputs(validateSchema.errors);
- }
+ let errors = spectralResult.errors;
+ const warnings = spectralResult.warnings;
- return new ValidationOutcome(jsonSchemaValidations, spectralResult.spectralIssues, errors, warnings);
- } catch (error) {
- logger.error('An error occured:', error);
- process.exit(1);
+ let jsonSchemaValidations = [];
+
+ if (!validateSchema(jsonSchemaArchitecture)) {
+ logger.debug(`JSON Schema validation raw output: ${prettifyJson(validateSchema.errors)}`);
+ errors = true;
+ jsonSchemaValidations = convertJsonSchemaIssuesToValidationOutputs(validateSchema.errors);
}
+
+ return new ValidationOutcome(jsonSchemaValidations, spectralResult.spectralIssues, errors, warnings);
}
/**
* Run validations for the case where only the pattern is provided.
- * This essentially tries to compile the pattern, and returns the errors thrown if it fails.
+ * This essentially runs the spectral validations and tries to compile the pattern.
*
- * @param spectralValidationResults The results from running Spectral on the pattern.
- * @param patternSchema The pattern as a JS object, parsed from the file.
- * @param ajv The AJV instance to compile with.
- * @param failOnWarnings Whether or not to treat a warning as a failure in the validation process.
+ * @param jsonSchemaLocation - the location of the patterns JSON Schema to validate.
+ * @param metaSchemaPath - the path of the meta schemas to use for ajv.
+ * @param debug - the flag to enable debug logging.
+ * @returns the validation outcome with the results of the spectral validation and the pattern compilation.
*/
-function validatePatternOnly(spectralValidationResults: SpectralResult, patternSchema: object, ajv: Ajv2020): ValidationOutcome {
- logger.debug('Architecture was not provided, only the JSON Schema will be validated');
+async function validatePatternOnly(jsonSchemaLocation: string, metaSchemaPath: string, debug: boolean): Promise {
+ logger.debug('Architecture was not provided, only the Pattern Schema will be validated');
+ const ajv = buildAjv2020(debug);
+ await loadMetaSchemas(ajv, metaSchemaPath);
+
+ const patternSchema = await getFileFromUrlOrPath(jsonSchemaLocation);
+ const spectralValidationResults: SpectralResult = await runSpectralValidations(stripRefs(patternSchema), validationRulesForPattern);
+
let errors = spectralValidationResults.errors;
const warnings = spectralValidationResults.warnings;
const jsonSchemaErrors = [];
try {
- ajv.compile(patternSchema);
+ await ajv.compileAsync(patternSchema);
} catch (error) {
errors = true;
jsonSchemaErrors.push(new ValidationOutput('json-schema', 'error', error.message, '/'));
}
- return new ValidationOutcome(jsonSchemaErrors, [], errors, warnings);
+ return new ValidationOutcome(jsonSchemaErrors, spectralValidationResults.spectralIssues, errors, warnings);// added spectral to return object
+}
+
+/**
+ * Run the spectral validations for the case where only the architecture is provided.
+ *
+ * @param architectureSchemaLocation - The location of the architecture schema.
+ * @returns the validation outcome with the results of the spectral validation.
+ */
+async function validateArchitectureOnly(architectureSchemaLocation: string): Promise {
+ logger.debug('Pattern was not provided, only the Architecture will be validated');
+
+ const jsonSchemaArchitecture = await getFileFromUrlOrPath(architectureSchemaLocation);
+ const spectralResultForArchitecture: SpectralResult = await runSpectralValidations(jsonSchemaArchitecture, validationRulesForArchitecture);
+ return new ValidationOutcome([], spectralResultForArchitecture.spectralIssues, spectralResultForArchitecture.errors, spectralResultForArchitecture.warnings);
}
function extractSpectralRuleNames(): string[] {
diff --git a/shared/src/commands/visualize/calmToDot.spec.ts b/shared/src/commands/visualize/calmToDot.spec.ts
index e7977e59..ddab97fa 100644
--- a/shared/src/commands/visualize/calmToDot.spec.ts
+++ b/shared/src/commands/visualize/calmToDot.spec.ts
@@ -34,7 +34,7 @@ describe('calmToDot', () => {
],
relationships: [
{
- 'uniqueId': 'relationship-1',
+ 'unique-id': 'relationship-1',
'description': 'test description',
'protocol': 'HTTPS',
'relationship-type': {
@@ -76,7 +76,7 @@ describe('calmToDot', () => {
...calm,
relationships: [
{
- 'uniqueId': 'subtest',
+ 'unique-id': 'subtest',
'relationship-type': {
'deployed-in': {
container: 'node-1',
diff --git a/shared/src/types.ts b/shared/src/types.ts
index f8587bf3..466db8c7 100644
--- a/shared/src/types.ts
+++ b/shared/src/types.ts
@@ -25,7 +25,7 @@ export interface CALMInteractsRelationship {
nodes: string[]
}
},
- uniqueId: string,
+ ['unique-id']: string,
description?: string
}
@@ -36,7 +36,7 @@ export interface CALMConnectsRelationship {
destination: { node: string, interface?: string }
}
},
- uniqueId: string,
+ ['unique-id']: string,
protocol?: string,
authentication?: string,
description?: string
@@ -49,7 +49,7 @@ export interface CALMDeployedInRelationship {
nodes: string[]
}
},
- uniqueId: string,
+ ['unique-id']: string,
description?: string
}
@@ -60,6 +60,6 @@ export interface CALMComposedOfRelationship {
nodes: string[]
},
}
- uniqueId: string,
+ ['unique-id']: string,
description?: string
}
\ No newline at end of file