Skip to content

Commit 246e2a0

Browse files
authored
Merge pull request finos#1574 from markscott-ms/fix-1555
fix(shared): validation of architectures against patterns with options (finos#1555)
2 parents 63ac5cb + 142897d commit 246e2a0

File tree

6 files changed

+405
-6
lines changed

6 files changed

+405
-6
lines changed
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { validate, applyArchitectureOptionsToPattern } from './validate.js';
3+
import { readFileSync } from 'fs';
4+
import path from 'path';
5+
import { FileSystemDocumentLoader } from '../../document-loader/file-system-document-loader.js';
6+
import { SchemaDirectory } from '../../schema-directory.js';
7+
8+
const inputArchPath = path.join(
9+
__dirname,
10+
'../../../test_fixtures/command/validate/options/arch.json'
11+
);
12+
const inputPatternPath = path.join(
13+
__dirname,
14+
'../../../test_fixtures/command/validate/options/pattern.json'
15+
);
16+
17+
const schemaDir = path.join(__dirname, '../../../../calm/release/1.0/meta/');
18+
19+
describe('validate E2E', () => {
20+
let schemaDirectory;
21+
22+
it('validates architecture against pattern with options', async () => {
23+
schemaDirectory = new SchemaDirectory(new FileSystemDocumentLoader([schemaDir], true));
24+
await schemaDirectory.loadSchemas();
25+
26+
const inputPattern = JSON.parse(readFileSync(inputPatternPath, 'utf-8'));
27+
const inputArch = JSON.parse(readFileSync(inputArchPath, 'utf-8'));
28+
const response = await validate(inputArch, inputPattern, schemaDirectory, true);
29+
30+
expect(response).not.toBeNull();
31+
expect(response).not.toBeUndefined();
32+
expect(response.hasErrors).toBeTruthy();
33+
// expect(response.hasWarnings).toBeTruthy();
34+
expect(response.jsonSchemaValidationOutputs).toHaveLength(1);
35+
expect(response.jsonSchemaValidationOutputs[0].path).toBe('/nodes/1/node-type');
36+
expect(response.spectralSchemaValidationOutputs).toHaveLength(2);
37+
expect(response.spectralSchemaValidationOutputs[0].path).toBe('/nodes/0/description');
38+
expect(response.spectralSchemaValidationOutputs[1].path).toBe('/nodes/1/description');
39+
});
40+
41+
describe('applyArchitectureOptionsToPattern', () => {
42+
it('works with one options relationship', async () => {
43+
const architecture = JSON.parse(
44+
readFileSync(inputArchPath, 'utf8')
45+
);
46+
const pattern = JSON.parse(
47+
readFileSync(inputPatternPath, 'utf8')
48+
);
49+
const expectedResult = JSON.parse(
50+
readFileSync(path.join(__dirname, '../../../test_fixtures/command/validate/options/pattern-resolved.json'), 'utf8')
51+
);
52+
53+
const newPattern = applyArchitectureOptionsToPattern(architecture, pattern);
54+
expect(newPattern).toStrictEqual(expectedResult);
55+
});
56+
});
57+
});

shared/src/commands/validate/validate.spec.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { validate, sortSpectralIssueBySeverity, convertSpectralDiagnosticToValidationOutputs, convertJsonSchemaIssuesToValidationOutputs, stripRefs, exitBasedOffOfValidationOutcome } from './validate';
1+
import { validate, sortSpectralIssueBySeverity, convertSpectralDiagnosticToValidationOutputs, convertJsonSchemaIssuesToValidationOutputs, stripRefs, exitBasedOffOfValidationOutcome, extractChoicesFromArchitecture } from './validate';
22
import { readFileSync } from 'fs';
33
import path from 'path';
44
import { ISpectralDiagnostic } from '@stoplight/spectral-core';
@@ -256,6 +256,68 @@ describe('validation support functions', () => {
256256
});
257257

258258
});
259+
260+
describe('extractChoicesFromArchitecture', () => {
261+
it('works with no options relationship', async () => {
262+
const architecture = {
263+
relationships: [
264+
{
265+
'unique-id': 'rel-1',
266+
'relationship-type': {
267+
connects: [
268+
{ 'description': 'A connection', 'source': { 'node': 'node-1' }, 'destination': { 'node': 'node-2' } }
269+
]
270+
}
271+
}
272+
]
273+
};
274+
const choices = extractChoicesFromArchitecture(architecture);
275+
expect(choices).toHaveLength(0);
276+
});
277+
278+
it('works with one options relationship', async () => {
279+
const architecture = {
280+
relationships: [
281+
{
282+
'unique-id': 'rel-1',
283+
'relationship-type': {
284+
options: [
285+
{ 'description': 'Option 1', 'nodes': ['node-1', 'node-2'], 'relationships': [] }
286+
]
287+
}
288+
}
289+
]
290+
};
291+
const choices = extractChoicesFromArchitecture(architecture);
292+
expect(choices).toHaveLength(1);
293+
expect(choices[0].description).toBe('Option 1');
294+
});
295+
296+
it('works with two options relationship', async () => {
297+
const architecture = {
298+
relationships: [
299+
{
300+
'unique-id': 'rel-1',
301+
'relationship-type': {
302+
options: [
303+
{ 'description': 'Option 1', 'nodes': ['node-1', 'node-2'], 'relationships': [] }
304+
]
305+
}
306+
},
307+
{
308+
'unique-id': 'rel-2',
309+
'relationship-type': {
310+
options: [
311+
{ 'description': 'Option A', 'nodes': ['node-4'], 'relationships': ['rel-9'] }
312+
]
313+
}
314+
}
315+
]
316+
};
317+
const choices = extractChoicesFromArchitecture(architecture);
318+
expect(choices).toHaveLength(2);
319+
});
320+
});
259321
});
260322

