Skip to content

Commit 734452d

Browse files
Merge pull request finos#1394 from rocketstack-matt/validate
Add ability to run validate against an architecture without a pattern…
2 parents 5b0d871 + d34608c commit 734452d

File tree

6 files changed

+119
-11
lines changed

6 files changed

+119
-11
lines changed

calm/getting-started/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ Verify the installation.
8989
```shell
9090
calm --version
9191
```
92-
This getting started has been verified to work against 0.7.8 of the cli.
92+
This getting started has been verified to work against 0.7.9 of the cli.
9393

9494
---
9595

cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@finos/calm-cli",
3-
"version": "0.7.8",
3+
"version": "0.7.9",
44
"description": "A set of tools for interacting with the Common Architecture Language Model (CALM)",
55
"main": "dist/index.js",
66
"files": [

cli/src/cli.e2e.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe('CLI Integration Tests', () => {
254254

255255
// This will enforce that people verify the getting-started guide works prior to any cli change
256256
const { stdout } = await execPromise(calm('--version'));
257-
expect(stdout.trim()).toMatch('0.7.8'); // basic semver check
257+
expect(stdout.trim()).toMatch('0.7.9'); // basic semver check
258258

259259
//STEP 1: Generate Architecture From Pattern
260260
const inputPattern = path.resolve(GETTING_STARTED_DIR, 'conference-signup.pattern.json');

docs/quick-start/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ Verify the installation.
103103
calm --version
104104
```
105105

106-
This getting started has been verified to work against 0.7.8 of the cli.
106+
This getting started has been verified to work against 0.7.9 of the cli.
107107

108108
---
109109

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,8 +528,45 @@ describe('validate - architecture only', () => {
528528
expect(response.allValidationOutputs()).not.toBeNull();
529529
expect(response.allValidationOutputs().length).toBe(0);
530530
});
531-
});
532531

532+
it('validates architecture against schema specified in $schema property when no pattern provided', async () => {
533+
const expectedSpectralOutput: ISpectralDiagnostic[] = [];
534+
mockRunFunction.mockReturnValue(expectedSpectralOutput);
535+
536+
// Create a simple valid architecture with a CALM schema reference
537+
const validArchitecture = {
538+
'$schema': 'https://raw.githubusercontent.com/finos/architecture-as-code/main/calm/draft/2024-03/meta/calm.json',
539+
'nodes': [
540+
{
541+
'unique-id': 'test-node',
542+
'node-type': 'system',
543+
'name': 'Test Node',
544+
'description': 'A test node'
545+
}
546+
],
547+
'relationships': []
548+
};
549+
550+
fetchMock.mockGlobal().route('http://exist/valid-architecture.json', JSON.stringify(validArchitecture));
551+
552+
// Mock the CALM schema
553+
const calmSchema = readFileSync(path.resolve(__dirname, '../../../test_fixtures/calm/calm.json'), 'utf8');
554+
fetchMock.mockGlobal().route('https://raw.githubusercontent.com/finos/architecture-as-code/main/calm/draft/2024-03/meta/calm.json', calmSchema);
555+
556+
// Mock the core schema
557+
const coreSchema = readFileSync(path.resolve(__dirname, '../../../test_fixtures/calm/core.json'), 'utf8');
558+
fetchMock.mockGlobal().route('https://raw.githubusercontent.com/finos/architecture-as-code/main/calm/draft/2024-03/meta/core.json', coreSchema);
559+
560+
const response = await validate('http://exist/valid-architecture.json', '', metaSchemaLocation, false);
561+
562+
expect(response).not.toBeNull();
563+
expect(response).not.toBeUndefined();
564+
565+
// For a valid architecture, we should not have errors
566+
expect(response.hasErrors).toBeFalsy();
567+
expect(response.hasWarnings).toBeFalsy();
568+
});
569+
});
533570

534571
function buildISpectralDiagnostic(code: string, message: string, severity: number): ISpectralDiagnostic {
535572
return {

shared/src/commands/validate/validate.ts

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export async function validate(
138138
} else if (jsonSchemaLocation) {
139139
return await validatePatternOnly(jsonSchemaLocation, metaSchemaPath, debug);
140140
} else if (jsonSchemaArchitectureLocation) {
141-
return await validateArchitectureOnly(jsonSchemaArchitectureLocation);
141+
return await validateArchitectureOnly(jsonSchemaArchitectureLocation, metaSchemaPath, debug);
142142
} else {
143143
logger.debug('You must provide at least an architecture or a pattern');
144144
throw new Error('You must provide at least an architecture or a pattern');
@@ -221,16 +221,87 @@ async function validatePatternOnly(jsonSchemaLocation: string, metaSchemaPath: s
221221

222222
/**
223223
* Run the spectral validations for the case where only the architecture is provided.
224+
* When no pattern is provided, validate the architecture against the CALM schema specified in its $schema property.
224225
*
225226
* @param architectureSchemaLocation - The location of the architecture schema.
226-
* @returns the validation outcome with the results of the spectral validation.
227+
* @param metaSchemaPath - The path of the meta schemas to use for ajv (optional).
228+
* @param debug - The flag to enable debug logging (optional).
229+
* @returns the validation outcome with the results of the spectral validation and JSON schema validation.
227230
*/
228-
async function validateArchitectureOnly(architectureSchemaLocation: string): Promise<ValidationOutcome> {
229-
logger.debug('Pattern was not provided, only the Architecture will be validated');
231+
async function validateArchitectureOnly(architectureSchemaLocation: string, metaSchemaPath?: string, debug: boolean = false): Promise<ValidationOutcome> {
232+
logger.debug('Pattern was not provided, validating Architecture against its specified CALM schema');
230233

231234
const jsonSchemaArchitecture = await getFileFromUrlOrPath(architectureSchemaLocation);
232-
const spectralResultForArchitecture: SpectralResult = await runSpectralValidations(jsonSchemaArchitecture, validationRulesForArchitecture);
233-
return new ValidationOutcome([], spectralResultForArchitecture.spectralIssues, spectralResultForArchitecture.errors, spectralResultForArchitecture.warnings);
235+
const spectralResultForArchitecture: SpectralResult = await runSpectralValidations(jsonSchemaArchitecture, validationRulesForArchitecture);
236+
237+
let jsonSchemaValidations = [];
238+
let errors = spectralResultForArchitecture.errors;
239+
const warnings = spectralResultForArchitecture.warnings;
240+
241+
logger.debug(`metaSchemaPath provided: ${metaSchemaPath}`);
242+
243+
// If metaSchemaPath is provided, attempt to validate against the CALM schema specified in the architecture
244+
if (metaSchemaPath) {
245+
logger.debug('Attempting CALM schema validation');
246+
try {
247+
const architectureObj = typeof jsonSchemaArchitecture === 'string' ? JSON.parse(jsonSchemaArchitecture) : jsonSchemaArchitecture;
248+
const schemaUrl = architectureObj.$schema;
249+
250+
logger.debug(`Parsed architecture object, $schema: ${schemaUrl}`);
251+
252+
if (schemaUrl) {
253+
logger.debug(`Found $schema reference: ${schemaUrl}`);
254+
logger.debug('Validating architecture against its specified CALM schema');
255+
256+
const schemaDirectory = await loadMetaSchemas(metaSchemaPath, debug);
257+
const ajv = buildAjv2020(schemaDirectory, debug);
258+
259+
// Load the schema from the URL specified in the architecture
260+
logger.debug(`Loading schema from: ${schemaUrl}`);
261+
262+
// For schema loading, we need to handle both URL and local file cases
263+
let calmSchemaObject;
264+
const urlPattern = /^https?:\/\//;
265+
if (urlPattern.test(schemaUrl)) {
266+
const content = await loadFileFromUrl(schemaUrl);
267+
calmSchemaObject = typeof content === 'string' ? JSON.parse(content) : content;
268+
} else {
269+
// For local files, read as raw string and parse
270+
if (!existsSync(schemaUrl)) {
271+
throw new Error(`Schema file could not be found at ${schemaUrl}`);
272+
}
273+
const rawContent = await fs.readFile(schemaUrl, 'utf-8');
274+
calmSchemaObject = JSON.parse(rawContent);
275+
}
276+
277+
logger.debug('Loaded schema object');
278+
279+
const validateSchema = await ajv.compileAsync(calmSchemaObject);
280+
281+
logger.debug('Compiled schema, running validation');
282+
const validationResult = validateSchema(architectureObj);
283+
logger.debug(`Validation result: ${validationResult}`);
284+
285+
if (!validationResult) {
286+
logger.debug(`JSON Schema validation raw output: ${prettifyJson(validateSchema.errors)}`);
287+
errors = true;
288+
jsonSchemaValidations = convertJsonSchemaIssuesToValidationOutputs(validateSchema.errors);
289+
logger.debug(`Converted ${jsonSchemaValidations.length} validation errors`);
290+
}
291+
} else {
292+
logger.debug('No $schema property found in architecture document, skipping CALM schema validation');
293+
}
294+
} catch (error) {
295+
logger.debug(`Error during CALM schema validation: ${error.message}`);
296+
logger.debug(`Error stack: ${error.stack}`);
297+
// Don't fail the entire validation if schema validation fails - just log and continue with spectral validation
298+
}
299+
} else {
300+
logger.debug('No metaSchemaPath provided, skipping CALM schema validation');
301+
}
302+
303+
logger.debug(`Returning validation outcome with ${jsonSchemaValidations.length} JSON schema validations, errors: ${errors}`);
304+
return new ValidationOutcome(jsonSchemaValidations, spectralResultForArchitecture.spectralIssues, errors, warnings);
234305
}
235306

236307
function extractSpectralRuleNames(): string[] {

0 commit comments

Comments
 (0)