Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,27 @@ 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);
Commands.validateAST(json);
} catch (err) {
Logger.error('Error validating AST');
process.exit(1);
}
})
.command('version <release>', 'modify the version of one or more model files', yargs => {
yargs.demandOption(['model'], 'Please provide Concerto model(s)');
yargs.positional('release', {
Expand Down
40 changes: 37 additions & 3 deletions lib/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,16 +240,51 @@ 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);
// 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;
}

/**
* Validates a Concerto JSON Abstract Syntax Tree (AST)
*
* @param {object} ast - The AST to validate
* @returns {string} Success message
* @throws {Error} If validation fails or file cannot be read/parsed.
*/
static validateAST(ast) {

const metaModelManager = new ModelManager({
addMetamodel: true
});

const factory = new Factory(metaModelManager);
const serializer = new Serializer(factory, metaModelManager);

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 [email protected]
serializer.fromJSON(ast);
} catch (error) {
throw new Error(`Invalid Concerto Metamodel AST: ${error.message}`);
}
return 'Concerto Metamodel AST is valid.';
}

/**
* Update the version of one or more model files.
*
Expand Down Expand Up @@ -294,7 +329,6 @@ class Commands {
}
}


/**
* Generate a sample JSON instance of a Concept
* @param {string[]} ctoFiles The path to the model file(s).
Expand Down
114 changes: 114 additions & 0 deletions test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ global.fetch = fetch;
const Commands = require('../lib/commands');
const { Parser } = require('@accordproject/concerto-cto');


describe('concerto-cli', () => {
const models = [path.resolve(__dirname, 'models/dom.cto'),path.resolve(__dirname, 'models/money.cto')];
const offlineModels = [path.resolve(__dirname, 'models/contract.cto'),path.resolve(__dirname, 'models/dom.cto'),path.resolve(__dirname, 'models/money.cto'),path.resolve(__dirname, 'models/person.cto')];
Expand Down Expand Up @@ -323,6 +324,45 @@ 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(/Unexpected token/);
} 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: '[email protected]',
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 Metamodel AST:');
} finally {
tempFile.cleanup();
}
});
});

describe('#version (simple)', async () => {
Expand Down Expand Up @@ -871,4 +911,78 @@ describe('concerto-cli', () => {
dir.cleanup();
});
});

describe('#validateAST', () => {
it('should validate a valid Model AST', () => {
const validModel = {
$class: '[email protected]',
namespace: '[email protected]',
imports: [],
declarations: []
};

const result = Commands.validateAST(validModel);
result.should.equal('Concerto Metamodel AST is valid.');
});

it('should detect missing namespace in Model', () => {
const invalidModel = {
$class: '[email protected]',
imports: [],
declarations: []
};

(() => Commands.validateAST(invalidModel)).should.throw('The instance "[email protected]" is missing the required field "namespace".');
});

it('should detect invalid declaration without $class', () => {
const invalidModel = {
$class: '[email protected]',
namespace: '[email protected]',
imports: [],
declarations: [
{
name: 'MyClass',
properties: []
}
]
};

(() => Commands.validateAST(invalidModel)).should.throw('Cannot instantiate the abstract type "Declaration" in the "[email protected]" namespace.');
});

it('should detect invalid declaration without name', () => {
const invalidModel = {
$class: '[email protected]',
namespace: '[email protected]',
imports: [],
declarations: [
{
$class: '[email protected]',
properties: []
}
]
};

(() => Commands.validateAST(invalidModel)).should.throw('The instance "[email protected]" is missing the required field "name".');
});

it('should detect missing $class property', () => {
const invalidAST = {
namespace: '[email protected]',
imports: [],
declarations: []
};

(() => Commands.validateAST(invalidAST)).should.throw('Invalid JSON data. Does not contain a $class type identifier.');
});

it('should handle AST with unsupported type', () => {
const unsupportedModel = {
$class: '[email protected]',
someProperty: 'test'
};
(() => Commands.validateAST(unsupportedModel)).should.throw('Type "UnknownType" is not defined in namespace "[email protected]".');
});
});
});