Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for issue #89: Add ctoSystem option to CLI, other CLI improvements #113

Merged
merged 5 commits into from
Oct 8, 2019
Merged
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
11 changes: 7 additions & 4 deletions packages/concerto-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Options:
--verbose, -v [default: false]
--help Show help [boolean]
--sample sample JSON to validate [string] [default: "sample.json"]
--ctoSystem system model to be used [string]
--ctoFiles array of CTO files [array] [default: "."]
```

Expand All @@ -49,6 +50,7 @@ Options:
--version Show version number [boolean]
--verbose, -v [default: false]
--help Show help [boolean]
--ctoSystem system model to be used [string]
--ctoFiles array of CTO files [array] [default: "."]
--format format of the code to generate
[string] [default: "JSONSchema"]
Expand Down Expand Up @@ -101,10 +103,11 @@ concerto get
save local copies of external model dependencies

Options:
--version Show version number [boolean]
--version Show version number [boolean]
--verbose, -v [default: false]
--help Show help [boolean]
--ctoFiles array of local CTO files [array] [default: "."]
--out output directory path [string] [default: "./"]
--help Show help [boolean]
--ctoFiles array of local CTO files [array] [default: "."]
--ctoSystem system model to be used [string]
--outputDirectory output directory path [string] [default: "./"]
```

23 changes: 19 additions & 4 deletions packages/concerto-cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ require('yargs')
type: 'string',
default: 'sample.json'
});
yargs.option('ctoSystem', {
describe: 'system model to be used',
type: 'string'
});
yargs.option('ctoFiles', {
describe: 'array of CTO files',
type: 'string',
Expand All @@ -38,7 +42,7 @@ require('yargs')
Logger.info(`validate sample in ${argv.format} against the models ${argv.ctoFiles}`);
}

return Commands.validate(argv.sample, argv.ctoFiles)
return Commands.validate(argv.sample, argv.ctoSystem, argv.ctoFiles)
.then((result) => {
Logger.info(result);
})
Expand All @@ -47,6 +51,10 @@ require('yargs')
});
})
.command('generate', 'generate code from model files', (yargs) => {
yargs.option('ctoSystem', {
describe: 'system model to be used',
type: 'string'
});
yargs.option('ctoFiles', {
describe: 'array of CTO files',
type: 'string',
Expand All @@ -68,7 +76,7 @@ require('yargs')
Logger.info(`generate code in format ${argv.format} from the models ${argv.ctoFiles} into directory ${argv.outputDirectory}`);
}

return Commands.generate(argv.format, argv.ctoFiles, argv.outputDirectory)
return Commands.generate(argv.format, argv.ctoSystem, argv.ctoFiles, argv.outputDirectory)
.then((result) => {
Logger.info(result);
})
Expand All @@ -83,6 +91,10 @@ require('yargs')
array: true,
default: '.'
});
yargs.option('ctoSystem', {
describe: 'system model to be used',
type: 'string'
});
yargs.option('outputDirectory', {
describe: 'output directory path',
type: 'string',
Expand All @@ -93,7 +105,10 @@ require('yargs')
Logger.info(`Saving external models from ${argv.ctoFiles} into directory: ${argv.outputDirectory}`);
}

return Commands.getExternalModels(argv.ctoFiles, argv.outputDirectory)
return Commands.getExternalModels(argv.ctoSystem, argv.ctoFiles, argv.outputDirectory)
.then((result) => {
Logger.info(result);
})
.catch((err) => {
Logger.error(err.message);
});
Expand All @@ -103,4 +118,4 @@ require('yargs')
default: false
})
.help()
.argv;
.argv;
121 changes: 85 additions & 36 deletions packages/concerto-cli/lib/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
'use strict';

const fs = require('fs');
const mkdirp = require('mkdirp');

const { ModelManager, Factory, Serializer } = require('@accordproject/concerto-core');
const ModelFile = require('@accordproject/concerto-core').ModelFile;
Expand All @@ -29,45 +30,106 @@ const PlantUMLVisitor = CodeGen.PlantUMLVisitor;
const TypescriptVisitor = CodeGen.TypescriptVisitor;
const XmlSchemaVisitor = CodeGen.XmlSchemaVisitor;

const defaultSystemContent = `namespace org.accordproject.base
abstract asset Asset { }
abstract participant Participant { }
abstract transaction Transaction identified by transactionId {
o String transactionId
}
abstract event Event identified by eventId {
o String eventId
}`;
const defaultSystemName = `@org.accordproject.base`

