Skip to content

Commit

Permalink
feat(cli): add new concerto version command (contributes to #465) (#467)
Browse files Browse the repository at this point in the history
Signed-off-by: Simon Stone <Simon.Stone@docusign.com>
  • Loading branch information
Simon Stone committed Jul 26, 2022
1 parent a08d54e commit 45f7690
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 0 deletions.
26 changes: 26 additions & 0 deletions packages/concerto-cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
'use strict';

const Logger = require('@accordproject/concerto-util').Logger;
const { glob } = require('glob');
const Commands = require('./lib/commands');

require('yargs')
Expand Down Expand Up @@ -223,6 +224,31 @@ require('yargs')
Logger.error(err.message);
});
})
.command('version <release>', 'modify the version of one or more model files', yargs => {
yargs.demandOption(['model'], 'Please provide Concerto model(s)');
yargs.option('model', {
alias: 'models',
describe: 'array of concerto model files',
type: 'string',
array: true
});
}, argv => {
const modelFiles = argv.model.flatMap(model => {
if (glob.hasMagic(model)) {
return glob.sync(model);
}
return model;
});
return Commands.version(argv.release, modelFiles)
.then((result) => {
if (result) {
Logger.info(result);
}
})
.catch((err) => {
Logger.error(err.message);
});
})
.option('verbose', {
alias: 'v',
default: false
Expand Down
110 changes: 110 additions & 0 deletions packages/concerto-cli/lib/commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
const fs = require('fs');
const path = require('path');
const mkdirp = require('mkdirp');
const semver = require('semver');

const Logger = require('@accordproject/concerto-util').Logger;
const FileWriter = require('@accordproject/concerto-util').FileWriter;
Expand Down Expand Up @@ -271,6 +272,115 @@ class Commands {
}
return result;
}

/**
* Update the version of one or more model files.
*
* @param {string} release the release, major/minor/patch, or a semantic version
* @param {string[]} modelFiles the list of model file paths
*/
static async version(release, modelFiles) {
for (const modelFile of modelFiles) {
const resolvedModelFile = path.resolve(modelFile);
await Commands.versionModelFile(release, resolvedModelFile);
}
}

/**
* Update the version of a model file.
*
* @param {string} release the release, major/minor/patch, or a semantic version
* @param {string} modelFile the model file path
* @private
*/
static async versionModelFile(release, modelFile) {
const data = fs.readFileSync(modelFile, 'utf-8');
const isMetaModel = Commands.isJSON(data);
if (isMetaModel) {
await Commands.versionMetaModelFile(release, modelFile, data);
} else {
await Commands.versionCtoModelFile(release, modelFile, data);
}
}

/**
* Update the version of a metamodel (JSON) model file.
*
* @param {string} release the release, major/minor/patch, or a semantic version
* @param {string} modelFile the model file path
* @param {string} data the model file data
* @private
*/
static async versionMetaModelFile(release, modelFile, data) {
const metamodel = JSON.parse(data);
const currentNamespace = metamodel.namespace;
const [namespace, currentVersion] = currentNamespace.split('@');
const newVersion = Commands.calculateNewVersion(release, currentVersion);
metamodel.namespace = [namespace, newVersion].join('@');
const newData = JSON.stringify(metamodel, null, 2);
fs.writeFileSync(modelFile, newData, 'utf-8');
Logger.info(`Updated version of "${modelFile}" from "${currentVersion}" to "${newVersion}"`);
}

/**
* Update the version of a CTO model file.
*
* @param {string} release the release, major/minor/patch, or a semantic version
* @param {string} modelFile the model file path
* @param {string} data the model file data
* @private
*/
static async versionCtoModelFile(release, modelFile, data) {
const metamodel = Parser.parse(data, modelFile);
const currentNamespace = metamodel.namespace;
const [name, currentVersion] = currentNamespace.split('@');
const newVersion = Commands.calculateNewVersion(release, currentVersion);
const newNamespace = [name, newVersion].join('@');
const newData = data.replace(/(namespace\s+)(\S+)/, (match, keyword) => {
return `${keyword}${newNamespace}`;
});
// Sanity check.
Parser.parse(newData, modelFile);
fs.writeFileSync(modelFile, newData, 'utf-8');
Logger.info(`Updated version of "${modelFile}" from "${currentVersion}" to "${newVersion}"`);
}

/**
* Calculate the new version using the specified release.
*
* @param {string} release the release, major/minor/patch, or a semantic version
* @param {string} currentVersion the current version
* @returns {string} the new version
* @private
*/
static calculateNewVersion(release, currentVersion) {
if (semver.valid(release)) {
return release;
} else if (!semver.valid(currentVersion)) {
throw new Error(`invalid current version "${currentVersion}"`);
}
const newVersion = semver.inc(currentVersion, release);
if (!newVersion) {
throw new Error(`invalid release "${release}"`);
}
return newVersion;
}

/**
* Determine if data is valid JSON or not.
*
* @param {string} data the data
* @returns {boolean} true if JSON, false if not
* @private
*/
static isJSON(data) {
try {
JSON.parse(data);
return true;
} catch (error) {
return false;
}
}
}

