UNPKG

apiconnect-project

Version:

Configuration module IBM API Connect Developer Toolkit

246 lines (234 loc) 8.31 kB
/********************************************************* {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 'use strict'; 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); };