UNPKG

@accordproject/cicero-cli

Version:
770 lines (693 loc) • 30.1 kB
/* * 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. */ 'use strict'; const fs = require('fs'); const path = require('path'); const mkdirp = require('mkdirp'); const Logger = require('@accordproject/concerto-util').Logger; const FileWriter = require('@accordproject/concerto-util').FileWriter; const Template = require('@accordproject/cicero-core').Template; const Clause = require('@accordproject/cicero-core').Clause; const Engine = require('@accordproject/cicero-engine').Engine; const CodeGen = require('@accordproject/cicero-tools').CodeGen; const GoLangVisitor = CodeGen.GoLangVisitor; const JavaVisitor = CodeGen.JavaVisitor; const CordaVisitor = CodeGen.CordaVisitor; const JSONSchemaVisitor = CodeGen.JSONSchemaVisitor; const PlantUMLVisitor = CodeGen.PlantUMLVisitor; const TypescriptVisitor = CodeGen.TypescriptVisitor; const defaultSample = 'text/sample.md'; const defaultData = 'data.json'; const defaultParams = 'params.json'; const defaultState = 'state.json'; /** * Utility class that implements the commands exposed by the Cicero CLI. * @class * @memberof module:cicero-cli */ class Commands { /** * Whether the template path is to a file (template archive) * @param {string} templatePath - path to the template directory or archive * @return {boolean} true if the path is to a file, false otherwise */ static isTemplateArchive(templatePath) { return fs.lstatSync(templatePath).isFile(); } /** * Return a promise to a template from either a directory or an archive file * @param {string} templatePath - path to the template directory or archive * @param {Object} [options] - an optional set of options * @return {Promise<Template>} a Promise to the instantiated template */ static loadTemplate(templatePath, options) { if (Commands.isTemplateArchive(templatePath)) { const buffer = fs.readFileSync(templatePath); return Template.fromArchive(buffer, options); } else { return Template.fromDirectory(templatePath, options); } } /** * Common default params before we create an archive using a template * * @param {object} argv - the inbound argument values object * @returns {object} a modfied argument object */ static validateCommonArgs(argv) { // the user typed 'cicero [command] [template]' if(argv._.length === 2){ argv.template = argv._[1]; } if(!argv.template){ Logger.info('Using current directory as template folder'); argv.template = '.'; } argv.template = path.resolve(argv.template); if (!Commands.isTemplateArchive(argv.template)) { const packageJsonExists = fs.existsSync(path.resolve(argv.template,'package.json')); let isAPTemplate = false; if(packageJsonExists){ let packageJsonContents = JSON.parse(fs.readFileSync(path.resolve(argv.template,'package.json')),'utf8'); isAPTemplate = packageJsonContents.accordproject; } if(!packageJsonExists || !isAPTemplate){ throw new Error(`${argv.template} is not a valid cicero template. Make sure that package.json exists and that it has a cicero entry.`); } } return argv; } /** * Check data params used by initialize, invoke and trigger commands. * Data must be provided to these commands either via a "sample.md" file or via a "data.json" file. * Function checks if params exist, if not then attempts to locate default "./text/sample.md" or "./data.json" files. * If neither found then throws exception. * * @param {object} argv - the inbound argument values object * @returns {object} a modfied argument object */ static validateDataArgs(argv) { if (argv.sample) { if (!fs.existsSync(argv.sample)) { throw new Error(`A sample file was specified as "${argv.sample}" but does not exist at this location.`); } } else if (argv.data) { if (!fs.existsSync(argv.data)) { throw new Error(`A data file was specified as "${argv.data}" but does not exist at this location.`); } } else { if (fs.existsSync(defaultSample)) { argv.sample = defaultSample; Logger.warn('A data file was not provided. Loading data from default "/text/sample.md" file.'); } else { throw new Error('A data file was not provided. Try the --sample flag to provide a data file in markdown format or the --data flag to provide a data file in JSON format.'); } } return argv; } /** * Set a default for a file argument * * @param {object} argv - the inbound argument values object * @param {string} argName - the argument name * @param {string} argDefaultName - the argument default name * @param {Function} argDefaultFun - how to compute the argument default * @param {object} argDefaultValue - an optional default value if all else fails * @returns {object} a modified argument object */ static setDefaultFileArg(argv, argName, argDefaultName, argDefaultFun) { if(!argv[argName]){ Logger.info(`Loading a default ${argDefaultName} file.`); argv[argName] = argDefaultFun(argv, argDefaultName); } let argExists = true; if (Array.isArray(argv[argName])) { // All files should exist for (let i = 0; i < argv[argName].length; i++) { if (fs.existsSync(argv[argName][i]) && argExists) { argExists = true; } else { argExists = false; } } } else { // This file should exist argExists = fs.existsSync(argv[argName]); } if (!argExists){ throw new Error(`A ${argDefaultName} file is required. Try the --${argName} flag or create a ${argDefaultName} in your template.`); } else { return argv; } } /** * Set default params before we parse a sample text using a template * * @param {object} argv - the inbound argument values object * @returns {object} a modfied argument object */ static validateParseArgs(argv) { argv = Commands.validateCommonArgs(argv); argv = Commands.setDefaultFileArg(argv, 'sample', 'text/sample.md', ((argv, argDefaultName) => { return path.resolve(argv.template,argDefaultName); })); if(argv.verbose) { Logger.info(`parse sample ${argv.sample} using a template ${argv.template}`); } return argv; } /** * Parse a sample text using a template * * @param {string} templatePath - path to the template directory or archive * @param {string} samplePath - path to the contract text * @param {string} outputPath - to an output file * @param {string} [currentTime] - the definition of 'now', defaults to current time * @param {number} [utcOffset] - UTC Offset for this execution, defaults to local offset * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of parsing */ static parse(templatePath, samplePath, outputPath, currentTime, utcOffset, options) { let clause; const sampleText = fs.readFileSync(samplePath, 'utf8'); return Commands.loadTemplate(templatePath, options) .then((template) => { clause = new Clause(template); clause.parse(sampleText, currentTime, utcOffset, samplePath); if (outputPath) { Logger.info('Creating file: ' + outputPath); fs.writeFileSync(outputPath, JSON.stringify(clause.getData(),null,2)); } return clause.getData(); }) .catch((err) => { Logger.error(err.message); }); } /** * Set default params before we draft a sample text using a template * * @param {object} argv - the inbound argument values object * @returns {object} a modfied argument object */ static validateDraftArgs(argv) { argv = Commands.validateCommonArgs(argv); argv = Commands.setDefaultFileArg(argv, 'data', defaultData, ((argv, argDefaultName) => { return path.resolve(argv.template,argDefaultName); })); if(argv.verbose) { Logger.info(`draft text from data ${argv.data} using a template ${argv.template}`); } return argv; } /** * Draft a contract text from JSON data * * @param {string} templatePath - path to the template directory or archive * @param {string} dataPath - path to the JSON data * @param {string} outputPath - to the contract file * @param {string} [currentTime] - the definition of 'now', defaults to current time * @param {number} [utcOffset] - UTC Offset for this execution, defaults to local offset * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of parsing */ static draft(templatePath, dataPath, outputPath, currentTime, utcOffset, options) { let clause; const dataJson = JSON.parse(fs.readFileSync(dataPath, 'utf8')); return Commands.loadTemplate(templatePath, options) .then(async function (template) { clause = new Clause(template); clause.setData(dataJson); const drafted = clause.draft(options, currentTime, utcOffset); if (outputPath) { Logger.info('Creating file: ' + outputPath); let text; if (options && options.format && (options.format === 'slate' || options.format === 'ciceromark_parsed')) { text = JSON.stringify(drafted); } else { text = drafted; } fs.writeFileSync(outputPath, text); } return drafted; }) .catch((err) => { Logger.error(err.message); }); } /** * Set default params before we parse a sample text using a template * * @param {object} argv - the inbound argument values object * @returns {object} a modfied argument object */ static validateNormalizeArgs(argv) { argv = Commands.validateParseArgs(argv); if (argv.overwrite) { if (argv.output) { throw new Error('Cannot use both --overwrite and --output'); } else { argv.output = argv.sample; } } return argv; } /** * Parse and re-create a sample contract * * @param {string} templatePath - path to the template directory or archive * @param {string} samplePath - to the sample file * @param {boolean} overwrite - true if overwriting the sample * @param {string} outputPath - to the contract file * @param {string} [currentTime] - the definition of 'now', defaults to current time * @param {number} [utcOffset] - UTC Offset for this execution, defaults to local offset * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of parsing */ static normalize(templatePath, samplePath, overwrite, outputPath, currentTime, utcOffset, options) { let clause; const sampleText = fs.readFileSync(samplePath, 'utf8'); return Commands.loadTemplate(templatePath, options) .then(async function (template) { clause = new Clause(template); clause.parse(sampleText, currentTime, utcOffset, samplePath); if (outputPath) { Logger.info('Creating file: ' + outputPath); fs.writeFileSync(outputPath, JSON.stringify(clause.getData(),null,2)); } const text = clause.draft(options, currentTime, utcOffset); if (outputPath) { Logger.info('Creating file: ' + outputPath); fs.writeFileSync(outputPath, text); } return text; }) .catch((err) => { Logger.error(err.message); }); } /** * Set default params before we trigger a template * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateTriggerArgs(argv) { argv = Commands.validateCommonArgs(argv); argv = Commands.validateDataArgs(argv); argv = Commands.setDefaultFileArg(argv, 'request', 'request.json', ((argv, argDefaultName) => { return [path.resolve(argv.template,argDefaultName)]; })); if (argv.verbose) { if (argv.sample) { Logger.info( `trigger: \n - Sample: ${argv.sample} \n - Template: ${argv.template} \n - Request: ${argv.request} \n - State: ${argv.state}` ); } else { Logger.info( `trigger: \n - Data: ${argv.data} \n - Template: ${argv.template} \n - Request: ${argv.request} \n - State: ${argv.state}` ); } } return argv; } /** * Trigger a sample text or data json using a template * * @param {string} templatePath - path to the template directory or archive * @param {string} samplePath - to the sample file * @param {string} dataPath - to the data file * @param {string[]} requestsPath - to the array of request files * @param {string} statePath - to the state file * @param {string} [currentTime] - the definition of 'now', defaults to current time * @param {number} [utcOffset] - UTC Offset for this execution, defaults to local offset * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of execution */ static trigger(templatePath, samplePath, dataPath, requestsPath, statePath, currentTime, utcOffset, options) { let clause; let sampleText; let dataJson; if (samplePath) { sampleText = fs.readFileSync(samplePath, 'utf8'); } else { dataJson = JSON.parse(fs.readFileSync(dataPath, 'utf8')); } let requestsJson = []; for (let i = 0; i < requestsPath.length; i++) { requestsJson.push(JSON.parse(fs.readFileSync(requestsPath[i], 'utf8'))); } const engine = new Engine(); return Commands.loadTemplate(templatePath, options) .then(async (template) => { // Initialize clause clause = new Clause(template); if (sampleText) { clause.parse(sampleText, currentTime, utcOffset); } else { clause.setData(dataJson); } let stateJson; if(!fs.existsSync(statePath)) { Logger.warn('A state file was not provided, initializing state. Try the --state flag or create a state.json in the root folder of your template.'); const initResult = await engine.init(clause, currentTime, utcOffset); stateJson = initResult.state; } else { stateJson = JSON.parse(fs.readFileSync(statePath, 'utf8')); } // First execution to get the initial response const firstRequest = requestsJson[0]; const initResponse = engine.trigger(clause, firstRequest, stateJson, currentTime, utcOffset); // Get all the other requests and chain execution through Promise.reduce() const otherRequests = requestsJson.slice(1, requestsJson.length); return otherRequests.reduce((promise,requestJson) => { return promise.then((result) => { return engine.trigger(clause, requestJson, result.state, currentTime, utcOffset); }); }, initResponse); }) .catch((err) => { Logger.error(err.message); }); } /** * Set default params before we invoke a clause * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateInvokeArgs(argv) { argv = Commands.validateCommonArgs(argv); argv = Commands.validateDataArgs(argv); if (!argv.clauseName) { throw new Error('No clause name provided. Try the --clauseName flag to provide a clause to be invoked.'); } if (argv.params) { if (!fs.existsSync(argv.params)) { throw new Error(`A params file was specified as "${argv.params}" but does not exist at this location.`); } } else { argv.params = defaultParams; Logger.warn(`A params file was not provided. Loading params from default "${defaultParams}" file.`); } if (argv.state) { if (!fs.existsSync(argv.state)) { throw new Error(`A state file was specified as "${argv.state}" but does not exist at this location.`); } } else { argv.state = defaultState; Logger.warn(`A state file was not provided. Loading state from default "${defaultState}" file.`); } if(argv.verbose) { if (argv.sample) { Logger.info( `invoke: \n - Sample: ${argv.sample} \n - Template: ${argv.template} \n Clause: ${argv.clauseName} \n Params: ${argv.paramsPath}` ); } else { Logger.info( `invoke: \n - Data: ${argv.data} \n - Template: ${argv.template} \n Clause: ${argv.clauseName} \n Params: ${argv.paramsPath}` ); } } return argv; } /** * Invoke a sample text using a template * * @param {string} templatePath - path to the template directory or archive * @param {string} samplePath - to the sample file * @param {string} dataPath - to the data file * @param {string} clauseName the name of the clause to invoke * @param {object} paramsPath the parameters for the clause * @param {string} statePath - to the state file * @param {string} [currentTime] - the definition of 'now', defaults to current time * @param {number} [utcOffset] - UTC Offset for this execution, defaults to local offset * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of execution */ static invoke(templatePath, samplePath, dataPath, clauseName, paramsPath, statePath, currentTime, utcOffset, options) { let clause; let sampleText; let dataJson; if (samplePath) { sampleText = fs.readFileSync(samplePath, 'utf8'); } else { dataJson = JSON.parse(fs.readFileSync(dataPath, 'utf8')); } const paramsJson = JSON.parse(fs.readFileSync(paramsPath, 'utf8')); const engine = new Engine(); return Commands.loadTemplate(templatePath, options) .then(async (template) => { // Initialize clause clause = new Clause(template); if (sampleText) { clause.parse(sampleText, currentTime, utcOffset); } else { clause.setData(dataJson); } let stateJson; if(!fs.existsSync(statePath)) { Logger.warn('A state file was not provided, initializing state. Try the --state flag or create a state.json in the root folder of your template.'); const initResult = await engine.init(clause, currentTime, utcOffset); stateJson = initResult.state; } else { stateJson = JSON.parse(fs.readFileSync(statePath, 'utf8')); } return engine.invoke(clause, clauseName, paramsJson, stateJson, currentTime, utcOffset); }) .catch((err) => { Logger.error(err.message); }); } /** * Set default params before we initialize a template * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateInitializeArgs(argv) { argv = Commands.validateCommonArgs(argv); argv = Commands.validateDataArgs(argv); if(argv.verbose) { if (argv.sample) { Logger.info( `initialize: \n - Sample: ${argv.sample} \n - Template: ${argv.template} \n - Request: ${argv.request} \n - State: ${argv.state}` ); } else { Logger.info( `initialize: \n - Data: ${argv.data} \n - Template: ${argv.template} \n - Request: ${argv.request} \n - State: ${argv.state}` ); } } return argv; } /** * Initializes a sample text using a template * * @param {string} templatePath - path to the template directory or archive * @param {string} samplePath - to the sample file * @param {string} dataPath - to the data file * @param {object} paramsPath - the parameters for the initialization * @param {string} [currentTime] - the definition of 'now', defaults to current time * @param {number} [utcOffset] - UTC Offset for this execution, defaults to local offset * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of execution */ static initialize(templatePath, samplePath, dataPath, paramsPath, currentTime, utcOffset, options) { let clause; let sampleText; let dataJson; if (samplePath) { sampleText = fs.readFileSync(samplePath, 'utf8'); } else { dataJson = JSON.parse(fs.readFileSync(dataPath, 'utf8')); } const paramsJson = paramsPath ? JSON.parse(fs.readFileSync(paramsPath, 'utf8')) : {}; const engine = new Engine(); return Commands.loadTemplate(templatePath, options) .then((template) => { // Initialize clause clause = new Clause(template); if (sampleText) { clause.parse(sampleText, currentTime, utcOffset); } else { clause.setData(dataJson); } return engine.init(clause, currentTime, utcOffset, paramsJson); }) .catch((err) => { Logger.error(err.message); }); } /** * Set default params before we create an archive using a template * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateArchiveArgs(argv) { argv = Commands.validateCommonArgs(argv); if(!argv.target){ Logger.info('Using ergo as the default target for the archive.'); argv.target = 'ergo'; } return argv; } /** * Create an archive using a template * * @param {string} templatePath - path to the template directory or archive * @param {string} target - target language for the archive (should be either 'ergo' or 'cicero') * @param {string} outputPath - to the archive file * @param {Object} [options] - an optional set of options * @returns {object} Promise to the code creating an archive */ static archive(templatePath, target, outputPath, options) { return Commands.loadTemplate(templatePath, options) .then(async (template) => { let keystore = null; if (options.keystore) { const p12File = fs.readFileSync(options.keystore.path, { encoding: 'base64' }); const inputKeystore = { p12File: p12File, passphrase: options.keystore.passphrase }; keystore = inputKeystore; } const archive = await template.toArchive(target, {keystore}, options); let file; if (outputPath) { file = outputPath; } else { const templateName = template.getMetadata().getName(); const templateVersion = template.getMetadata().getVersion(); file = `${templateName}@${templateVersion}.cta`; } Logger.info('Creating archive: ' + file); fs.writeFileSync(file, archive); return true; }); } /** * Set default params before we verify signatures of template author/developer * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateVerifyArgs(argv) { argv = Commands.validateCommonArgs(argv); return argv; } /** * Verify the template developer/author's signatures * * @param {string} templatePath - path to the template directory or archive * @param {Object} [options] - an optional set of options * @returns {object} returns true if signature is valid else false */ static verify(templatePath, options) { return Commands.loadTemplate(templatePath, options) .then((template) => { return template.verifyTemplateSignature(); }); } /** * Set default params before we compile a template * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateCompileArgs(argv) { argv = Commands.validateCommonArgs(argv); if(argv.verbose) { Logger.info(`compile using a template ${argv.template}`); } return argv; } /** * Compile the template to a given target * * @param {string} templatePath - path to the template directory or archive * @param {string} target - the target format * @param {string} outputPath - the output directory * @param {Object} [options] - an optional set of options * @returns {object} Promise to the result of code generation */ static compile(templatePath, target, outputPath, options) { return Commands.loadTemplate(templatePath, options) .then((template) => { let visitor = null; switch(target) { case 'Go': visitor = new GoLangVisitor(); break; case 'PlantUML': visitor = new PlantUMLVisitor(); break; case 'Typescript': visitor = new TypescriptVisitor(); break; case 'Java': visitor = new JavaVisitor(); break; case 'Corda': visitor = new CordaVisitor(); break; case 'JSONSchema': visitor = new JSONSchemaVisitor(); break; default: throw new Error ('Unrecognized code generator: ' + target); } let parameters = {}; parameters.fileWriter = new FileWriter(outputPath); template.getModelManager().accept(visitor, parameters); }) .catch((err) => { Logger.error(err); }); } /** * Set default params before we download external dependencies * * @param {object} argv the inbound argument values object * @returns {object} a modfied argument object */ static validateGetArgs(argv) { argv = Commands.validateCommonArgs(argv); if (!argv.output) { if (Commands.isTemplateArchive(argv.template)) { argv.output = './model'; } else { argv.output = path.resolve(argv.template,'model'); } } return argv; } /** * Fetches all external for a set of models dependencies and * saves all the models to a target directory * * @param {string} templatePath the system model * @param {string} output the output directory * @return {string} message */ static async get(templatePath, output) { return Commands.loadTemplate(templatePath, {}) .then((template) => { const modelManager = template.getModelManager(); mkdirp.sync(output); modelManager.writeModelsToFileSystem(output); return `Loaded external models in '${output}'.`; }); } } module.exports = Commands;