From 888a943c73721b05c7f70c40042d8e6ca0006ba2 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Thu, 13 Mar 2025 15:11:15 +0200 Subject: [PATCH 1/5] feat(validate): add command to validate JSON AST against Concerto metamodel and enhance print function with AST validation Add validateAST method to validate Concerto metamodel JSON structures Enhance print function to validate AST structure before converting to CTO Provide detailed error messages for invalid AST structur Signed-off-by: Hussein Saad --- index.js | 32 +++++++ lib/commands.js | 110 ++++++++++++++++++++- test/cli.js | 247 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 387 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 6d9f99f..0c137f5 100755 --- a/index.js +++ b/index.js @@ -19,6 +19,7 @@ const Logger = require('@accordproject/concerto-util').Logger; const fs = require('fs'); const { glob } = require('glob'); const Commands = require('./lib/commands'); +const c = require('ansi-colors'); require('yargs') .scriptName('concerto') @@ -262,6 +263,37 @@ require('yargs') Logger.error(err.message); }); }) + .command('validate-ast', 'validate a JSON syntax tree against the Concerto metamodel', (yargs) => { + yargs.demandOption(['input'], 'Please provide an input Concerto syntax tree'); + yargs.option('input', { + describe: 'the metamodel to validate', + type: 'string' + }); + yargs.option('strict', { + describe: 'perform strict validation', + type: 'boolean', + default: true + }); + }, (argv) => { + try { + const inputString = fs.readFileSync(argv.input, 'utf8'); + const json = JSON.parse(inputString); + const validationResult = Commands.validateAST(json, argv.strict); + if (validationResult.valid) { + Logger.info(c.green('AST is valid')); + return true; + } else { + Logger.error('AST validation failed:'); + validationResult.errors.forEach(error => { + Logger.error(` - ${error}`); + }); + process.exit(1); + } + } catch (err) { + Logger.error(`Error validating AST: ${err.message}`); + process.exit(1); + } + }) .command('version ', 'modify the version of one or more model files', yargs => { yargs.demandOption(['model'], 'Please provide Concerto model(s)'); yargs.positional('release', { diff --git a/lib/commands.js b/lib/commands.js index bbe1681..73812ed 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -240,7 +240,21 @@ class Commands { */ static async print(input, outputPath) { const inputString = fs.readFileSync(input, 'utf8'); - const json = JSON.parse(inputString); + let json; + try { + json = JSON.parse(inputString); + } catch (e) { + throw new Error(`Failed to parse metamodel: ${e.message}`); + } + + // Validate the AST before printing + const validationResult = Commands.validateAST(json); + if (!validationResult.valid) { + throw new Error( + `Invalid Concerto AST: ${validationResult.errors.join(', ')}` + ); + } + const result = Printer.toCTO(json); if (outputPath) { Logger.info('Creating file: ' + outputPath); @@ -250,6 +264,99 @@ class Commands { return result; } + /** + * Validates a Concerto JSON Abstract Syntax Tree (AST) + * + * @param {object} ast - The AST to validate + * @param {boolean} [strict=true] - Whether to perform strict validation + * @returns {object} - Validation result with valid flag and errors array + */ + static validateAST(ast, strict = true) { + const errors = []; + let valid = true; + + try { + // Check if the AST has the expected structure + if (!ast) { + errors.push('AST is null or undefined'); + return { valid: false, errors }; + } + + // For model type, validate the structure based on Concerto metamodel + if (ast.$class === 'concerto.metamodel@1.0.0.Model') { + // Validate namespace + if (!ast.namespace) { + errors.push('Model must have a namespace'); + valid = false; + } + + // Validate declarations + if (!Array.isArray(ast.declarations)) { + errors.push('Model declarations must be an array'); + valid = false; + } else { + // Check each declaration + ast.declarations.forEach((declaration, index) => { + if (!declaration.$class) { + errors.push( + `Declaration at index ${index} must have a $class property` + ); + valid = false; + } + + if (!declaration.name) { + errors.push( + `Declaration at index ${index} must have a name property` + ); + valid = false; + } + }); + } + } else if (ast.$class === 'concerto.metamodel@1.0.0.Models') { + // Validate Models container + if (!Array.isArray(ast.models)) { + errors.push('Models must have a models array'); + valid = false; + } else { + // Validate each model in the array + ast.models.forEach((model, index) => { + const modelResult = Commands.validateAST(model, strict); + if (!modelResult.valid) { + valid = false; + errors.push( + `Model at index ${index} is invalid: ${modelResult.errors.join( + ', ' + )}` + ); + } + }); + } + } else if (!ast.$class) { + errors.push( + 'AST must have a $class property defining its type' + ); + valid = false; + } + + // Use MetaModelUtil for additional validation if available + if (strict && MetaModelUtil.validateMetaModel) { + try { + MetaModelUtil.validateMetaModel(ast); + } catch (validationError) { + errors.push( + `MetaModel validation error: ${validationError.message}` + ); + valid = false; + } + } + } catch (error) { + errors.push(`Unexpected error during validation: ${error.message}`); + valid = false; + } + + return { valid, errors }; + } + /** * Update the version of one or more model files. * @@ -294,7 +401,6 @@ class Commands { } } - /** * Generate a sample JSON instance of a Concept * @param {string[]} ctoFiles The path to the model file(s). diff --git a/test/cli.js b/test/cli.js index aca226b..67677b9 100644 --- a/test/cli.js +++ b/test/cli.js @@ -30,6 +30,7 @@ global.fetch = fetch; const Commands = require('../lib/commands'); const { Parser } = require('@accordproject/concerto-cto'); +const { MetaModelUtil } = require('@accordproject/concerto-metamodel'); describe('concerto-cli', () => { const models = [path.resolve(__dirname, 'models/dom.cto'),path.resolve(__dirname, 'models/money.cto')]; @@ -323,6 +324,46 @@ describe('concerto-cli', () => { result.should.equal(expected); output.cleanup(); }); + it('should handle invalid JSON metamodel content', async () => { + const tempFile = await tmp.file({ unsafeCleanup: true }); + fs.writeFileSync(tempFile.path, 'This is not valid JSON', 'utf-8'); + try { + await Commands.print(tempFile.path); + expect.fail('Expected error was not thrown'); + } catch (err) { + err.should.be.an.instanceOf(Error); + err.message.should.match(/Failed to parse metamodel/); + } finally { + tempFile.cleanup(); + } + }); + it('should handle file not found errors', async () => { + try { + await Commands.print('/path/to/nonexistent/file.json'); + expect.fail('Expected error was not thrown'); + } catch (err) { + err.should.be.an.instanceOf(Error); + } + }); + it('should handle invalid AST validation in metamodel', async () => { + const tempFile = await tmp.file({ unsafeCleanup: true }); + const invalidAST = { + $class: 'concerto.metamodel@1.0.0.Model', + imports: [], + declarations: [] + }; + fs.writeFileSync(tempFile.path, JSON.stringify(invalidAST), 'utf-8'); + try { + await Commands.print(tempFile.path); + expect.fail('Expected error was not thrown'); + } catch (err) { + err.should.be.an.instanceOf(Error); + err.message.should.include('Invalid Concerto AST:'); + err.message.should.include('Model must have a namespace'); + } finally { + tempFile.cleanup(); + } + }); }); describe('#version (simple)', async () => { @@ -871,4 +912,210 @@ describe('concerto-cli', () => { dir.cleanup(); }); }); + + describe('#validateAST', () => { + it('should validate a valid Model AST', () => { + const validModel = { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example@1.0.0', + imports: [], + declarations: [] + }; + + const result = Commands.validateAST(validModel); + result.should.have.property('valid', true); + result.should.have.property('errors').with.lengthOf(0); + }); + + it('should validate a Models container with valid models', () => { + const validModels = { + $class: 'concerto.metamodel@1.0.0.Models', + models: [ + { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example1@1.0.0', + imports: [], + declarations: [] + }, + { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example2@1.0.0', + imports: [], + declarations: [] + } + ] + }; + + const result = Commands.validateAST(validModels); + result.should.have.property('valid', true); + result.should.have.property('errors').with.lengthOf(0); + }); + + it('should detect missing namespace in Model', () => { + const invalidModel = { + $class: 'concerto.metamodel@1.0.0.Model', + imports: [], + declarations: [] + }; + + const result = Commands.validateAST(invalidModel); + result.should.have.property('valid', false); + result.errors.should.include('Model must have a namespace'); + }); + + it('should detect missing declarations array in Model', () => { + const invalidModel = { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example@1.0.0', + imports: [] + }; + + const result = Commands.validateAST(invalidModel); + result.should.have.property('valid', false); + result.errors.should.include('Model declarations must be an array'); + }); + + it('should detect invalid declaration without $class', () => { + const invalidModel = { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example@1.0.0', + imports: [], + declarations: [ + { + name: 'MyClass', + properties: [] + } + ] + }; + + const result = Commands.validateAST(invalidModel); + result.should.have.property('valid', false); + result.errors.should.include('Declaration at index 0 must have a $class property'); + }); + + it('should detect invalid declaration without name', () => { + const invalidModel = { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example@1.0.0', + imports: [], + declarations: [ + { + $class: 'concerto.metamodel@1.0.0.ConceptDeclaration', + properties: [] + } + ] + }; + + const result = Commands.validateAST(invalidModel); + result.should.have.property('valid', false); + result.errors.should.include('Declaration at index 0 must have a name property'); + }); + + it('should detect missing models array in Models container', () => { + const invalidModels = { + $class: 'concerto.metamodel@1.0.0.Models', + }; + + const result = Commands.validateAST(invalidModels); + result.should.have.property('valid', false); + result.errors.should.include('Models must have a models array'); + }); + + it('should detect invalid model in Models container', () => { + const invalidModels = { + $class: 'concerto.metamodel@1.0.0.Models', + models: [ + { + $class: 'concerto.metamodel@1.0.0.Model', + imports: [], + declarations: [] + } + ] + }; + + const result = Commands.validateAST(invalidModels); + result.should.have.property('valid', false); + result.errors[0].should.include('Model at index 0 is invalid'); + }); + + it('should detect missing $class property', () => { + const invalidAST = { + namespace: 'org.example@1.0.0', + imports: [], + declarations: [] + }; + + const result = Commands.validateAST(invalidAST); + result.should.have.property('valid', false); + result.errors.should.include('AST must have a $class property defining its type'); + }); + + it('should validate with strict mode off', () => { + const validModel = { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example@1.0.0', + imports: [], + declarations: [] + }; + const result = Commands.validateAST(validModel, false); + result.should.have.property('valid', true); + result.should.have.property('errors').with.lengthOf(0); + }); + + it('should handle null/undefined AST', () => { + const result = Commands.validateAST(null); + result.should.have.property('valid', false); + result.errors.should.include('AST is null or undefined'); + }); + + it('should handle validation errors from MetaModelUtil', () => { + const originalValidateMetaModel = MetaModelUtil.validateMetaModel; + MetaModelUtil.validateMetaModel = function() { + throw new Error('Validation failed'); + }; + const validModel = { + $class: 'concerto.metamodel@1.0.0.Model', + namespace: 'org.example@1.0.0', + imports: [], + declarations: [] + }; + try { + const result = Commands.validateAST(validModel); + result.should.have.property('valid', false); + result.errors.should.include('MetaModel validation error: Validation failed'); + } finally { + MetaModelUtil.validateMetaModel = originalValidateMetaModel; + } + }); + + it('should handle unexpected errors during validation', () => { + const badModel = {}; + Object.defineProperty(badModel, '$class', { + get: function() { throw new Error('Unexpected error'); } + }); + const result = Commands.validateAST(badModel); + result.should.have.property('valid', false); + result.errors.should.include('Unexpected error during validation: Unexpected error'); + }); + + it('should handle AST with unsupported type', () => { + const unsupportedModel = { + $class: 'concerto.metamodel@1.0.0.UnknownType', + someProperty: 'test' + }; + const result = Commands.validateAST(unsupportedModel); + result.should.have.property('valid', true); + result.should.have.property('errors').with.lengthOf(0); + }); + + it('should validate a Models container with an empty models array', () => { + const emptyModelsContainer = { + $class: 'concerto.metamodel@1.0.0.Models', + models: [] + }; + const result = Commands.validateAST(emptyModelsContainer); + result.should.have.property('valid', true); + result.should.have.property('errors').with.lengthOf(0); + }); + }); }); From dcebc427421b62af1bac1065f042cb783e733866 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sat, 29 Mar 2025 05:10:57 +0200 Subject: [PATCH 2/5] refactor(validation): simplify AST validation process Signed-off-by: Hussein Saad --- index.js | 15 +------ lib/commands.js | 113 ++++++++++-------------------------------------- 2 files changed, 25 insertions(+), 103 deletions(-) diff --git a/index.js b/index.js index 0c137f5..977e583 100755 --- a/index.js +++ b/index.js @@ -19,7 +19,6 @@ const Logger = require('@accordproject/concerto-util').Logger; const fs = require('fs'); const { glob } = require('glob'); const Commands = require('./lib/commands'); -const c = require('ansi-colors'); require('yargs') .scriptName('concerto') @@ -278,19 +277,9 @@ require('yargs') try { const inputString = fs.readFileSync(argv.input, 'utf8'); const json = JSON.parse(inputString); - const validationResult = Commands.validateAST(json, argv.strict); - if (validationResult.valid) { - Logger.info(c.green('AST is valid')); - return true; - } else { - Logger.error('AST validation failed:'); - validationResult.errors.forEach(error => { - Logger.error(` - ${error}`); - }); - process.exit(1); - } + Commands.validateAST(json); } catch (err) { - Logger.error(`Error validating AST: ${err.message}`); + Logger.error('Error validating AST'); process.exit(1); } }) diff --git a/lib/commands.js b/lib/commands.js index 73812ed..ebd0058 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -243,23 +243,22 @@ class Commands { let json; try { json = JSON.parse(inputString); - } catch (e) { - throw new Error(`Failed to parse metamodel: ${e.message}`); + } catch (error) { + throw new Error(`Failed to parse metamodel: ${error.message}`); } - // Validate the AST before printing - const validationResult = Commands.validateAST(json); - if (!validationResult.valid) { - throw new Error( - `Invalid Concerto AST: ${validationResult.errors.join(', ')}` - ); + try{ + // Validate the AST before printing + this.validateAST(json); + } catch (error){ + throw new Error(error.message); } const result = Printer.toCTO(json); if (outputPath) { Logger.info('Creating file: ' + outputPath); fs.writeFileSync(outputPath, result); - return; + return; // Return nothing if writing to file } return result; } @@ -268,93 +267,27 @@ class Commands { * Validates a Concerto JSON Abstract Syntax Tree (AST) * * @param {object} ast - The AST to validate - * @param {boolean} [strict=true] - Whether to perform strict validation - * @returns {object} - Validation result with valid flag and errors array + * @returns {string} Success message + * @throws {Error} If validation fails or file cannot be read/parsed. */ - static validateAST(ast, strict = true) { - const errors = []; - let valid = true; + static validateAST(ast) { - try { - // Check if the AST has the expected structure - if (!ast) { - errors.push('AST is null or undefined'); - return { valid: false, errors }; - } - - // For model type, validate the structure based on Concerto metamodel - if (ast.$class === 'concerto.metamodel@1.0.0.Model') { - // Validate namespace - if (!ast.namespace) { - errors.push('Model must have a namespace'); - valid = false; - } + const metaModelManager = new ModelManager(); + // Load the official Concerto Metamodel CTO definition + metaModelManager.addCTOModel(MetaModelUtil.metaModelCto); - // Validate declarations - if (!Array.isArray(ast.declarations)) { - errors.push('Model declarations must be an array'); - valid = false; - } else { - // Check each declaration - ast.declarations.forEach((declaration, index) => { - if (!declaration.$class) { - errors.push( - `Declaration at index ${index} must have a $class property` - ); - valid = false; - } - - if (!declaration.name) { - errors.push( - `Declaration at index ${index} must have a name property` - ); - valid = false; - } - }); - } - } else if (ast.$class === 'concerto.metamodel@1.0.0.Models') { - // Validate Models container - if (!Array.isArray(ast.models)) { - errors.push('Models must have a models array'); - valid = false; - } else { - // Validate each model in the array - ast.models.forEach((model, index) => { - const modelResult = Commands.validateAST(model, strict); - if (!modelResult.valid) { - valid = false; - errors.push( - `Model at index ${index} is invalid: ${modelResult.errors.join( - ', ' - )}` - ); - } - }); - } - } else if (!ast.$class) { - errors.push( - 'AST must have a $class property defining its type' - ); - valid = false; - } + const factory = new Factory(metaModelManager); + const serializer = new Serializer(factory, metaModelManager); - // Use MetaModelUtil for additional validation if available - if (strict && MetaModelUtil.validateMetaModel) { - try { - MetaModelUtil.validateMetaModel(ast); - } catch (validationError) { - errors.push( - `MetaModel validation error: ${validationError.message}` - ); - valid = false; - } - } + try { + // deserialize the user's AST using the metamodel's rules. + // This will validates the structure, types, and constraints ($class, required fields etc.) + // defined in concerto.metamodel@1.0.0.cto + serializer.fromJSON(ast); } catch (error) { - errors.push(`Unexpected error during validation: ${error.message}`); - valid = false; + throw new Error(`Invalid Concerto Metamodel AST: ${error.message}`); } - - return { valid, errors }; + return 'Concerto Metamodel AST is valid.'; } /** From cf870a6d6860d7b596c63b0b476368b93b0c2e52 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 6 Apr 2025 01:19:02 +0200 Subject: [PATCH 3/5] fix(commands): enable automatic addition of the Concerto metamodel in AST validation Signed-off-by: Hussein Saad --- lib/commands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/commands.js b/lib/commands.js index ebd0058..026406c 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -272,9 +272,9 @@ class Commands { */ static validateAST(ast) { - const metaModelManager = new ModelManager(); - // Load the official Concerto Metamodel CTO definition - metaModelManager.addCTOModel(MetaModelUtil.metaModelCto); + const metaModelManager = new ModelManager({ + addMetamodel: true + }); const factory = new Factory(metaModelManager); const serializer = new Serializer(factory, metaModelManager); From f7baab81e3dd3f8eb3aebb4867c5e4d691fc5a6e Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 6 Apr 2025 01:27:42 +0200 Subject: [PATCH 4/5] refactor(commands): Refactor error handling in print method Signed-off-by: Hussein Saad --- lib/commands.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/commands.js b/lib/commands.js index 026406c..e150c85 100644 --- a/lib/commands.js +++ b/lib/commands.js @@ -243,14 +243,9 @@ class Commands { let json; try { json = JSON.parse(inputString); - } catch (error) { - throw new Error(`Failed to parse metamodel: ${error.message}`); - } - - try{ // Validate the AST before printing this.validateAST(json); - } catch (error){ + } catch (error) { throw new Error(error.message); } From 460d757e63521a34610d5d35a400626000743fa9 Mon Sep 17 00:00:00 2001 From: Hussein Saad Date: Sun, 6 Apr 2025 01:46:07 +0200 Subject: [PATCH 5/5] fix(test): Fix the tests for AST validation Signed-off-by: Hussein Saad --- test/cli.js | 151 ++++------------------------------------------------ 1 file changed, 9 insertions(+), 142 deletions(-) diff --git a/test/cli.js b/test/cli.js index 67677b9..eb9a9b3 100644 --- a/test/cli.js +++ b/test/cli.js @@ -30,7 +30,7 @@ global.fetch = fetch; const Commands = require('../lib/commands'); const { Parser } = require('@accordproject/concerto-cto'); -const { MetaModelUtil } = require('@accordproject/concerto-metamodel'); + describe('concerto-cli', () => { const models = [path.resolve(__dirname, 'models/dom.cto'),path.resolve(__dirname, 'models/money.cto')]; @@ -332,7 +332,7 @@ describe('concerto-cli', () => { expect.fail('Expected error was not thrown'); } catch (err) { err.should.be.an.instanceOf(Error); - err.message.should.match(/Failed to parse metamodel/); + err.message.should.match(/Unexpected token/); } finally { tempFile.cleanup(); } @@ -358,8 +358,7 @@ describe('concerto-cli', () => { expect.fail('Expected error was not thrown'); } catch (err) { err.should.be.an.instanceOf(Error); - err.message.should.include('Invalid Concerto AST:'); - err.message.should.include('Model must have a namespace'); + err.message.should.include('Invalid Concerto Metamodel AST:'); } finally { tempFile.cleanup(); } @@ -923,32 +922,7 @@ describe('concerto-cli', () => { }; const result = Commands.validateAST(validModel); - result.should.have.property('valid', true); - result.should.have.property('errors').with.lengthOf(0); - }); - - it('should validate a Models container with valid models', () => { - const validModels = { - $class: 'concerto.metamodel@1.0.0.Models', - models: [ - { - $class: 'concerto.metamodel@1.0.0.Model', - namespace: 'org.example1@1.0.0', - imports: [], - declarations: [] - }, - { - $class: 'concerto.metamodel@1.0.0.Model', - namespace: 'org.example2@1.0.0', - imports: [], - declarations: [] - } - ] - }; - - const result = Commands.validateAST(validModels); - result.should.have.property('valid', true); - result.should.have.property('errors').with.lengthOf(0); + result.should.equal('Concerto Metamodel AST is valid.'); }); it('should detect missing namespace in Model', () => { @@ -958,21 +932,7 @@ describe('concerto-cli', () => { declarations: [] }; - const result = Commands.validateAST(invalidModel); - result.should.have.property('valid', false); - result.errors.should.include('Model must have a namespace'); - }); - - it('should detect missing declarations array in Model', () => { - const invalidModel = { - $class: 'concerto.metamodel@1.0.0.Model', - namespace: 'org.example@1.0.0', - imports: [] - }; - - const result = Commands.validateAST(invalidModel); - result.should.have.property('valid', false); - result.errors.should.include('Model declarations must be an array'); + (() => Commands.validateAST(invalidModel)).should.throw('The instance "concerto.metamodel@1.0.0.Model" is missing the required field "namespace".'); }); it('should detect invalid declaration without $class', () => { @@ -988,9 +948,7 @@ describe('concerto-cli', () => { ] }; - const result = Commands.validateAST(invalidModel); - result.should.have.property('valid', false); - result.errors.should.include('Declaration at index 0 must have a $class property'); + (() => Commands.validateAST(invalidModel)).should.throw('Cannot instantiate the abstract type "Declaration" in the "concerto.metamodel@1.0.0" namespace.'); }); it('should detect invalid declaration without name', () => { @@ -1006,36 +964,7 @@ describe('concerto-cli', () => { ] }; - const result = Commands.validateAST(invalidModel); - result.should.have.property('valid', false); - result.errors.should.include('Declaration at index 0 must have a name property'); - }); - - it('should detect missing models array in Models container', () => { - const invalidModels = { - $class: 'concerto.metamodel@1.0.0.Models', - }; - - const result = Commands.validateAST(invalidModels); - result.should.have.property('valid', false); - result.errors.should.include('Models must have a models array'); - }); - - it('should detect invalid model in Models container', () => { - const invalidModels = { - $class: 'concerto.metamodel@1.0.0.Models', - models: [ - { - $class: 'concerto.metamodel@1.0.0.Model', - imports: [], - declarations: [] - } - ] - }; - - const result = Commands.validateAST(invalidModels); - result.should.have.property('valid', false); - result.errors[0].should.include('Model at index 0 is invalid'); + (() => Commands.validateAST(invalidModel)).should.throw('The instance "concerto.metamodel@1.0.0.ConceptDeclaration" is missing the required field "name".'); }); it('should detect missing $class property', () => { @@ -1045,57 +974,7 @@ describe('concerto-cli', () => { declarations: [] }; - const result = Commands.validateAST(invalidAST); - result.should.have.property('valid', false); - result.errors.should.include('AST must have a $class property defining its type'); - }); - - it('should validate with strict mode off', () => { - const validModel = { - $class: 'concerto.metamodel@1.0.0.Model', - namespace: 'org.example@1.0.0', - imports: [], - declarations: [] - }; - const result = Commands.validateAST(validModel, false); - result.should.have.property('valid', true); - result.should.have.property('errors').with.lengthOf(0); - }); - - it('should handle null/undefined AST', () => { - const result = Commands.validateAST(null); - result.should.have.property('valid', false); - result.errors.should.include('AST is null or undefined'); - }); - - it('should handle validation errors from MetaModelUtil', () => { - const originalValidateMetaModel = MetaModelUtil.validateMetaModel; - MetaModelUtil.validateMetaModel = function() { - throw new Error('Validation failed'); - }; - const validModel = { - $class: 'concerto.metamodel@1.0.0.Model', - namespace: 'org.example@1.0.0', - imports: [], - declarations: [] - }; - try { - const result = Commands.validateAST(validModel); - result.should.have.property('valid', false); - result.errors.should.include('MetaModel validation error: Validation failed'); - } finally { - MetaModelUtil.validateMetaModel = originalValidateMetaModel; - } - }); - - it('should handle unexpected errors during validation', () => { - const badModel = {}; - Object.defineProperty(badModel, '$class', { - get: function() { throw new Error('Unexpected error'); } - }); - const result = Commands.validateAST(badModel); - result.should.have.property('valid', false); - result.errors.should.include('Unexpected error during validation: Unexpected error'); + (() => Commands.validateAST(invalidAST)).should.throw('Invalid JSON data. Does not contain a $class type identifier.'); }); it('should handle AST with unsupported type', () => { @@ -1103,19 +982,7 @@ describe('concerto-cli', () => { $class: 'concerto.metamodel@1.0.0.UnknownType', someProperty: 'test' }; - const result = Commands.validateAST(unsupportedModel); - result.should.have.property('valid', true); - result.should.have.property('errors').with.lengthOf(0); - }); - - it('should validate a Models container with an empty models array', () => { - const emptyModelsContainer = { - $class: 'concerto.metamodel@1.0.0.Models', - models: [] - }; - const result = Commands.validateAST(emptyModelsContainer); - result.should.have.property('valid', true); - result.should.have.property('errors').with.lengthOf(0); + (() => Commands.validateAST(unsupportedModel)).should.throw('Type "UnknownType" is not defined in namespace "concerto.metamodel@1.0.0".'); }); }); });