├── test ├── fixtures │ └── fittings │ │ ├── emit.js │ │ ├── test.js │ │ ├── error.js │ │ └── this_is_20_character.js ├── fittings │ └── http.js └── bagpipes.js ├── lib ├── fittings │ ├── emit.js │ ├── first.js │ ├── values.js │ ├── memo.js │ ├── jspath.js │ ├── parse.js │ ├── eval.js │ ├── omit.js │ ├── pick.js │ ├── path.js │ ├── amend.js │ ├── http.js │ ├── onError.js │ ├── read.js │ ├── render.js │ └── parallel.js ├── index.js ├── fittingTypes │ ├── system.js │ ├── node-machine.js │ ├── connect-middleware.js │ ├── user.js │ └── swagger.js ├── helpers.js └── bagpipes.js ├── .gitignore ├── LICENSE ├── package.json └── README.md /test/fixtures/fittings/emit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function create() { 4 | return function emit(context, cb) { 5 | cb(null, 'test'); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/fittings/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function create() { 4 | return function test(context, cb) { 5 | cb(null, 'test'); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/fittings/error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function create() { 4 | return function test(context, cb) { 5 | throw new Error('test error'); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/fittings/this_is_20_character.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function create() { 4 | return function test(context, cb) { 5 | cb(null, 'test'); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /lib/fittings/emit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | 5 | module.exports = function create() { 6 | 7 | return function emit(context, cb) { 8 | 9 | cb(null, context.input); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var bagpipes = require('./bagpipes'); 4 | 5 | module.exports = { 6 | create: create 7 | }; 8 | 9 | function create(pipesDefs, config) { 10 | return bagpipes.create(pipesDefs, config); 11 | } 12 | -------------------------------------------------------------------------------- /lib/fittings/first.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var debug = require('debug')('pipes'); 5 | 6 | module.exports = function create() { 7 | 8 | return function first(context, cb) { 9 | 10 | cb(null, _.first(context.output)); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/fittings/values.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var debug = require('debug')('pipes'); 5 | 6 | module.exports = function create() { 7 | 8 | return function values(context, cb) { 9 | 10 | cb(null, _.values(context.output)); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /lib/fittings/memo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | 5 | module.exports = function create() { 6 | 7 | return function memo(context, cb) { 8 | 9 | context[context.input] = context.output; 10 | 11 | cb(null, context.output); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /lib/fittings/jspath.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var JSPath = require('jspath'); 5 | 6 | module.exports = function create() { 7 | 8 | return function path(context, cb) { 9 | 10 | var input = context.input; 11 | var output = JSPath.apply(input, context.output); 12 | cb(null, output); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/fittings/parse.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | 5 | module.exports = function create() { 6 | 7 | return function parse(context, cb) { 8 | 9 | if (context.input !== 'json') { throw new Error('parse input must be "json"'); } 10 | if (typeof context.output !== 'string') { throw new Error('context.output must be a string'); } 11 | 12 | cb(null, JSON.parse(context.output)); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lib/fittings/eval.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // todo: This is just for play... fix it or (probably) remove it! 4 | 5 | var debug = require('debug')('pipes'); 6 | 7 | module.exports = function create() { 8 | 9 | return function evaluate(context, cb) { 10 | 11 | if (typeof context.input !== 'string') { throw new Error('eval input must be a string'); } 12 | 13 | eval(context.input); 14 | 15 | cb(null, context.output); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /lib/fittings/omit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var debug = require('debug')('pipes'); 5 | 6 | module.exports = function create() { 7 | 8 | return function omit(context, cb) { 9 | 10 | var input = context.input; 11 | 12 | if (Array.isArray(context.output)) { 13 | cb(null, _.map(context.output, _.partialRight(_.omit, input))); 14 | } else { 15 | cb(null, _.omit(context.output, input)); 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/fittings/pick.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var _ = require('lodash'); 5 | 6 | module.exports = function create() { 7 | 8 | return function omit(context, cb) { 9 | 10 | var input = context.input; 11 | 12 | if (Array.isArray(context.output)) { 13 | cb(null, _.map(context.output, _.partialRight(_.pick, input))); 14 | } else { 15 | cb(null, _.pick(context.output, input)); 16 | } 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /lib/fittings/path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | 5 | module.exports = function create() { 6 | 7 | return function path(context, cb) { 8 | 9 | var input = context.input; 10 | var output = _path(input, context.output); 11 | cb(null, output); 12 | } 13 | }; 14 | 15 | function _path(path, obj) { 16 | if (!obj || !path || !path.length) { return null; } 17 | var paths = path.split('.'); 18 | var val = obj; 19 | for (var i = 0, len = paths.length; i < len && val != null; i += 1) { 20 | val = val[paths[i]]; 21 | } 22 | return val; 23 | } 24 | -------------------------------------------------------------------------------- /lib/fittings/amend.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var _ = require('lodash'); 5 | 6 | module.exports = function create() { 7 | 8 | return function amend(context, cb) { 9 | 10 | if (typeof context.input !== 'object' || Array.isArray(context.input)) { 11 | throw new Error('input must be an object'); 12 | } 13 | 14 | if (context.output === null || context.output === undefined) { context.output = {}; } 15 | 16 | if (typeof context.output !== 'object' || Array.isArray(context.output)) { 17 | throw new Error('output must be an object'); 18 | } 19 | 20 | cb(null, _.assign(context.output, context.input)); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /lib/fittings/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var _ = require('lodash'); 5 | var Http = require('machinepack-http'); 6 | 7 | // defaults output -> url 8 | module.exports = function create(fittingDef) { 9 | 10 | var config = _.extend({ baseUrl: '' }, fittingDef.config); 11 | 12 | return function http(context, cb) { 13 | 14 | var input = (typeof context.input === 'string') ? { url: context.input } : context.input; 15 | 16 | var options = _.extend({ url: context.output }, config, input); 17 | 18 | Http.sendHttpRequest(options, cb); 19 | } 20 | }; 21 | 22 | /* input: 23 | url: '/pets/18', 24 | baseUrl: 'http://google.com', 25 | method: 'get', 26 | params: {}, 27 | headers: {} 28 | */ 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE files 2 | .idea 3 | 4 | # Logs 5 | logs 6 | *.log 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Commenting this out is preferred by some people, see 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # Users Environment Variables 31 | .lock-wscript 32 | 33 | .nyc_output 34 | -------------------------------------------------------------------------------- /lib/fittings/onError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var util = require('util'); 5 | 6 | module.exports = function create(fittingDef, bagpipes) { 7 | 8 | if (typeof fittingDef.input !== 'string') { throw new Error('input must be a pipe name'); } 9 | 10 | try { 11 | var pipe = bagpipes.getPipe(fittingDef.input); 12 | } catch (err) { 13 | var pipeDef = [ fittingDef.input ]; 14 | pipe = bagpipes.createPipe(pipeDef); 15 | } 16 | 17 | if (!pipe) { 18 | var msg = util.format('unknown pipe: %s', context.input); 19 | console.error(msg); 20 | throw new Error(msg); 21 | } 22 | 23 | return function onError(context, cb) { 24 | 25 | debug('setting error handler: %s', context.input); 26 | context._errorHandler = pipe; 27 | cb(); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /lib/fittingTypes/system.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes:fittings'); 4 | var path = require('path'); 5 | var assert = require('assert'); 6 | var util = require('util'); 7 | 8 | module.exports = function createFitting(pipes, fittingDef) { 9 | 10 | assert(fittingDef.name, util.format('name is required on fitting: %j', fittingDef)); 11 | 12 | var dir = pipes.config.fittingsDir || path.resolve(__dirname, '../fittings'); 13 | 14 | var modulePath = path.resolve(dir, fittingDef.name); 15 | try { 16 | var module = require(modulePath); 17 | var fitting = module(fittingDef, pipes); 18 | debug('loaded system fitting %s from %s', fittingDef.name, dir); 19 | return fitting; 20 | } catch (err) { 21 | debug('no system fitting %s in %s', fittingDef.name, dir); 22 | throw err; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /lib/fittingTypes/node-machine.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes:fittings'); 4 | var _ = require('lodash'); 5 | var util = require('util'); 6 | var assert = require('assert'); 7 | 8 | module.exports = function createFitting(pipes, fittingDef) { 9 | 10 | assert(fittingDef.machinepack, util.format('machinepack is required on fitting: %j', fittingDef)); 11 | assert(fittingDef.machine, util.format('machine is required on fitting: %j', fittingDef)); 12 | 13 | var machinepack = require(fittingDef.machinepack); 14 | 15 | var machine = machinepack[fittingDef.machine] || _.find(machinepack, function(machine) { 16 | return fittingDef.machine == machine.id; 17 | }); 18 | 19 | if (!machine) { 20 | throw new Error(util.format('unknown machine: %s : %s', fittingDef.machinepack, fittingDef.machine)); 21 | } 22 | 23 | return function(context, next) { 24 | machine(context.input, next); 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Apigee Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bagpipes", 3 | "version": "0.2.2", 4 | "description": "Less code, more flow. Let's dance!", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "mocha test/*", 8 | "coverage": "nyc npm test", 9 | "coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/apigee-127/bagpipes.git" 14 | }, 15 | "keywords": [ 16 | "swagger", 17 | "plumbing", 18 | "pipes" 19 | ], 20 | "author": "Scott Ganyo ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/apigee-127/bagpipes/issues" 24 | }, 25 | "homepage": "https://github.com/apigee-127/bagpipes", 26 | "dependencies": { 27 | "async": "^2.6.4", 28 | "debug": "^2.1.2", 29 | "jspath": "^0.3.1", 30 | "lodash": "^4.17.10", 31 | "machinepack-http": "^8.0.0", 32 | "mustache": "^2.1.3", 33 | "pipeworks": "^1.3.0" 34 | }, 35 | "devDependencies": { 36 | "coveralls": "^3.0.11", 37 | "mocha": "^10.2.0", 38 | "mocha-lcov-reporter": "^1.0.0", 39 | "nyc": "^15.0.1", 40 | "proxyquire": "^2.1.1", 41 | "should": "^7.1.0", 42 | "supertest": "^3.1.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lib/fittings/read.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var assert = require('assert'); 7 | var util = require('util'); 8 | var async = require('async'); 9 | 10 | // todo: redo this as a general "file" operation w/ actions: read, write, ... 11 | 12 | module.exports = function create(fittingDef) { 13 | 14 | var searchPaths = fittingDef.searchPaths; 15 | 16 | assert(searchPaths, util.format('searchPaths is required on fitting: %j', fittingDef)); 17 | if (typeof searchPaths === 'string') { 18 | searchPaths = [searchPaths]; 19 | } 20 | 21 | return function read(context, cb) { 22 | 23 | var fileName = context.input; 24 | 25 | if (typeof fileName !== 'string') { 26 | return cb(new Error('Bad input, must be a file name.')); 27 | } 28 | 29 | // todo: variations.. string vs buffer? utf8 vs other? 30 | var paths = searchPaths.map(function(path) { 31 | path.resolve(path, fileName) 32 | }); 33 | 34 | async.detect(paths, fs.exists, function(file) { 35 | if (!file) { 36 | return cb(new Error(util.format('file %s not found in: %s', fileName, searchPaths))); 37 | } 38 | debug('reading file: %s', file); 39 | fs.readFile(file, 'utf8', cb); 40 | }); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /lib/fittings/render.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var Mustache = require('mustache'); 5 | var path = require('path'); 6 | var fs = require('fs'); 7 | var assert = require('assert'); 8 | var util = require('util'); 9 | 10 | module.exports = function create(fittingDef, bagpipes) { 11 | 12 | var viewDirs = bagpipes.config.userViewsDirs; 13 | assert(viewDirs, 'userViewsDirs not configured'); 14 | 15 | return function render(context, cb) { 16 | 17 | var input = context.input; 18 | 19 | if (typeof input === 'string' && input[0] === '@') { 20 | 21 | var fileName = input.slice(1); 22 | input = getInput(viewDirs, fileName); 23 | if (!input) { 24 | throw new Error(util.format('file not found for %j in %s', fittingDef, bagpipes.config.userViewsDirs)); 25 | } 26 | } 27 | 28 | var output = Mustache.render(input, context.output); 29 | cb(null, output); 30 | } 31 | }; 32 | 33 | function getInput(viewDirs, fileName) { 34 | for (var i = 0; i < viewDirs.length; i++) { 35 | var dir = viewDirs[i]; 36 | var file = path.resolve(dir, fileName); 37 | try { 38 | debug('reading mustache file: %s', file); 39 | return fs.readFileSync(file, 'utf8'); 40 | } catch (err) { 41 | debug('no mustache file here: %s', file); 42 | } 43 | } 44 | return null; 45 | } 46 | -------------------------------------------------------------------------------- /lib/fittings/parallel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes'); 4 | var async = require('async'); 5 | var _ = require('lodash'); 6 | var pipeworks = require('pipeworks'); 7 | var helpers = require('../helpers'); 8 | 9 | 10 | // todo: this only currently works with backward references 11 | 12 | module.exports = function create(fittingDef, bagpipes) { 13 | 14 | return function parallel(context, cb) { 15 | debug('create parallel'); 16 | 17 | var tasks = {}; 18 | _.each(context.input, function(pipeNameOrDef, key) { 19 | var pipe = (typeof pipeNameOrDef === 'string') 20 | ? bagpipes.getPipe(pipeNameOrDef) 21 | : bagpipes.getPipe(null, pipeNameOrDef); 22 | tasks[key] = createTask(context, pipe, key); 23 | }); 24 | 25 | async.parallel(tasks, function(err, result) { 26 | debug('parallel done'); 27 | cb(err, result); 28 | }); 29 | }; 30 | }; 31 | 32 | function createTask(context, pipe, name) { 33 | return function execTask(cb) { 34 | pipeworks() 35 | .fit(function startParallel(context, next) { 36 | debug('starting parallel pipe: %s', name); 37 | pipe.siphon(context, next); 38 | }) 39 | .fit(function finishParallel(context, ignore) { 40 | debug('finished parallel pipe: %s', name); 41 | cb(null, context.output); 42 | }) 43 | .fault(helpers.faultHandler) 44 | .flow(context); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /lib/fittingTypes/connect-middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes:fittings'); 4 | var path = require('path'); 5 | var _ = require('lodash'); 6 | var assert = require('assert'); 7 | var util = require('util'); 8 | 9 | module.exports = function createFitting(pipes, fittingDef) { 10 | 11 | assert(fittingDef.module, util.format('module is required on fitting: %j', fittingDef)); 12 | assert(fittingDef.function, util.format('function is required on fitting: %j', fittingDef)); 13 | 14 | for (var i = 0; i < pipes.config.connectMiddlewareDirs.length; i++) { 15 | var dir = pipes.config.connectMiddlewareDirs[i]; 16 | 17 | try { 18 | var modulePath = path.resolve(dir, fittingDef.module); 19 | var controller = require(modulePath); 20 | var fn = controller[fittingDef['function']]; 21 | 22 | if (fn) { 23 | debug('using %s controller in %s', fittingDef.module, dir); 24 | return connectCaller(fn); 25 | } else { 26 | debug('missing function %s on controller %s in %s', fittingDef['function'], fittingDef.module, dir); 27 | } 28 | } catch (err) { 29 | debug('no controller %s in %s', fittingDef.module, dir); 30 | } 31 | } 32 | 33 | throw new Error('controller not found in %s for fitting %j', pipes.config.connectMiddlewareDirs, fittingDef); 34 | }; 35 | 36 | function connectCaller(fn) { 37 | return function connect_middleware(context, next) { 38 | fn(context.request, context.response, function(err) { 39 | next(err); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/fittingTypes/user.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes:fittings'); 4 | var path = require('path'); 5 | var util = require('util'); 6 | var assert = require('assert'); 7 | 8 | module.exports = function createFitting(pipes, fittingDef) { 9 | 10 | assert(fittingDef.name, util.format('name is required on fitting: %j', fittingDef)); 11 | 12 | // If there is pre-initialized fittings modules available, return these 13 | if (pipes.config.fittings && pipes.config.fittings[fittingDef.name]) { 14 | debug('loaded user fitting %s from pre-initialized modules', fittingDef.name); 15 | return pipes.config.fittings[fittingDef.name](fittingDef, pipes); 16 | } 17 | 18 | if (!pipes.config.userFittingsDirs) { return null; } 19 | 20 | for (var i = 0; i < pipes.config.userFittingsDirs.length; i++) { 21 | var dir = pipes.config.userFittingsDirs[i]; 22 | 23 | var modulePath = path.resolve(dir, fittingDef.name); 24 | try { 25 | var module = require(modulePath); 26 | if (module.default && typeof module.default === 'function') { 27 | module = module.default; 28 | } 29 | 30 | var fitting = module(fittingDef, pipes); 31 | debug('loaded user fitting %s from %s', fittingDef.name, dir); 32 | return fitting; 33 | } catch (err) { 34 | if (err.code !== 'MODULE_NOT_FOUND') { throw err; } 35 | var pathFromError = err.message.match(/'.*?'/)[0]; 36 | var split = pathFromError.split(path.sep); 37 | if (split[split.length - 1] === fittingDef.name + "'") { 38 | debug('no user fitting %s in %s', fittingDef.name, dir); 39 | } else { 40 | throw err; 41 | } 42 | } 43 | } 44 | 45 | if (fittingDef.type !== 'user') { 46 | return null; 47 | } 48 | 49 | throw new Error('user fitting %s not found in %s', fittingDef, pipes.config.userFittingsDirs); 50 | }; 51 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'); 4 | var debug = require('debug')('pipes'); 5 | var JSPath = require('jspath'); 6 | 7 | module.exports = { 8 | resolveInput: resolveInput, 9 | faultHandler: faultHandler 10 | }; 11 | 12 | function resolveInput(context, input) { 13 | 14 | if (!input) { return context.output; } 15 | 16 | if (Array.isArray(input)) { 17 | return _.map(input, function(input) { return resolveInput(context, input); }); 18 | } 19 | 20 | if (isParameter(input)) { 21 | debug('isInput: ', input); 22 | return getParameterValue(input, context); 23 | } 24 | 25 | if (typeof input === 'object') { 26 | var result = {}; 27 | _.each(input, function(inputDef, name) { 28 | 29 | result[name] = 30 | (isParameter(inputDef)) 31 | ? getParameterValue(inputDef, context) 32 | : resolveInput(context, inputDef); 33 | }); 34 | return result; 35 | } 36 | 37 | return input; 38 | } 39 | 40 | function isParameter(inputDef) { 41 | return !_.isUndefined(inputDef) 42 | && (typeof inputDef === 'string' || 43 | (typeof inputDef === 'object' && inputDef.path && inputDef.default)); 44 | } 45 | 46 | // parameter: string || { path, default } 47 | function getParameterValue(parameter, context) { 48 | 49 | var path = parameter.path || parameter; 50 | 51 | var value = (path[0] === '.') ? JSPath.apply(path, context) : path; 52 | 53 | //console.log('****', path, context, value); 54 | 55 | // Use the default value when necessary 56 | if (_.isUndefined(value)) { value = parameter.default; } 57 | 58 | return value; 59 | } 60 | 61 | // todo: move to connect_middleware ! 62 | function faultHandler(context, error) { 63 | debug('default errorHandler: %s', error.stack ? error.stack : error.message); 64 | if (context.response) { 65 | context.response.statusCode = 500; 66 | context.response.end(error.message); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/fittingTypes/swagger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var debug = require('debug')('pipes:fittings'); 4 | var util = require('util'); 5 | var assert = require('assert'); 6 | 7 | module.exports = function createFitting(pipes, fittingDef) { 8 | 9 | assert(fittingDef.url, util.format('url is required on fitting: %j', fittingDef)); 10 | 11 | var client = require('swagger-client'); 12 | var swagger = new client.SwaggerClient({ 13 | url: fittingDef.url, 14 | success: function() { 15 | debug('swaggerjs initialized'); 16 | }, 17 | failure: function(err) { 18 | console.log('swaggerjs', err); // todo: what? 19 | }, 20 | progress: function(msg) { 21 | debug('swaggerjs', msg); 22 | } 23 | }); 24 | swagger.build(); 25 | 26 | return function(context, next) { 27 | var api = swagger.apis[context.input.api]; 28 | var operation = api[context.input.operation]; 29 | debug('swagger-js api: %j', api); 30 | debug('swagger-js operation: %j', operation); 31 | debug('swagger-js input: %j', context.input); 32 | operation(context.input, function(result) { 33 | next(null, result); 34 | }); 35 | }; 36 | }; 37 | 38 | /* 39 | Example SwaggerJs result: 40 | 41 | { headers: 42 | { input: 43 | { 'access-control-allow-origin': '*', 44 | 'access-control-allow-methods': 'GET, POST, DELETE, PUT', 45 | 'access-control-allow-headers': 'Content-Type, api_key, Authorization', 46 | 'content-type': 'application/json', 47 | connection: 'close', 48 | server: 'Jetty(9.2.7.v20150116)' }, 49 | normalized: 50 | { 'Access-Control-Allow-Origin': '*', 51 | 'Access-Control-Allow-Methods': 'GET, POST, DELETE, PUT', 52 | 'Access-Control-Allow-Headers': 'Content-Type, api_key, Authorization', 53 | 'Content-Type': 'application/json', 54 | Connection: 'close', 55 | Server: 'Jetty(9.2.7.v20150116)' } }, 56 | url: 'http://petstore.swagger.io:80/v2/pet/1', 57 | method: 'GET', 58 | status: 200, 59 | data: , 60 | obj: 61 | { id: 1, 62 | category: { id: 1, name: 'string' }, 63 | name: 'doggie', 64 | photoUrls: [ 'string' ], 65 | tags: [ [Object] ], 66 | status: 'string' } } 67 | */ 68 | -------------------------------------------------------------------------------- /test/fittings/http.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var _ = require('lodash'); 7 | var Bagpipes = require('../../lib'); 8 | const proxyquire = require('proxyquire'); 9 | 10 | describe('http', function() { 11 | 12 | var fitting = proxyquire('../../lib/fittings/http', { 13 | 'machinepack-http': { 14 | sendHttpRequest: function(options, cb) { 15 | cb(null, options) 16 | } 17 | } 18 | }); 19 | 20 | it('should override config baseUrl with input baseURL', function(done) { 21 | 22 | var fittingDef = { 23 | config: { 24 | baseUrl: 'configBaseURL', 25 | }, 26 | input: { 27 | baseUrl: 'inputBaseURL', 28 | url: 'someURL', 29 | method: 'get', 30 | params: {}, 31 | headers: {} 32 | } 33 | } 34 | 35 | context = { 36 | input: fittingDef.input 37 | } 38 | 39 | var http = fitting(fittingDef) 40 | http(context, function(err, configured) { 41 | configured.baseUrl.should.eql(fittingDef.input.baseUrl); 42 | done() 43 | }) 44 | }) 45 | 46 | it('should use config baseUrl when no input', function(done) { 47 | 48 | var fittingDef = { 49 | config: { 50 | baseUrl: 'configBaseURL', 51 | }, 52 | input: { 53 | url: 'someURL', 54 | method: 'get', 55 | params: {}, 56 | headers: {} 57 | } 58 | } 59 | 60 | context = { 61 | input: fittingDef.input 62 | } 63 | 64 | var http = fitting(fittingDef) 65 | http(context, function(err, configured) { 66 | configured.baseUrl.should.eql(fittingDef.config.baseUrl); 67 | done() 68 | }) 69 | }) 70 | 71 | it('should use input baseUrl when no config', function(done) { 72 | 73 | var fittingDef = { 74 | config: { 75 | }, 76 | input: { 77 | baseUrl: 'inputBaseURL', 78 | url: 'someURL', 79 | method: 'get', 80 | params: {}, 81 | headers: {} 82 | } 83 | } 84 | 85 | context = { 86 | input: fittingDef.input 87 | } 88 | 89 | var http = fitting(fittingDef) 90 | http(context, function(err, output) { 91 | output.should.eql(context.input); 92 | done() 93 | }) 94 | }) 95 | }); 96 | -------------------------------------------------------------------------------- /test/bagpipes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var should = require('should'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var _ = require('lodash'); 7 | var Bagpipes = require('../lib'); 8 | 9 | describe('bagpipes', function() { 10 | 11 | it('should load all system fittings', function(done) { 12 | var dir = path.resolve(__dirname, '../lib/fittings'); 13 | fs.readdir(dir, function(err, files) { 14 | if (err) { return done(err); } 15 | var fittingNames = files.map(function(name) { return name.split('.')[0] }); 16 | var skipFittings = [ 'onError', 'render', 'read' ]; // these need extra parameters to create 17 | fittingNames = fittingNames.filter(function(name) { return skipFittings.indexOf(name) < 0 }); 18 | var fittings = fittingNames.map(function (name) { 19 | var fittingDef = {}; 20 | fittingDef[name] = 'nothing'; 21 | return fittingDef; 22 | }); 23 | var bagpipes = Bagpipes.create({ fittings: fittings }); 24 | bagpipes.pipes.fittings.pipes.length.should.eql(fittingNames.length); 25 | done(); 26 | }); 27 | }); 28 | 29 | it('should load all user fittings', function(done) { 30 | var dir = path.resolve(__dirname, './fixtures/fittings'); 31 | var userFittingsDirs = [ dir ]; 32 | fs.readdir(dir, function(err, files) { 33 | if (err) { return done(err); } 34 | var fittingNames = files.map(function(name) { return name.split('.')[0] }); 35 | var fittings = fittingNames.map(function (name) { 36 | var fittingDef = {}; 37 | fittingDef[name] = 'nothing'; 38 | return fittingDef; 39 | }); 40 | var bagpipes = Bagpipes.create({ fittings: fittings }, { userFittingsDirs: userFittingsDirs }); 41 | bagpipes.pipes.fittings.pipes.length.should.eql(fittingNames.length); 42 | done(); 43 | }); 44 | }); 45 | 46 | it('should run a pipe with a system fitting', function(done) { 47 | var pipe = [ { 'emit': 'something' } ]; 48 | var bagpipes = Bagpipes.create({ pipe: pipe }); 49 | var context = {}; 50 | bagpipes.play(bagpipes.getPipe('pipe'), context); 51 | context.output.should.eql('something'); 52 | done(); 53 | }); 54 | 55 | it('should load a fitting from a custom directory', function(done) { 56 | var userFittingsDirs = [ path.resolve(__dirname, './fixtures/fittings') ]; 57 | var pipe = [ 'emit' ]; 58 | var bagpipes = Bagpipes.create({ pipe: pipe }, { userFittingsDirs: userFittingsDirs }); 59 | var context = {}; 60 | bagpipes.play(bagpipes.getPipe('pipe'), context); 61 | context.output.should.eql('test'); 62 | done(); 63 | }); 64 | 65 | it('should load pre-initialized fittings', function(done) { 66 | var emitFitting = function create() { 67 | return function (context, cb) { 68 | cb(null, 'pre-initialized'); 69 | }}; 70 | var pipe = [ 'emit' ]; 71 | var bagpipes = Bagpipes.create({ pipe: pipe }, {fittings: { emit: emitFitting}}); 72 | var context = {}; 73 | bagpipes.play(bagpipes.getPipe('pipe'), context); 74 | context.output.should.eql('pre-initialized'); 75 | done(); 76 | }); 77 | 78 | it('should allow user fittings to override system fittings', function(done) { 79 | var userFittingsDirs = [ path.resolve(__dirname, './fixtures/fittings') ]; 80 | var pipe = [ 'test' ]; 81 | var bagpipes = Bagpipes.create({ pipe: pipe }, { userFittingsDirs: userFittingsDirs }); 82 | var context = {}; 83 | bagpipes.play(bagpipes.getPipe('pipe'), context); 84 | context.output.should.eql('test'); 85 | done(); 86 | }); 87 | 88 | it('should throw errors if no onError handler', function(done) { 89 | var userFittingsDirs = [ path.resolve(__dirname, './fixtures/fittings') ]; 90 | var pipe = [ 'error' ]; 91 | var bagpipes = Bagpipes.create({ pipe: pipe }, { userFittingsDirs: userFittingsDirs }); 92 | var context = {}; 93 | (function() { 94 | bagpipes.play(bagpipes.getPipe('pipe'), context) 95 | }).should.throw.error; 96 | done(); 97 | }); 98 | 99 | it('should handle errors if onError registered', function(done) { 100 | var userFittingsDirs = [ path.resolve(__dirname, './fixtures/fittings') ]; 101 | var pipe = [ { onError: 'emit'}, 'error' ]; 102 | var bagpipes = Bagpipes.create({ pipe: pipe }, { userFittingsDirs: userFittingsDirs }); 103 | var context = {}; 104 | bagpipes.play(bagpipes.getPipe('pipe'), context); 105 | context.error.should.be.an.Error; 106 | context.output.should.eql('test'); 107 | done(); 108 | }) 109 | }); 110 | -------------------------------------------------------------------------------- /lib/bagpipes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pipeworks = require('pipeworks'); 4 | var _ = require('lodash'); 5 | var debug = require('debug')('pipes'); 6 | var debugContent = require('debug')('pipes:content'); 7 | var helpers = require('./helpers'); 8 | var util = require('util'); 9 | var fs = require('fs'); 10 | var path = require('path'); 11 | var assert = require('assert'); 12 | 13 | // todo: allow for forward pipe refs? 14 | 15 | module.exports = { 16 | // conf: { connectMiddlewareDirs: [], userFittingsDirs: [], userViewsDirs: [] } 17 | create: function createPipes(pipesDefs, conf) { 18 | return new Bagpipes(pipesDefs, conf); 19 | } 20 | }; 21 | 22 | function Bagpipes(pipesDefs, conf) { 23 | debug('creating pipes:', pipesDefs); 24 | //debug('config:', conf); 25 | 26 | this.fittingTypes = {}; 27 | this.pipes = {}; 28 | this.config = _.clone(conf || {}); 29 | 30 | this.loadFittingTypes(); 31 | 32 | var self = this; 33 | _.each(pipesDefs, function(pipeDef, pipeName) { 34 | self.getPipe(pipeName, pipeDef); 35 | }); 36 | 37 | debug('done creating pipes'); 38 | } 39 | 40 | // lazy create if pipeDef is included 41 | Bagpipes.prototype.getPipe = function getPipe(pipeName, pipeDef) { 42 | 43 | if (pipeName && this.pipes[pipeName]) { return this.pipes[pipeName]; } // defined pipe 44 | 45 | if (!pipeDef) { 46 | throw new Error('Pipe not found: ' + pipeName); 47 | } 48 | 49 | debug('creating pipe %s: %j', pipeName, pipeDef); 50 | return this.pipes[pipeName] = this.createPipe(pipeDef); 51 | }; 52 | 53 | Bagpipes.prototype.play = function play(pipe, context) { 54 | 55 | assert(typeof context === 'undefined' || typeof context === 'object', 'context must be an object'); 56 | if (!context) { context = {}; } 57 | 58 | var pw = pipeworks(); 59 | pw.fit(function(context, next) { pipe.siphon(context, next); }); 60 | if (context._finish) { pw.fit(context._finish); } 61 | pw.flow(context); 62 | }; 63 | 64 | Bagpipes.prototype.createPipe = function createPipe(pipeDef) { 65 | 66 | var self = this; 67 | var pipe = pipeworks(); 68 | 69 | if (Array.isArray(pipeDef)) { // an array is a pipe 70 | 71 | pipeDef.forEach(function(step) { 72 | 73 | var keys = (typeof step === 'object') ? Object.keys(step) : undefined; 74 | 75 | if (keys && keys.length > 1) { // parallel pipe 76 | 77 | fittingDef = { 78 | name: 'parallel', 79 | input: step 80 | }; 81 | pipe.fit(self.createFitting(fittingDef)); 82 | 83 | } else { 84 | 85 | var name = keys ? keys[0] : step; 86 | var input = keys ? step[name] : undefined; 87 | 88 | if (self.pipes[name]) { // a defined pipe 89 | 90 | debug('fitting pipe: %s', name); 91 | var stepPipe = self.getPipe(name); 92 | pipe.fit(function(context, next) { 93 | debug('running pipe: %s', name); 94 | if (input) { 95 | context.input = helpers.resolveInput(context, input); 96 | debug('input: %j', context.input); 97 | } 98 | stepPipe.siphon(context, next); 99 | }); 100 | 101 | } else { // a fitting 102 | 103 | var fittingDef = { name: name, input: input }; 104 | pipe.fit(self.createFitting(fittingDef)); 105 | } 106 | } 107 | }); 108 | 109 | } else { // a 1-fitting pipe 110 | 111 | pipe.fit(this.createFitting(pipeDef)); 112 | } 113 | 114 | return pipe; 115 | }; 116 | 117 | Bagpipes.prototype.createPipeFromFitting = function createPipeFromFitting(fitting, def) { 118 | return pipeworks().fit(this.wrapFitting(fitting, def)); 119 | }; 120 | 121 | Bagpipes.prototype.loadFittingTypes = function loadFittingTypes() { 122 | 123 | var fittingTypesDir = path.resolve(__dirname, 'fittingTypes'); 124 | 125 | var files = fs.readdirSync(fittingTypesDir); 126 | 127 | var self = this; 128 | files.forEach(function(file) { 129 | if (file.substr(-3) === '.js') { 130 | var name = file.substr(0, file.length - 3); 131 | self.fittingTypes[name] = require(path.resolve(fittingTypesDir, file)); 132 | debug('loaded fitting type: %s', name); 133 | } 134 | }); 135 | return this.fittingTypes; 136 | }; 137 | 138 | Bagpipes.prototype.createFitting = function createFitting(fittingDef) { 139 | 140 | debug('create fitting %j', fittingDef); 141 | 142 | if (fittingDef.type) { 143 | return this.newFitting(fittingDef.type, fittingDef); 144 | } 145 | 146 | // anonymous fitting, try user then system 147 | var fitting = this.newFitting('user', fittingDef); 148 | if (!fitting) { 149 | fitting = this.newFitting('system', fittingDef); 150 | } 151 | return fitting; 152 | }; 153 | 154 | Bagpipes.prototype.newFitting = function newFitting(fittingType, fittingDef) { 155 | var fittingFactory = this.fittingTypes[fittingType]; 156 | if (!fittingFactory) { throw new Error('invalid fitting type: ' + fittingType); } 157 | 158 | var fitting = fittingFactory(this, fittingDef); 159 | return this.wrapFitting(fitting, fittingDef); 160 | }; 161 | 162 | Bagpipes.prototype.wrapFitting = function wrapFitting(fitting, fittingDef) { 163 | 164 | if (!fitting) { return null; } 165 | 166 | var self = this; 167 | return function(context, next) { 168 | try { 169 | preflight(context, fittingDef); 170 | debug('enter fitting: %s', fittingDef.name); 171 | fitting(context, function(err, result) { 172 | debug('exit fitting: %s', fittingDef.name); 173 | if (err) { return self.handleError(context, err); } 174 | postFlight(context, fittingDef, result, next); 175 | }); 176 | } catch (err) { 177 | self.handleError(context, err); 178 | } 179 | }; 180 | }; 181 | 182 | Bagpipes.prototype.handleError = function handleError(context, err) { 183 | 184 | if (!util.isError(err)) { err = new Error(JSON.stringify(err)); } 185 | 186 | debug('caught error: %s', err.stack); 187 | if (!context._errorHandler) { throw err; } 188 | 189 | context.error = err; 190 | debug('starting onError pipe'); 191 | var errorHandler = context._errorHandler; 192 | delete(context._errorHandler); 193 | this.play(errorHandler, context); 194 | }; 195 | 196 | function preflight(context, fittingDef) { 197 | 198 | debug('pre-flight fitting: %s', fittingDef.name); 199 | var resolvedInput = helpers.resolveInput(context, fittingDef.input); 200 | if (typeof resolvedInput === 'object' && !Array.isArray(resolvedInput)) { 201 | context.input = _.defaults({}, context.input, resolvedInput); 202 | } else { 203 | context.input = resolvedInput || context.input; 204 | } 205 | debug('input: %j', context.input); 206 | } 207 | 208 | function postFlight(context, fittingDef, result, next) { 209 | 210 | debug('post-flight fitting: %s', fittingDef.name); 211 | context.input = undefined; 212 | var target = fittingDef.output; 213 | if (target) { 214 | if (target[0] === '_') { throw new Error('output names starting with _ are reserved'); } 215 | } else { 216 | target = 'output'; 217 | } 218 | context[target] = result; 219 | debugContent('output (context[%s]): %j', target, context.output); 220 | next(context); 221 | } 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bagpipes 2 | 3 | [![Coverage Status](https://coveralls.io/repos/github/apigee-127/bagpipes/badge.svg?branch=master)](https://coveralls.io/github/apigee-127/bagpipes?branch=master) 4 | 5 | 6 | ### NOTE: THIS IS PRE-RELEASE SOFTWARE - SUBJECT TO CHANGE ### 7 | 8 | ** Quick Reference links: ** 9 | 10 | * [Installation](#installation) 11 | * [Pipes](#pipes) 12 | * [Parallel Execution](#parallel-execution) 13 | * [Context](#context) 14 | * [Error Handling](#error-handling) 15 | * [Fittings](#fittings) 16 | * [System Fittings](#system-fittings) 17 | * [User Defined Fittings](#user-defined-fittings) 18 | * [Debugging](#debugging) 19 | * [Change Log](#change-log) 20 | 21 | ## What is Bagpipes? 22 | 23 | Bagpipes was developed as a way to enable API flows and mashups to be created declaratively in YAML (or JSON) 24 | without writing code. It works a lot like functional programming... there's no global state, data is just 25 | passed from one function to the next down the line until we're done. (Similar to connect middleware.) 26 | 27 | For example, to expose an API to get the latitude and longitude of an address using Google's Geocode API, one 28 | could simply define a flow that looks like this: 29 | 30 | ```yaml 31 | # 1. Define a http callout we'll use in our pipe 32 | google_geocode: 33 | name: http 34 | input: 35 | url: http://maps.googleapis.com/maps/api/geocode/json?sensor=true 36 | params: 37 | address: .request.parameters.address.value[0] 38 | 39 | # 2. Defined the pipe flow we'll play 40 | getAddressLocation: 41 | - google_geocode # call the fitting defined in this swagger 42 | - path: body # system fitting: get body from output 43 | - parse: json # body is a json string, parse to object 44 | - path: results # get results from body 45 | - first # get first result 46 | - path: geometry.location # output = { lat: n, lng: n } 47 | ``` 48 | 49 | But that's just a quick example, you can do much, much more... including filtering, error handling, and even 50 | parallel handling like mashup HTTP requests to multiple services. 51 | 52 | ## Getting started 53 | 54 | Here's a simple, self-contained "Hello, World" example you can run: 55 | 56 | ```js 57 | var bagpipes = require('bagpipes'); 58 | 59 | var pipesDefs = { 60 | HelloWorld: [ 61 | { emit: 'Hello, World!' } 62 | ] 63 | }; 64 | 65 | var pipesConfig = {}; 66 | var pipes = bagpipes.create(pipesDefs, pipesConfig); 67 | var pipe = pipes.getPipe('HelloWorld'); 68 | 69 | // log the output to standard out 70 | pipe.fit(function(context, cb) { 71 | console.log(context.output); 72 | cb(null, context); 73 | }); 74 | 75 | var context = {}; 76 | pipes.play(pipe, context); 77 | ``` 78 | 79 | As you can see, the pipe in the hello world above is defined programmatically. This is perfectly ok, but 80 | in general, you'll probably want load your pipe definitions from a YAML file something like this: 81 | 82 | ```yaml 83 | HelloWorld: 84 | - emit: 'Hello, World!' 85 | ``` 86 | 87 | ```js 88 | var bagpipes = require('bagpipes'); 89 | var yaml = require('js-yaml'); 90 | 91 | var pipesDefs = yaml.safeLoad(fs.readFileSync('HelloWorld.yaml')); 92 | var pipes = bagpipes.create(pipesDefs, pipesConfig); 93 | var pipe = pipes.getPipe('HelloWorld'); 94 | 95 | // log the output to standard out 96 | pipe.fit(function(context, cb) { 97 | console.log(context.output); 98 | cb(null, context); 99 | }); 100 | 101 | var context = {}; 102 | pipes.play(pipe, context); 103 | ``` 104 | 105 | Either way, have fun! 106 | 107 | ## Fittings 108 | 109 | So what are these things called "fittings"? Well, simply, if a pipe is a list of steps, a fitting describes 110 | what a single step actually accomplishes. 111 | 112 | Let's take a very simple example: Say we have some data that looks like this: 113 | 114 | ```js 115 | [ { "name": "Scott", "city": "Los Angeles" } 116 | { "name": "Jeff", "city": "San Francisco" } ] 117 | ``` 118 | 119 | Now, we'll create a pipe that just retrieves the first name. In the definition below, we've defined a pipe called 120 | "getFirstUserName" that consists of a couple of system-provided fittings: 121 | 122 | ```yaml 123 | getFirstUserName: 124 | - first 125 | - path: name 126 | ``` 127 | 128 | The "first" fitting selects the first element of an array passed in. The "path" fitting selects the "user" attribute 129 | from the object passed on by the first fitting. Thus, the result from our example is "Scott". 130 | 131 | Or, say we want to get all the names from our data as an array. We could simply do it like this: 132 | 133 | ```yaml 134 | getUserNames: 135 | - pick: name 136 | ``` 137 | 138 | Obviously, these are trivial examples, but you can create pipes as long and as complex as you wish. In fact, you can 139 | even write your own special-purpose fittings. We'll get to that [later](#user-defined-fittings). 140 | 141 | ### Fitting Definition 142 | 143 | When you want to use a fitting in your pipe, you have 2 options: 144 | 145 | 1. A system or user fitting with zero or one input can be defined in-line, as we have shown above. 146 | 2. A fitting with configuration or more complex inputs may need to be defined before use. 147 | 148 | Let's look at the 2nd type. Here's an example of a fitting that calls out to an API with a URL that looks like 149 | something like this: http://maps.googleapis.com/maps/api/geocode/json?sensor=true?address=Los%20Angeles. And, of 150 | course, we'll want to make the address dynamic. This requires a a little bit of configuration: We need to tell the 151 | "http" fitting the URL, the operation, and what parameters to use (and how to get them): 152 | 153 | ```yaml 154 | geocode: 155 | name: http 156 | input: 157 | operation: get 158 | url: http://maps.googleapis.com/maps/api/geocode/json 159 | params: 160 | sensor: true 161 | address: .output.address[0] 162 | ``` 163 | 164 | As you can see above, we've give our fitting a name ("geocode") and specified which type of fitting we're creating 165 | (a "system" fitting called "http"). This fitting requires several inputs including the HTTP operation, the URL, and 166 | parameters to pass. Each of these is just a static string in this case except for the "address" parameter. The 167 | address is merely retrieved by picking the "address" property from the "output" object of whatever fitting came 168 | before it in the pipe. (Note: There are several options for input sources that will be defined later.) 169 | 170 | By default, the output of this operation will be placed on the pipe in the "output" variable for the next fitting 171 | to use - or to be returned to the client if it's the last fitting to execute. 172 | 173 | ----- 174 | 175 | # Reference 176 | 177 | ## Pipe 178 | 179 | A Pipe is just defined in YAML as an Array. It can be reference by its key and can reference other pipes and fittings by 180 | their keys. Each step in a pipe may be one of the following: 181 | 182 | 1. A pipe name 183 | 2. A fitting name (with an optional value) 184 | 3. An set of key/value pairs defining pipes to be performed in parallel 185 | 186 | If a fitting reference includes a value, that value will be emitted onto the output for the fitting to consume. Most 187 | of the system fittings are able to operate solely on the output without any additional configuration - similar to a 188 | Unix pipe. 189 | 190 | ### Parallel Execution 191 | 192 | Generally, a pipe flows from top to bottom in serial manner. However, in some cases it is desirable to execute two 193 | pipes in parallel (for example, a mashup of two external APIs). 194 | 195 | Parallel execution of pipes can be done by using key/value pairs on the pipe in place of a single step. The output 196 | from each pipe will be assigned to the key associated with it. It's probably easiest to explain by example: 197 | 198 | ```yaml 199 | getRestaurantsAndWeather: 200 | - getAddressLocation 201 | - restaurants: getRestaurants 202 | weather: getWeather 203 | ``` 204 | 205 | This pipe will first flow through getAddressLocation step. Then, because the restaurants and weather keys are both on 206 | the same step, it will execute the getRestaurants and getWeather pipes concurrently. The final output of this pipe 207 | will be an object that looks like this: { restaurants: {...}, weather: {...} } where the values will be the output 208 | from the respective pipes. 209 | 210 | ### Context 211 | 212 | The context object that is passed through the pipe has the following properties that should be generally used by the 213 | fittings to accept input and deliver output via the pipe to other fittings or to the client: 214 | 215 | * **input**: the input defined in the fitting definition (string, number, object, array) 216 | * **output**: output to be delivered to the next fitting or client 217 | 218 | In addition, the context has the following properties that should not be modified - and, in general, you shouldn't 219 | need to access them at all: 220 | 221 | * **_errorHandler**: the pipe played if an error occurs in the pipe 222 | * **_finish**: a final fitting or pipe run once the pipe has finished (error or not) 223 | 224 | Finally, the context object itself will contain any properties that you've assigned to it via the 'output' option on 225 | your fitting definition. 226 | 227 | Notes: 228 | 229 | The context object is extensible as well. The names listed above as well as any name starting with '_' should be 230 | considered reserved, but you may assign other additional properties to the context should you need it for communication 231 | between fittings (see also the [memo](#memo) fitting). 232 | 233 | ### Error Handling 234 | 235 | You may install a custom error handler pipe by specifying them using the system [onError](#onError) fitting. (As 236 | you might guess, this actually sets the _errorHandler property on context.) 237 | 238 | ## Fittings 239 | 240 | All fittings may have the following values (all of which are optional): 241 | 242 | * **type**: one of: system, user, swagger, node-machine 243 | * **name**: the name of the fitting of the type specified 244 | * **config**: static values passed to the fitting during construction 245 | * **input**: dynamic values passed to the fitting during execution 246 | * **output**: The name of the context key to which the output value is assigned 247 | 248 | #### Type 249 | 250 | If type is omitted (as it must be for in-line usage), Bagpipes will first check the user fittings then the 251 | system fittings for the name and use the first fitting found. Thus be aware that if you define a fitting with the 252 | same name as a system one, your fitting will override it. 253 | 254 | #### Input 255 | 256 | The **input** may be a hash, array, or constant. The value or sub-values of the input is defined as either: 257 | 258 | * a constant string or number value 259 | * a **reference** to a value 260 | 261 | A **reference** is a value populated either from data on the request or from the output of previous fittings on the 262 | pipe. It is defined like so: 263 | 264 | ```yaml 265 | key: # the variable name (key) on context.input to which the value is assigned 266 | path: '' # the variable to pick from context using [json path syntax](https://www.npmjs.com/package/jspath) 267 | default: '' # (optional) value to assign if the referenced value is undefined 268 | ``` 269 | 270 | Note: If there is no input definition, input will be assigned to the prior fitting's output. 271 | 272 | See also [Context](#context) for more information. 273 | 274 | 275 | #### System Fittings 276 | 277 | There are 2 basic types of system fittings: Internal fittings that just modify output in a flow and those that are 278 | callouts to other systems. These are listed below by category: 279 | 280 | ##### Internal Fittings 281 | 282 | ###### amend: input 283 | 284 | Amend the pipe output by copying the fields from input. Overrides output. Input and output must be objects. 285 | 286 | ###### emit: input 287 | 288 | Emit the fitting's input onto onto the pipe output. 289 | 290 | ###### eval: 'script' 291 | 292 | Used for testing and will likely be removed, but evaluates provided javascript directly. 293 | 294 | ###### first 295 | 296 | Select the first element from an array. 297 | 298 | ###### jspath: jspath 299 | 300 | Selects output using [json path](https://www.npmjs.com/package/jspath) syntax. 301 | 302 | ###### memo: key 303 | 304 | Saves the current context.output value to context[key]. Can be later retrieved via: 305 | 306 | ```yaml 307 | emit: 308 | name: key 309 | in: context 310 | ``` 311 | 312 | ###### omit: key | [ keys ] 313 | 314 | Omit the specified key or keys from an object. 315 | 316 | ###### onError: pipename 317 | 318 | In case of error, redirect the flow to the specified pipe. 319 | 320 | ###### parallel: [ pipenames ] 321 | 322 | Run multiple pipe flows concurrently. Generally not used directly (use shorthand syntax on pipe). 323 | 324 | ###### parse: json 325 | 326 | Parses a String. Currently input must be 'json'. 327 | 328 | ###### path: path 329 | 330 | Selects an element from an object by dot-delimited keys. 331 | 332 | ###### pick: key | [ keys ] 333 | 334 | Selects only the specified key or keys from an object. 335 | 336 | ###### render: string | @filename 337 | 338 | Render the object using a mustache template specified as the string or loaded from the filename in the user view 339 | directory. 340 | 341 | ###### values 342 | 343 | Select the values of an object as an array. 344 | 345 | ##### Callout Fittings 346 | 347 | ###### http 348 | 349 | Make a call to a URL. 350 | 351 | config keys: 352 | 353 | * baseUrl (optional: default = '') 354 | 355 | input keys: 356 | 357 | * url (optional: default = context.output) 358 | * method (optional: default = get) (get, post, put, delete, patch, etc.) 359 | * params (optional) key/value pairs 360 | * headers (optional) key/value pairs 361 | * baseUrl (optional) overrides config.baseUrl 362 | 363 | output: 364 | 365 | { 366 | status: statusCode 367 | headers: JSON string 368 | body: JSON string 369 | } 370 | 371 | 372 | #### User Defined Fittings 373 | 374 | The user fitting is a custom function you can write and place in the fittings directory. It requires the following 375 | values: 376 | 377 | * **name**: the javascript module name in the 'fittings' folder 378 | 379 | ``` 380 | exampleUserFitting: 381 | name: customizeResponse 382 | ``` 383 | 384 | Javascript implementation: 385 | 386 | A user fitting is a fitting defined in the user's fittings directory. It exposes a creation function that accepts a 387 | fittingDefinition and the swagger-pipes configuration. This function is executed during parsing. Thus, it should access 388 | the fittingDef.config (if any) and create any static resources at this time. 389 | 390 | The creation function returns an execution function that will called during pipe flows. This function accepts a 391 | context object and a standard javascript asynchronous callback. When executed, this function should perform its 392 | intended function and then call the callback function with (error, response) when complete. 393 | 394 | Here's an example fitting that will query Yelp for businesses near a location with an input of 395 | { latitude: n, longitude: n }: 396 | 397 | ```js 398 | var Yelp = require('yelp'); 399 | var util = require('util'); 400 | 401 | module.exports = function create(fittingDef, bagpipes) { 402 | 403 | var yelp = Yelp.createClient(fittingDef.config); 404 | 405 | return function yelp_search(context, cb) { 406 | 407 | var input = context.input; 408 | 409 | var options = { 410 | term: input.term, 411 | ll: util.format('%s,%s', input.latitude, input.longitude) 412 | }; 413 | 414 | yelp.search(options, function(error, data) { 415 | 416 | if (error) { return cb(error); } 417 | if (data.error) { return cb(data.error); } 418 | 419 | cb(null, data.businesses); 420 | }); 421 | } 422 | }; 423 | ``` 424 | 425 | #### Swagger fittings 426 | 427 | ** Experimental ** 428 | 429 | You can access Swagger APIs by simply loading that Swagger. A Swagger fitting expects this: 430 | 431 | * **type**: 'swagger' 432 | * **url**: url to the swagger definition 433 | 434 | ```yaml 435 | exampleSwaggerFitting: 436 | type: swagger 437 | url: http://petstore.swagger.io/v2/swagger.json 438 | ``` 439 | 440 | #### Node-machine fittings 441 | 442 | ** Experimental ** 443 | 444 | A node-machine is a self-documenting component format that we've adapted to the a127 (see [http://node-machine.org]()). 445 | You can use a node-machine just by using 'npm install' and declaring the fitting. The fitting definition expects a 446 | minimum of: 447 | 448 | * **type**: 'node-machine' 449 | * **machinepack**: the name of the machinepack 450 | * **machine**: the function name (or id) of the machine 451 | 452 | ```yaml 453 | exampleNodeMachineFitting: 454 | type: node-machine 455 | machinepack: machinepack-github 456 | machine: list-repos 457 | ``` 458 | 459 | #### Connect-middleware fittings 460 | 461 | ** Experimental ** 462 | 463 | Connect-middleware fittings are special-purpose fittings provided as a convenience if you want to call out to any 464 | connect middleware that you have. Before calling a connect-middleware fitting, you must set a `request` and `response` 465 | property on context with appropriate values from your request chain. These will be passed to the associated connect 466 | middleware. Also, you must have passed in to the bagpipes configuration a value for connectMiddlewareDirs. 467 | Be aware, however, as these controllers almost certainly interact directly with the response and aren't designed for 468 | use within the Bagpipes system, either appropriately wrap the response object or use this option with caution. 469 | 470 | * **type**: 'connect-middleware' 471 | * **module**: the name of the file or module to load from your middleware directory 472 | * **function**: the exported function to call on the middleware 473 | 474 | ```yaml 475 | exampleMiddlewareFitting: 476 | type: connect-middleware 477 | module: my_module 478 | function: someFunction 479 | ``` 480 | 481 | ## Debugging 482 | 483 | Currently, debugging is limited to reading log entries and the debugger. However, there is a lot of information 484 | available to you by enabling the DEBUG log. By enabling the DEBUG=pipes log, you will be able to see the entire 485 | flow of the swagger-pipes system sent to the console: 486 | 487 | DEBUG=pipes 488 | 489 | You can get more debug information from the fittings with: 490 | 491 | DEBUG=pipes:fittings 492 | 493 | You can also emit the actual output from each step by enabling pipes:content: 494 | 495 | DEBUG=pipes:content 496 | 497 | Finally, you can enable all the pipes debugging by using a wildcard: 498 | 499 | DEBUG=pipes* 500 | 501 | ## Change Log 502 | 503 | 504 | 505 | Enjoy! 506 | --------------------------------------------------------------------------------