/**
* Utility class that implements the commands exposed by the CLI.
* @class
* @memberof module:concerto-cli
*/
class Commands {

/**
* Add model file
*
* @param {object} modelFileLoader the model loader
* @param {object} modelManager the model manager
* @param {string} ctoFile the model file
* @param {boolean} system whether this is a system model
* @return {object} the model manager
*/
static async addModel(modelFileLoader, modelManager, ctoFile, system) {
let modelFile = null;
if (system && !ctoFile) {
modelFile = new ModelFile(modelManager, defaultSystemContent, defaultSystemName, true);
} else if(modelFileLoader.accepts(ctoFile)) {
modelFile = await modelFileLoader.load(ctoFile);
} else {
const content = fs.readFileSync(ctoFile, 'utf8');
modelFile = new ModelFile(modelManager, content, ctoFile);
}

if (system) {
modelManager.addModelFile(modelFile, modelFile.getName(), false, true);
} else {
modelManager.addModelFile(modelFile, modelFile.getName(), true, false);
}

return modelManager;
}

/**
* Load system and models in a new model manager
*
* @param {string} ctoSystemFile the system model
* @param {string[]} ctoFiles the CTO files (can be local file paths or URLs)
* @return {object} the model manager
*/
static async loadModelManager(ctoSystemFile, ctoFiles) {
let modelManager = new ModelManager();
const modelFileLoader = new DefaultModelFileLoader(modelManager);

// Load system model
modelManager = await Commands.addModel(modelFileLoader,modelManager,ctoSystemFile,true);

// Load user models
for( let ctoFile of ctoFiles ) {
modelManager = await Commands.addModel(modelFileLoader,modelManager,ctoFile,false);
}

// Validate update models
await modelManager.updateExternalModels();
return modelManager;
}

/**
* Validate a sample JSON against the model
*
* @param {string} sample the sample to validate
* @param {string} ctoSystem the system model
* @param {string[]} ctoFiles the CTO files to convert to code
* @returns {string} serialized form of the validated JSON
*/
static async validate(sample, ctoFiles, out) {
static async validate(sample, ctoSystemFile, ctoFiles, out) {
const json = JSON.parse(fs.readFileSync(sample, 'utf8'));

const modelManager = new ModelManager();
const modelManager = await Commands.loadModelManager(ctoSystemFile, ctoFiles);
const factory = new Factory(modelManager);
const serializer = new Serializer(factory, modelManager);

const modelFiles = ctoFiles.map((ctoFile) => {
return fs.readFileSync(ctoFile, 'utf8');
});
modelManager.addModelFiles(modelFiles, ctoFiles, true);
await modelManager.updateExternalModels();
const object = serializer.fromJSON(json);
return JSON.stringify(serializer.toJSON(object));
}

static async generate(format, ctoFiles, outputDirectory) {

const modelManager = new ModelManager();

const modelFiles = ctoFiles.map((ctoFile) => {
return fs.readFileSync(ctoFile, 'utf8');
});
modelManager.addModelFiles(modelFiles, ctoFiles, true);
await modelManager.updateExternalModels();
/**
* Generate code from models for a given format
*
* @param {string} the format of the code to generate
* @param {string} ctoSystem the system model
* @param {string[]} ctoFiles the CTO files to convert to code
* @param {string} outputDirectory the output directory
*/
static async generate(format, ctoSystemFile, ctoFiles, outputDirectory) {
const modelManager = await Commands.loadModelManager(ctoSystemFile, ctoFiles);

let visitor = null;

Expand Down Expand Up @@ -96,7 +158,7 @@ class Commands {
let parameters = {};
parameters.fileWriter = new FileWriter(outputDirectory);
modelManager.accept(visitor, parameters);
return `Generated ${format} code.`;
return `Generated ${format} code in '${outputDirectory}'.`;
}
else {
return 'Unrecognized code generator: ' + format;
Expand All @@ -107,29 +169,16 @@ class Commands {
* Fetches all external for a set of models dependencies and
* saves all the models to a target directory
*
* @param {string} ctoSystemFile the system model
* @param {string[]} ctoFiles the CTO files (can be local file paths or URLs)
* @param {string} outputDirectory the output directory
*/
static async getExternalModels(ctoFiles, outputDirectory) {

const modelManager = new ModelManager();
const modelFileLoader = new DefaultModelFileLoader(modelManager);

for( let ctoFile of ctoFiles ) {
let modelFile = null;
if(modelFileLoader.accepts(ctoFile)) {
modelFile = await modelFileLoader.load(ctoFile);
} else {
const content = fs.readFileSync(ctoFile, 'utf8');
modelFile = new ModelFile(modelManager, content, ctoFile);
}

modelManager.addModelFile(modelFile, modelFile.getName(), true);
}

await modelManager.updateExternalModels();
static async getExternalModels(ctoSystemFile, ctoFiles, outputDirectory) {
const modelManager = await Commands.loadModelManager(ctoSystemFile, ctoFiles);
mkdirp.sync(outputDirectory);
modelManager.writeModelsToFileSystem(outputDirectory);
return `Loaded external models in '${outputDirectory}'.`;
}
}

module.exports = Commands;
module.exports = Commands;
32 changes: 21 additions & 11 deletions packages/concerto-cli/test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,21 @@ const Commands = require('../lib/commands');

describe('cicero-cli', () => {
const models = [path.resolve(__dirname, 'models/dom.cto'),path.resolve(__dirname, 'models/money.cto')];
const hlModel = path.resolve(__dirname, 'models/org.hyperledger.composer.system.cto');
const sample1 = path.resolve(__dirname, 'data/sample1.json');
const sample2 = path.resolve(__dirname, 'data/sample2.json');
const sampleText1 = fs.readFileSync(sample1, 'utf8');
const sampleText2 = fs.readFileSync(sample2, 'utf8');

describe('#validate', () => {
it('should validate against a model', async () => {
const result = await Commands.validate(sample1, models);
const result = await Commands.validate(sample1, null, models);
JSON.parse(result).should.deep.equal(JSON.parse(sampleText1));
});

it('should fail to validate against a model', async () => {
try {
const result = await Commands.validate(sample2, models);
const result = await Commands.validate(sample2, null, models);
JSON.parse(result).should.deep.equal(JSON.parse(sampleText1));
} catch (err) {
err.message.should.equal('Instance undefined invalid enum value true for field CurrencyCode');
Expand All @@ -52,43 +53,43 @@ describe('cicero-cli', () => {

it('should generate a Go model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('Go', models, dir.path, true);
await Commands.generate('Go', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.above(0);
dir.cleanup();
});
it('should generate a PlantUML model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('PlantUML', models, dir.path, true);
await Commands.generate('PlantUML', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.above(0);
dir.cleanup();
});
it('should generate a Typescript model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('Typescript', models, dir.path, true);
await Commands.generate('Typescript', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.above(0);
dir.cleanup();
});
it('should generate a Java model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('Java', models, dir.path, true);
await Commands.generate('Java', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.above(0);
dir.cleanup();
});
it('should generate a JSONSchema model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('JSONSchema', models, dir.path, true);
await Commands.generate('JSONSchema', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.above(0);
dir.cleanup();
});
it('should generate a XMLSchema model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('XMLSchema', models, dir.path, true);
await Commands.generate('XMLSchema', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.above(0);
dir.cleanup();
});
it('should not generate an unknown model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.generate('BLAH', models, dir.path, true);
await Commands.generate('BLAH', null, models, dir.path);
fs.readdirSync(dir.path).length.should.be.equal(0);
dir.cleanup();
});
Expand All @@ -97,7 +98,7 @@ describe('cicero-cli', () => {
describe('#getExternalModels', () => {
it('should save external dependencies', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.getExternalModels(models, dir.path);
await Commands.getExternalModels(null, models, dir.path);
fs.readdirSync(dir.path).should.eql([
'@models.accordproject.org.cicero.contract.cto',
'dom.cto',
Expand All @@ -108,7 +109,7 @@ describe('cicero-cli', () => {

it('should save external dependencies for an external model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
await Commands.getExternalModels(['https://models.accordproject.org/patents/patent.cto'], dir.path);
await Commands.getExternalModels(null,['https://models.accordproject.org/patents/patent.cto'], dir.path);
fs.readdirSync(dir.path).should.eql([
"@models.accordproject.org.address.cto",
"@models.accordproject.org.geo.cto",
Expand All @@ -122,5 +123,14 @@ describe('cicero-cli', () => {
]);
dir.cleanup();
});

it('should fail saving external dependencies for an external model but with the wrong system model', async () => {
const dir = await tmp.dir({ unsafeCleanup: true});
try {
await Commands.getExternalModels(hlModel,['https://models.accordproject.org/patents/patent.cto'], dir.path);
} catch (err) {
err.message.should.contain('Relationship transactionInvoked must be to an asset or participant, but is to org.hyperledger.composer.system.Transaction');
}
});
});
});
Loading