module.exports = Commands;
2 changes: 2 additions & 0 deletions packages/concerto-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@
"@accordproject/concerto-metamodel": "2.3.0",
"@accordproject/concerto-tools": "2.3.0",
"@accordproject/concerto-util": "2.3.0",
"glob": "7.2.0",
"mkdirp": "1.0.4",
"semver": "7.3.5",
"yargs": "17.3.1"
},
"license-check-and-add-config": {
Expand Down
50 changes: 50 additions & 0 deletions packages/concerto-cli/test/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ chai.use(require('chai-things'));
chai.use(require('chai-as-promised'));

const Commands = require('../lib/commands');
const { Parser } = require('@accordproject/concerto-cto');

describe('cicero-cli', () => {
const models = [path.resolve(__dirname, 'models/dom.cto'),path.resolve(__dirname, 'models/money.cto')];
Expand Down Expand Up @@ -270,4 +271,53 @@ describe('cicero-cli', () => {
output.cleanup();
});
});

describe('#version', async () => {
let ctoPath;
let metamodelPath;

beforeEach(async () => {
const sourceCtoPath = path.resolve(__dirname, 'models', 'version.cto');
const sourceCto = fs.readFileSync(sourceCtoPath, 'utf-8');
ctoPath = (await tmp.file({ unsafeCleanup: true })).path;
fs.writeFileSync(ctoPath, sourceCto, 'utf-8');
metamodelPath = (await tmp.file({ unsafeCleanup: true })).path;
const metamodel = Parser.parse(sourceCto);
fs.writeFileSync(metamodelPath, JSON.stringify(metamodel, null, 2), 'utf-8');
});

const tests = [
{ name: 'patch', release: 'patch', expectedNamespace: 'org.accordproject.concerto.test@1.2.4' },
{ name: 'minor', release: 'minor', expectedNamespace: 'org.accordproject.concerto.test@1.3.0' },
{ name: 'major', release: 'major', expectedNamespace: 'org.accordproject.concerto.test@2.0.0' },
{ name: 'explicit', release: '4.5.6', expectedNamespace: 'org.accordproject.concerto.test@4.5.6' },
{ name: 'prerelease', release: '5.6.7-pr.3472381', expectedNamespace: 'org.accordproject.concerto.test@5.6.7-pr.3472381' }
];

tests.forEach(({ name, release, expectedNamespace }) => {

it(`should patch bump a cto file [${name}]`, async () => {
await Commands.version(release, [ctoPath]);
const cto = fs.readFileSync(ctoPath, 'utf-8');
const metamodel = Parser.parse(cto);
metamodel.namespace.should.equal(expectedNamespace);
});

it(`should patch bump a metamodel file [${name}]`, async () => {
await Commands.version(release, [metamodelPath]);
const metamodel = JSON.parse(fs.readFileSync(metamodelPath, 'utf-8'));
metamodel.namespace.should.equal(expectedNamespace);
});

});

it('should reject an invalid release', async () => {
await Commands.version('foobar', [ctoPath]).should.be.rejectedWith(/invalid release "foobar"/);
});

it('should reject an invalid version', async () => {
const sourceCtoPath = path.resolve(__dirname, 'models', 'badversion.cto');
await Commands.version('patch', [sourceCtoPath]).should.be.rejectedWith(/invalid current version "undefined"/);
});
});
});
19 changes: 19 additions & 0 deletions packages/concerto-cli/test/models/badversion.cto
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.accordproject.concerto.test@foobar

concept Foo {
o String bar
}
19 changes: 19 additions & 0 deletions packages/concerto-cli/test/models/version.cto
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.accordproject.concerto.test@1.2.3

concept Foo {
o String bar
}

0 comments on commit 45f7690

Please sign in to comment.