261323
describe('validate pattern and architecture', () => {
@@ -385,7 +447,6 @@ describe('validate pattern and architecture', () => {
385447
expect(response.allValidationOutputs()).not.toBeNull();
386448
expect(response.allValidationOutputs().length).toBeGreaterThan(0);
387449
});
388-
389450
});
390451

391452
describe('validate pattern only', () => {

shared/src/commands/validate/validate.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import createJUnitReport from './output-formats/junit-output.js';
1111
import prettyFormat from './output-formats/pretty-output.js';
1212
import { SchemaDirectory } from '../../schema-directory.js';
1313
import { JsonSchemaValidator } from './json-schema-validator.js';
14+
import { selectChoices, CalmChoice } from '../generate/components/options.js';
1415

1516
let logger: Logger; // defined later at startup
1617

@@ -124,10 +125,6 @@ export async function validate(
124125
* @returns the validation outcome with the results of the spectral and json schema validations.
125126
*/
126127
async function validateArchitectureAgainstPattern(architecture: object, pattern: object, schemaDirectory: SchemaDirectory, debug: boolean): Promise<ValidationOutcome> {
127-
// Use JsonSchemaValidator
128-
const jsonSchemaValidator = new JsonSchemaValidator(schemaDirectory, pattern, debug);
129-
await jsonSchemaValidator.initialize();
130-
131128
const spectralResultForPattern: SpectralResult = await runSpectralValidations(stripRefs(pattern), validationRulesForPattern);
132129
const spectralResultForArchitecture: SpectralResult = await runSpectralValidations(JSON.stringify(architecture), validationRulesForArchitecture);
133130

@@ -136,6 +133,11 @@ async function validateArchitectureAgainstPattern(architecture: object, pattern:
136133
let errors = spectralResult.errors;
137134
const warnings = spectralResult.warnings;
138135

136+
const patternResolved = applyArchitectureOptionsToPattern(architecture, pattern);
137+
138+
const jsonSchemaValidator = new JsonSchemaValidator(schemaDirectory, patternResolved, debug);
139+
await jsonSchemaValidator.initialize();
140+
139141
let jsonSchemaValidations = [];
140142

141143
const schemaErrors = jsonSchemaValidator.validate(architecture);
@@ -198,6 +200,39 @@ async function validateArchitectureOnly(architecture: object): Promise<Validatio
198200
return new ValidationOutcome(jsonSchemaValidations, spectralResultForArchitecture.spectralIssues, errors, warnings);
199201
}
200202

203+
/**
204+
* If a pattern contains objects, we need to apply the chosen options recorded
205+
* in the architecture to the pattern to produce a JSON schema pattern that can
206+
* be used for validation.
207+
* @param architecture Architecture which may contain options.
208+
* @param pattern Pattern which may contain options.
209+
* @returns Pattern with options applied and flattened.
210+
*/
211+
export function applyArchitectureOptionsToPattern(architecture: object, pattern: object): object {
212+
213+
const choices: CalmChoice[] = extractChoicesFromArchitecture(architecture);
214+
if (choices.length === 0) {
215+
return pattern;
216+
}
217+
218+
return selectChoices(pattern, choices, true);
219+
}
220+
221+
export function extractChoicesFromArchitecture(architecture: object): CalmChoice[] {
222+
if (!architecture || !Object.prototype.hasOwnProperty.call(architecture, 'relationships')) {
223+
return [];
224+
}
225+
226+
return architecture['relationships']
227+
.filter((rel: object) => rel['relationship-type'] && Object.prototype.hasOwnProperty.call(rel['relationship-type'], 'options'))
228+
.map((rel: object) => rel['relationship-type']['options'][0])
229+
.map((rel: object) => ({
230+
description: rel['description'],
231+
nodes: rel['nodes'] || [],
232+
relationships: rel['relationships'] || []
233+
}));
234+
}
235+
201236
function extractSpectralRuleNames(): string[] {
202237
const architectureRuleNames = getRuleNamesFromRuleset(validationRulesForArchitecture);
203238
const patternRuleNames = getRuleNamesFromRuleset(validationRulesForPattern);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"nodes": [
3+
{
4+
"unique-id": "node-1",
5+
"name": "Node 1",
6+
"node-type": "service",
7+
"description": "[[ DESCRIPTION ]]"
8+
},
9+
{
10+
"unique-id": "node-2",
11+
"name": "Node 2",
12+
"node-type": "system",
13+
"description": "[[ DESCRIPTION ]]"
14+
}
15+
],
16+
"relationships": [
17+
{
18+
"unique-id": "options",
19+
"description": "Which databases does your application connect to?",
20+
"relationship-type": {
21+
"options": [
22+
{
23+
"description": "Both Nodes",
24+
"nodes": [
25+
"node-1",
26+
"node-2"
27+
],
28+
"relationships": []
29+
}
30+
]
31+
}
32+
}
33+
],
34+
"$schema": "https://calm.finos.org/draft/2025-03/prototype/anyof/pattern.json"
35+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
{
2+
"$schema": "https://calm.finos.org/release/1.0/meta/calm.json",
3+
"$id": "https://calm.finos.org/draft/2025-03/prototype/anyof/pattern.json",
4+
"title": "Pattern Options Issue",
5+
"type": "object",
6+
"properties": {
7+
"nodes": {
8+
"type": "array",
9+
"maxItems": 2,
10+
"prefixItems": [
11+
{
12+
"$ref": "https://calm.finos.org/release/1.0/meta/core.json#/defs/node",
13+
"type": "object",
14+
"properties": {
15+
"unique-id": {
16+
"const": "node-1"
17+
},
18+
"name": {
19+
"const": "Node 1"
20+
},
21+
"node-type": {
22+
"const": "service"
23+
}
24+
}
25+
},
26+
{
27+
"$ref": "https://calm.finos.org/release/1.0/meta/core.json#/defs/node",
28+
"type": "object",
29+
"properties": {
30+
"unique-id": {
31+
"const": "node-2"
32+
},
33+
"name": {
34+
"const": "Node 2"
35+
},
36+
"node-type": {
37+
"const": "service"
38+
}
39+
}
40+
}
41+
]
42+
},
43+
"relationships": {
44+
"type": "array",
45+
"minItems": 1,
46+
"maxItems": 1,
47+
"prefixItems": [
48+
{
49+
"$ref": "https://calm.finos.org/release/1.0/meta/core.json#/defs/relationship",
50+
"type": "object",
51+
"properties": {
52+
"unique-id": {
53+
"const": "options"
54+
},
55+
"description": {
56+
"const": "Which databases does your application connect to?"
57+
},
58+
"relationship-type": {
59+
"type": "object",
60+
"properties": {
61+
"options": {
62+
"type": "array",
63+
"minItems": 1,
64+
"maxItems": 1,
65+
"prefixItems": [
66+
{
67+
"$ref": "https://calm.finos.org/release/1.0/meta/core.json#/defs/decision",
68+
"type": "object",
69+
"properties": {
70+
"description": {
71+
"const": "Both Nodes"
72+
},
73+
"nodes": {
74+
"const": [
75+
"node-1",
76+
"node-2"
77+
]
78+
},
79+
"relationships": {
80+
"const": []
81+
}
82+
}
83+
}
84+
]
85+
}
86+
}
87+
}
88+
}
89+
}
90+
]
91+
}
92+
},
93+
"required": ["nodes", "relationships"]
94+
}

0 commit comments

Comments
 (0)