diff --git a/packages/concerto-cli/README.md b/packages/concerto-cli/README.md index 54d95cb35b..57d802e594 100644 --- a/packages/concerto-cli/README.md +++ b/packages/concerto-cli/README.md @@ -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: "."] ``` @@ -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"] @@ -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: "./"] ``` diff --git a/packages/concerto-cli/index.js b/packages/concerto-cli/index.js index b6701ec95e..db12069b2b 100755 --- a/packages/concerto-cli/index.js +++ b/packages/concerto-cli/index.js @@ -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', @@ -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); }) @@ -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', @@ -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); }) @@ -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', @@ -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); }); @@ -103,4 +118,4 @@ require('yargs') default: false }) .help() - .argv; \ No newline at end of file + .argv; diff --git a/packages/concerto-cli/lib/commands.js b/packages/concerto-cli/lib/commands.js index d94c62ef6e..0ec15a10ac 100644 --- a/packages/concerto-cli/lib/commands.js +++ b/packages/concerto-cli/lib/commands.js @@ -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; @@ -29,6 +30,17 @@ 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 @@ -36,38 +48,88 @@ const XmlSchemaVisitor = CodeGen.XmlSchemaVisitor; */ 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; @@ -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; @@ -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; \ No newline at end of file +module.exports = Commands; diff --git a/packages/concerto-cli/test/cli.js b/packages/concerto-cli/test/cli.js index 6925330308..70b0dc0bf6 100644 --- a/packages/concerto-cli/test/cli.js +++ b/packages/concerto-cli/test/cli.js @@ -27,6 +27,7 @@ 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'); @@ -34,13 +35,13 @@ describe('cicero-cli', () => { 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'); @@ -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(); }); @@ -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', @@ -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", @@ -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'); + } + }); }); }); \ No newline at end of file diff --git a/packages/concerto-cli/test/models/org.hyperledger.composer.system.cto b/packages/concerto-cli/test/models/org.hyperledger.composer.system.cto new file mode 100644 index 0000000000..30ebb993f9 --- /dev/null +++ b/packages/concerto-cli/test/models/org.hyperledger.composer.system.cto @@ -0,0 +1,311 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +namespace org.hyperledger.composer.system + +/** + * Abstract system asset that all assets extend. + * Has no properties, and is soley used as a basis to model other assets. + */ +@docs('asset.md') +abstract asset Asset { } + +/** + * Abstract system participant that all participants extend. + * Has no properties, and is soley used as a basis to model other assets. + */ +@docs('participant.md') +abstract participant Participant { } + +/** + * Abstract transaction that all transactions, including system ones, extend. + * + * Has two properties that are used set and are accessible in all transactions. + * + * + * @param {String} transactionId Identifier for this transaction + */ +@docs('transaction.md') +abstract transaction Transaction identified by transactionId { + o String transactionId +} + +/** + * Abstract event that all events, including system ones, extend. + * + * Has two properties that are used set and are accessible in all transactions. + * + * @param {String} eventId Identifier for this event + */ +@docs('event.md') +abstract event Event identified by eventId { + o String eventId +} + +/** + * Abstract Registry asset, that is used as the basis for all types of registries. + * + * @param {String} registryId identity + * @param {String} name Name of the registry + * @param {String} type type of the registry + * @param {Boolean} system Is this a system registry? + */ +@docs('registry.md') +abstract asset Registry identified by registryId { + o String registryId + o String name + o String type + o Boolean system +} + +/** + * AssetRegistry extends the Registry to define the type that of all registries + * that are primarily intended for storing Assets. + * + */ +@docs('assetRegistry.md') +asset AssetRegistry extends Registry { } + +/** + * ParticipantRegistry extends the Registry to define the type that of all registries + * that are primarily intended for storing Participants + */ +@docs('participantRegistry.md') +asset ParticipantRegistry extends Registry { } + +/** + * TransactionRegistry extends the Registry to define the type that of all registries + * that are primarily intended for storing Transactions + */ +@docs('transactionRegistry.md') +asset TransactionRegistry extends Registry { } + + +/** + * Asset to represent the idea of a Business Network. + * All actions will require participants to have access to this Asset. Failure to have at least *READ* access + * will mean that participants are unable to access the network. + * + * Participants who are authorized administrators, can be granted *UPDATE* and/or *DELETE* permissions + + * @param {String} networkId of the business network + */ +@docs('network.md') +asset Network identified by networkId { + o String networkId + o String runtimeVersion +} + +/** + * A predefined participant that can be given the authority to adiminister the Business Network + * + * @param {String} participantId Identifier fields of the participant + */ +@docs('networkAdmin.md') +participant NetworkAdmin identified by participantId { + o String participantId +} + +// ----------------------------------------------------------------------------- +// Historian + +/** + * Asset to represent each historian registry entry + * + * @param {String} transactionId Using the transaction id as the uuid + * @param {Transaction} transactionInvoked Relationship to transaction + * @param {Participant} participantInvoking Participant who invoked this transaction + * @param {Identity} identityUsed The identity that was used by the participant + * @param {Event[]} eventsEmitted The events that were emitted by this transactionId + * @param {DateTime} transactionTimestamp Use the transaction's timestamp + */ +@docs('historian.md') +@docsuri('Composer Documentation','../business-network/historian') +asset HistorianRecord identified by transactionId { + o String transactionId + o String transactionType + --> Transaction transactionInvoked + --> Participant participantInvoking optional + --> Identity identityUsed optional + o Event[] eventsEmitted optional + o DateTime transactionTimestamp +} + +// ----------------------------------------------------------------------------- +// System transactions that act on Registries of any type +/** + * An abstract definition of a transaction that affects a registry in some way + * @param {Registry} targetRegistry Registry that will be manipulated + */ +@docs('registryTransaction.md') +abstract transaction RegistryTransaction { + --> Registry targetRegistry +} + +/** + * An abstract definition of a transaction that affects assets in a registry in some way + * @param {Asset[]} resources Resources that will be manipulated + */ +@docs('assetTransaction.md') +abstract transaction AssetTransaction extends RegistryTransaction { + o Asset[] resources +} + +/** + * An abstract definition of a transaction that affects participants in a registry in some way + * @param {Participant[]} resources participants that will be manipulated + */ +@docs('participantTransaction.md') +abstract transaction ParticipantTransaction extends RegistryTransaction { + o Participant[] resources +} + +/** + * Transaction that will add asset(s) to a registry + */ +transaction AddAsset extends AssetTransaction { } + +/** + * Transaction that will update asset(s) in a registry + */ +transaction UpdateAsset extends AssetTransaction { } + +/** + * Transaction that will remove asset(s) from a registry + * @param {String[]} resourceIds Identifiers of the assets to remove + */ +transaction RemoveAsset extends AssetTransaction { + o String[] resourceIds +} + +/** + * Transaction that will add participants(s) to a registry + */ +transaction AddParticipant extends ParticipantTransaction { } + +/** + * Transaction that will update participants(s) in a registry + */ +transaction UpdateParticipant extends ParticipantTransaction { } + +/** + * Transaction that will remove participants(s) from a registry + * @param {String[]} resourceIds Identifiers of the participants to remove + */ +transaction RemoveParticipant extends ParticipantTransaction { + o String[] resourceIds +} + + +// ----------------------------------------------------------------------------- +// Identity + +/** The valid states of an identity + * @enum {ISSUED} identity has been issued + * @enum {BOUND} identity has been bound to a participant + * @enum {ACTIVATED} identity has been activated + * @enum {REVOKED} identity has been revoked + */ +@docs('identityState.md') +enum IdentityState { + o ISSUED + o BOUND + o ACTIVATED + o REVOKED +} + +/** + * Asset representing the idea of an Identity + * + * @param {String} identityId Unique Identifiers + * @param {String} name Name given to this identity + * @param {String} issuer The issuer + * @param {String} certificate The certificate + * @param {IdentityState} state State the identity is in + * @param {Participant} participant Associated participant + */ +@docs('identity.md') +asset Identity identified by identityId { + o String identityId + o String name + o String issuer + o String certificate + o IdentityState state + --> Participant participant +} + +/** + * Transaction that will issue the identity + * @param {Participant} participant to issue the identity to + * @param {String} identityName name to use for this identity + */ +@docs('issueIdentity.md') +transaction IssueIdentity { + --> Participant participant + o String identityName +} + +/** + * Transaction that will bind the identity + * @param {Participant} participant to issue bind identity to + * @param {String} certificate to use + */ +@docs('bindIdentity.md') +transaction BindIdentity { + --> Participant participant + o String certificate +} + +/** + * Transaction that will activate the current the identity + */ +@docs('activateIdentity.md') +transaction ActivateCurrentIdentity { } + +/** + * Transaction that will revoke the identity + * @param {Identity} identity to revoke + */ +@docs('revokeIdentity.md') +transaction RevokeIdentity { + --> Identity identity +} + +/** + * Transaction that will Start a business network + * @param {Transaction[]} [bootstrapTransactions] optional transactions to use to bootstrap the network + * @param {String} [logLevel] Log level to use optionally + * + */ +@docs('startBusinessNetwork.md') +transaction StartBusinessNetwork { + o String logLevel optional + o Transaction[] bootstrapTransactions optional +} + +/** + * Transaction that will Reset a business network. This removes all the data but leaves the structure of the business network intact + */ +@docs('resetBusinessNetwork.md') +transaction ResetBusinessNetwork { + +} + +/** + * Sets the log level of the Business Network runtime to that specified + * @param {String} newLogLevel a valid debug string + */ +@docs('setLogLevel.md') +transaction SetLogLevel { + o String newLogLevel +}