Skip to content

Commit

Permalink
Add support for model npm modules with dependencies (hyperledger-arch…
Browse files Browse the repository at this point in the history
…ives#631)

Add support for model npm modules with dependencies 

* Fixed bug generating sample data for an asset containing a concept

* Follow dependencies for model npm packages

* Fixed to ignore test cto files

* Added JSDoc
  • Loading branch information
dselman authored Apr 5, 2017
1 parent aa893fb commit d113bf2
Show file tree
Hide file tree
Showing 14 changed files with 658 additions and 68 deletions.
83 changes: 19 additions & 64 deletions packages/composer-common/lib/businessnetworkdefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const Factory = require('./factory');
const Serializer = require('./serializer');
const ScriptManager = require('./scriptmanager');
const BusinessNetworkMetadata = require('./businessnetworkmetadata');
const ModelCollector = require('./tools/modelcollector');
const JSZip = require('jszip');
const semver = require('semver');
const fs = require('fs');
Expand Down Expand Up @@ -308,6 +309,10 @@ class BusinessNetworkDefinition {
* passes the options.dependencyGlob pattern.
* </p>
* <p>
* If the network depends on an npm module its dependencies (transitive closure)
* will also be scanned for model (CTO) files.
* </p>
* <p>
* The directory may optionally contain a README.md file which is accessible from the
* BusinessNetworkMetadata.getREADME method.
* </p>
Expand All @@ -334,23 +339,28 @@ class BusinessNetworkDefinition {
*/
static fromDirectory(path, options) {

if(!options) {
options = {};
}
const method = 'fromDirectory';

if(!options.dependencyGlob) {
options.dependencyGlob = '**';
// collect all the CTO files, following the npm dependencies
const modelCollector = new ModelCollector();
const models = modelCollector.collect(path, options);
const modelFiles = [];
const modelFileNames = [];

for(let modelInfo of models) {
LOG.debug(method, 'Adding model', JSON.stringify(modelInfo));
modelFiles.push(modelInfo.contents);
modelFileNames.push(modelInfo.module + '/' + modelInfo.relativePath + '/' + modelInfo.file);
}

if(!options.modelFileGlob) {
options.modelFileGlob = '**/models/**/*.cto';
if(!options) {
options = {};
}

if(!options.scriptGlob) {
options.scriptGlob = '**/lib/**/*.js';
}

const method = 'fromDirectory';
LOG.entry(method, path);

// grab the README.md
Expand All @@ -374,50 +384,11 @@ class BusinessNetworkDefinition {

// parse the package.json
let jsonObject = JSON.parse(packageJsonContents);
let packageName = jsonObject.name;

// create the business network definition
const businessNetwork = new BusinessNetworkDefinition(null, null, jsonObject, readmeContents);
const modelFiles = [];
const modelFileNames = [];

// process each module dependency
// filtering using a glob on the module dependency name
if(jsonObject.dependencies) {
LOG.debug(method, 'All dependencies', Object.keys(jsonObject.dependencies).toString());
const dependencies = Object.keys(jsonObject.dependencies).filter(minimatch.filter(options.dependencyGlob));
LOG.debug(method, 'Matched dependencies', dependencies);

for( let dep of dependencies) {
// find all the *.cto files under the npm install dependency path
let dependencyPath = fsPath.resolve(path, 'node_modules', dep);
LOG.debug(method, 'Checking dependency path', dependencyPath);
if (!fs.existsSync(dependencyPath)) {
// need to check to see if this is in a peer directory as well
//
LOG.debug(method,'trying different path '+path.replace(packageName,''));
dependencyPath = fsPath.resolve(path.replace(packageName,''),dep);
if(!fs.existsSync(dependencyPath)){
throw new Error('npm dependency path ' + dependencyPath + ' does not exist. Did you run npm install?');
}
}

BusinessNetworkDefinition.processDirectory(dependencyPath, {
accepts: function(file) {
return minimatch(file, options.modelFileGlob);
},
acceptsDir: function(dir) {
return true;
},
process: function(path,contents) {
modelFiles.push(contents);
LOG.debug(method, 'Found model file', path);
}
});
}
}

// define a help function that will filter out files
// define a helper function that will filter out files
// that are inside a node_modules directory under the path
// we are processing
const isFileInNodeModuleDir = function(file) {
Expand All @@ -432,22 +403,6 @@ class BusinessNetworkDefinition {
return result;
};

// find CTO files outside the npm install directory
//
BusinessNetworkDefinition.processDirectory(path, {
accepts: function(file) {
return isFileInNodeModuleDir(file) === false && minimatch(file, options.modelFileGlob);
},
acceptsDir: function(dir) {
return !isFileInNodeModuleDir(dir);
},
process: function(path,contents) {
modelFiles.push(contents);
modelFileNames.push(path);
LOG.debug(method, 'Found model file', path);
}
});

businessNetwork.getModelManager().addModelFiles(modelFiles,modelFileNames);
LOG.debug(method, 'Added model files', modelFiles.length);

Expand Down
190 changes: 190 additions & 0 deletions packages/composer-common/lib/tools/modelcollector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* 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 fsPath = require('path');
const Logger = require('../log/logger');
const minimatch = require('minimatch');

const LOG = Logger.getLog('ModelCollector');
const ENCODING = 'utf8';

/**
* ModelCollector collects all the CTO files for an npm module
* along with all its transitive dependencies declared in package.json
*
* @private
* @class
* @memberof module:composer-common
*/
class ModelCollector {

/**
* Create a ModelCollector. Call the collect method to aggregate information
* about the model files in memory.
*/
constructor() {
}

/**
* Collect
*
* @param {string} path - the path to process
* @param {Object} [options] - an optional set of options to configure the instance.
* @param {Object} [options.dependencyGlob] - specify the glob pattern used to match
* the npm dependencies to process. Defaults to **
* @param {boolean} [options.modelFileGlob] - specify the glob pattern used to match
* the model files to include. Defaults to **\/models/**\/*.cto
* @param {boolean} [options.scriptGlob] - specify the glob pattern used to match
* the script files to include. Defaults to **\/lib/**\/*.js
* @return {Object[]} an array of objects that describe the models
* @private
*/
collect(path, options) {
const method = 'collect';

if (!options) {
options = {};
}

if (!options.dependencyGlob) {
options.dependencyGlob = '**';
}

if (!options.modelFileGlob) {
options.modelFileGlob = '**/models/**/*.cto';
}

// grab the package.json
let packageJsonContents = fs.readFileSync(fsPath.resolve(path, 'package.json'), ENCODING);

if (!packageJsonContents) {
throw new Error('Failed to find package.json');
}

LOG.debug(method, 'Loaded package.json', packageJsonContents);

// parse the package.json
let jsonObject = JSON.parse(packageJsonContents);
let packageName = jsonObject.name;
const modelFiles = [];

// grab all the model files that are beneath the current directory
ModelCollector.processDirectory(path, {
accepts: function (file) {
// we use this dirty hack to prevent
// finding the test CTO files that are part of composer-common
return (minimatch(file, options.modelFileGlob) &&
file.indexOf('/node_modules/composer-common/test/') <0);
},
acceptsDir: function (dir) {
return true;
},
process: function (pathToContents, contents) {
modelFiles.push({
'module': packageName,
'file': fsPath.basename(pathToContents),
'path': pathToContents,
'relativePath' : fsPath.relative(path,pathToContents),
'contents': contents
});
LOG.debug(method, 'Found model file', pathToContents);
}
});

// we then process each of the dependencies
if (jsonObject.dependencies) {
LOG.debug(method, 'All dependencies', Object.keys(jsonObject.dependencies).toString());
const dependencies = Object.keys(jsonObject.dependencies).filter(minimatch.filter(options.dependencyGlob));
LOG.debug(method, 'Matched dependencies', dependencies);

for (let dep of dependencies) {
let dependencyPath = fsPath.resolve(path, 'node_modules', dep);
LOG.debug(method, 'Checking dependency path', dependencyPath);
if (!fs.existsSync(dependencyPath)) {
// need to check to see if this is in a peer directory as well
LOG.debug(method, 'trying different path ' + path.replace(packageName, ''));
dependencyPath = fsPath.resolve(path.replace(packageName, ''), dep);
if (!fs.existsSync(dependencyPath)) {
throw new Error('npm dependency path ' + dependencyPath + ' does not exist. Did you run npm install?');
}
}

// collect everything in the dependency path
modelFiles.concat(this.collect(dependencyPath, options));
}
}

return modelFiles;
}

/**
* @param {String} path - the path to process
* @param {Object} fileProcessor - the file processor. It must have
* accept and process methods.
* @private
*/
static processDirectory(path, fileProcessor) {
const items = ModelCollector.walkSync(path, [], fileProcessor);
items.sort();
LOG.debug('processDirectory', 'Path ' + path, items);
items.forEach((item) => {
ModelCollector.processFile(item, fileProcessor);
});
}

/**
* @param {String} file - the file to process
* @param {Object} fileProcessor - the file processor. It must have
* accepts and process methods.
* @private
*/
static processFile(file, fileProcessor) {

if (fileProcessor.accepts(file)) {
LOG.debug('processFile', 'FileProcessor accepted', file);
let fileContents = fs.readFileSync(file, ENCODING);
fileProcessor.process(file, fileContents);
} else {
LOG.debug('processFile', 'FileProcessor rejected', file);
}
}

/**
* @param {String} dir - the dir to walk
* @param {Object[]} filelist - input files
* @param {Object} fileProcessor - the file processor. It must have
* accepts and process methods.
* @return {Object[]} filelist - output files
* @private
*/
static walkSync(dir, filelist, fileProcessor) {
let files = fs.readdirSync(dir);
files.forEach(function (file) {
let nestedPath = fsPath.join(dir, file);
if (fs.lstatSync(nestedPath).isDirectory()) {
if (fileProcessor.acceptsDir(nestedPath)) {
filelist = ModelCollector.walkSync(nestedPath, filelist, fileProcessor);
}
} else {
filelist.push(nestedPath);
}
});
return filelist;
}
}

module.exports = ModelCollector;
8 changes: 5 additions & 3 deletions packages/composer-common/test/businessnetworkdefinition.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,12 +123,14 @@ describe('BusinessNetworkDefinition', () => {
businessNetwork.getName().should.equal('test-npm-archive');
businessNetwork.getVersion().should.equal('0.0.1');
businessNetwork.getDescription().should.equal('A test business network using npm model dependencies.');
Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(2);
// should load 1 model from the network, 1 from animaltracking and
// 2 from the dependency on composer.business (composer.business + composer.base)
Object.keys(businessNetwork.modelManager.modelFiles).should.have.length(4);
Object.keys(businessNetwork.scriptManager.scripts).should.have.length(2);

const intro = businessNetwork.getIntrospector();
intro.getClassDeclarations().length.should.equal(13);
businessNetwork.getModelManager().getModelFiles().length.should.equal(2);
intro.getClassDeclarations().length.should.equal(22);
businessNetwork.getModelManager().getModelFiles().length.should.equal(4);
const sm = businessNetwork.getScriptManager();
sm.getScripts().length.should.equal(2);
});
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d113bf2

Please sign in to comment.