apiconnect-project
Version:
Configuration module IBM API Connect Developer Toolkit
246 lines (234 loc) • 8.31 kB
JavaScript
/********************************************************* {COPYRIGHT-TOP} ***
* Licensed Materials - Property of IBM
* 5725-Z22, 5725-Z63, 5725-U33, 5725-Z63
*
* (C) Copyright IBM Corporation 2016, 2017
*
* All Rights Reserved.
* US Government Users Restricted Rights - Use, duplication or disclosure
* restricted by GSA ADP Schedule Contract with IBM Corp.
********************************************************** {COPYRIGHT-END} **/
// Node module: apiconnect-project
;
var Promise = require('bluebird');
var _ = require('lodash');
var debug = require('debug')('apiconnect-project:lib:project-loader');
var f = require('util').format;
var fs = require('fs');
var glob = Promise.promisify(require('glob'));
var jsonDeref = require('./json-api-deref');
var jsYaml = require('js-yaml');
var path = require('path');
var pathInspector = require('./path-inspector');
var readFile = Promise.promisify(fs.readFile);
var stat = Promise.promisify(fs.stat);
module.exports = loadProject;
/**
* @typedef {object} Artifact
* @property {string} type `product` or `swagger`.
* @property {string} filePath Resolved path to product or swagger file.
* @property {string} name
* @property {string} version
* @property {Date} mtime Time when filePath was last modified.
* @property {string} raw contents of filePath
* @property {Artifacts} refs - if( type===product ) and referenced product.apis[$ref] exist. Those will also be loaded
* @property {Error} err optional Error
* @property {object} data js-yaml parsed raw content
*/
/**
* @typedef Artifacts
* @type Array.<Artifact>
*/
/**
* Given a file or directory path, return all {Artifact} found in that path.
* If the path is a known project, uses project local `artifacts` configuration to try to resolve
* product/swagger information.
*
* @param {string} basePath Path to file or directory.
* @param {Object} [options] options
* @param {boolean} [options.resolveExternalApiRefs] If true external API $refs will be expanded. If $ref is invalid
* an error will be set into returned <Artifact>.err.
*
* @return Promise.<Artifacts>
* @throws Will throw an error if path is not a file or directory.
*/
function loadProject(basePath, options) {
options = options || {};
var info = pathInspector(basePath);
var load;
if (info.type === 'file') {
load = loadFile(basePath, options).then(function(f) {
return Promise.resolve([ f ]);
});
} else if (info.type === 'project' && info.projectType === 'swiftserver') {
load = loadDir(path.join(info.basePath, 'definitions'), options);
} else if (info.type === 'project' && info.projectType === 'loopback') {
load = loadLoopbackProject(info, options);
} else {
// Default to loading a directory
load = loadDir(basePath, options);
}
return load.then(resolveApiRefs)
.then(_.bind(dereferenceApis, null, _, options));
}
function loadDir(dirPath, options) {
return glob('*.yaml', { cwd: dirPath, nodir: true }).then(function(entries) {
return Promise.all(_.map(entries, function(entry) {
entry = path.resolve(dirPath, entry);
return loadFile(entry, options);
}));
}).then(_.filter)
.then(function(artifacts) {
var apis = [];
artifacts.forEach(function(artifact) {
if (artifact.type !== 'product') {
return;
}
// Ensure that all product->apis refs are reflected in the returned artifacts[].
// The only time apis won't be in there are if they aren't in the root directory.
// We need to do this on the directory path as we only glob for project files that are in the root
// directory, but we support having apis refed in other dirs.
if (artifact.data && artifact.data.apis) {
var productDir = path.dirname(artifact.filePath);
Object.keys(artifact.data.apis).forEach(function(apiName) {
var api = path.resolve(productDir, artifact.data.apis[apiName].$ref);
if (!_.find(apis, api) && !_.find(artifacts, { filePath: api })) {
apis.push(api);
}
});
}
});
var loadApis = Promise.map(apis, _.bind(loadFile, null, _, options));
return Promise.all([ loadApis, Promise.resolve(artifacts) ]);
})
.then(_.flatten)
.then(_.filter);
}
function loadLoopbackProject(info, options) {
var projectDir = info.basePath;
var ignorePattern = [ '**/node_modules/**' ];
var ignoreFile = path.join(projectDir, '.apicignore');
if (fs.existsSync(ignoreFile)) {
var patterns = fs.readFileSync(ignoreFile).toString().split(/\r?\n/);
patterns.forEach(function(item) {
// ignore the empty string and comment line
if (item !== '' && !item.match(/^#.*/)) {
ignorePattern.push(item);
}
});
}
debug('ignoring ', ignorePattern);
return glob('**/*.yaml', { cwd: projectDir, ignore: ignorePattern }).then(function(artifactPaths) {
return Promise.all(_.map(artifactPaths, function(p) {
p = path.resolve(projectDir, p);
try {
var info = pathInspector(p);
if (info.type === 'file') {
return loadFile(p, options);
}
} catch (err) {
debug(err);
}
return Promise.resolve([]);
})).then(function(entries) {
return _.compact(_.flatten(entries));
});
});
}
function loadFile(filePath) {
debug('loadFile %j', filePath);
var raw = readFile(filePath, 'utf8');
return raw.then(function(rawStr) {
return Promise.all([ jsYaml.safeLoad(rawStr), stat(filePath) ]);
})
.spread(function(data, stats) {
if (!data) {
return {
type: 'unknown',
filePath: filePath,
raw: raw.value(),
mtime: stats.mtime,
err: new Error(f('Encountered an empty file %s.', path.basename(filePath))),
};
} else if (data.product === '1.0.0') {
return {
type: 'product',
filePath: filePath,
mtime: stats.mtime,
name: data.info && (data.info['x-ibm-name'] || data.info.name),
data: data,
raw: raw.value(),
version: data.info && data.info.version,
};
} else if (data.swagger === '2.0') {
return {
type: 'swagger',
filePath: filePath,
name: data.info && (data.info['x-ibm-name'] || data.info.name),
data: data,
raw: raw.value(),
mtime: stats.mtime,
version: data.info && data.info.version,
};
} else {
// Non product / api
return {
type: 'unknown',
filePath: filePath,
data: data,
raw: raw.value(),
mtime: (stats ? stats.mtime : undefined),
err: new Error(f('Encountered a file that is not a product or api %s.', filePath)),
};
}
}).catch(function(err) {
var str;
if (raw.isFulfilled()) {
str = raw.value();
}
return {
type: 'unknown',
filePath: filePath,
raw: str,
err: err,
};
});
}
function resolveApiRefs(artifacts, opts) {
return Promise.map(artifacts, function(artifact) {
// Don't do anything if we don't have a product, we couldn't parse or it doesn't have any apis
if (artifact.type !== 'product' || !artifact.data || !artifact.data.apis) {
if (artifact.type === 'product') {
artifact.refs = [];
}
return artifact;
}
return Promise.map(Object.keys(artifact.data.apis), function(apiName) {
var api = artifact.data.apis[apiName];
if (api['$ref']) {
var refFile = path.resolve(path.dirname(artifact.filePath), api['$ref']);
var resolved = _.find(artifacts, { filePath: refFile });
// If we've already loaded this ref, use that Artifact
if (resolved) {
return resolved;
}
return loadFile(refFile);
}
return undefined;
}).then(function(apis) {
apis = _.filter(apis);
if (apis.length > 0) {
artifact.refs = apis;
} else {
artifact.refs = [];
}
return artifact;
});
});
};
function dereferenceApis(artifacts, opts) {
if (!opts.resolveExternalApiRefs) {
return artifacts;
}
return jsonDeref(artifacts);
};