<commit_before>/* global require */

var os = require('os');
var fs = require('fs');
var _ = require('lodash');
var through = require('through');
var mdd = require('mddata');

var __debug = function (msg) {
	// console.log('# ' + msg);
};

var isText = _.isString;
var isNode = function (x) { return x && isText(x.name); };
var isArray = _.isArray;
var isContainer = function (x) { return x && isArray(x.children); };
var isNodeWithChildren = function (x) { return x && isNode(x) && isContainer(x); };

var formatData = function (data) {
	'use strict';
	if (isText(data)) {
		return 'Text(' + data + ')';
	} else if (isArray(data)) {
		return 'Array (' + _.size(data) + ')';
	} else if (isNodeWithChildren(data)) {
		return 'Node(' + data.name + ', ' + _.size(data) + ')';
	} else if (isNode(data)) {
		return 'Node(' + data.name + ')';
	} else {
		return 'Unknown';
	}
};

var parsePath = function (path) {
	'use strict';
	if (typeof(path) == 'string') {
		return path.split("/");
	} else {
		return path;
	}
};

var buildCriterium = function (pathPart) {
	'use strict';
	var typTest = function () { return true; };
	var separator = pathPart.indexOf(':');
	if (separator > -1) {
		var typ = pathPart.slice(0, separator);
		typTest = function (t) { return typ === '' || t === typ; };
		pathPart = pathPart.slice(separator + 1);
	}

	var regex = new RegExp(
		'^' + _.escapeRegExp(pathPart).
		replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$');
	return function (x) {
		if (isText(x)) {
			return regex.test(x);
		} else if (isNode(x)) {
			return typTest(x.typ) && regex.test(x.name);
		} else {
			return false;
		}
	};
};

var formatResult = function (selection) {
	'use strict';
	if (!selection) {
		return null;
	}
	var path = selection[0];
	var node = selection[1];
	if (isText(node)) {
		return { name: node, path: path };
	} else if (isNode(node)) {
		var result = _.omit(node, 'children');
		result.path = path;
		return result;
	} else {
		return null;
	}
};

var extendResultList = function (resultList, additionals) {
	'use strict';
	function arraysEqual(a, b) {
		if (a === b) return true;
		if (a === null || b === null) return false;
		if (a.length != b.length) return false;
		for (var i = 0; i < a.length; ++i) {
			if (a[i] !== b[i]) return false;
		}
		return true;
	}
	_.forEach(additionals, function (x) {
		if (_.every(resultList, function (y) {
			return !arraysEqual(x.path, y.path);
		})) {
			resultList.push(x);
		}
	});
};

var resolvePath = function (coll, path) {
	'use strict';
	if (_.size(path) === 0) {
		return null;
	}
	var n = { children: coll };
	_.forEach(path, function (i) {
		n = n.children[i];
	});
	if (isText(n)) {
		n = { name: n };
	}
	return n;
};

var globPath = function globPath(coll, queryPath, absolute, path, result) {
	'use strict';
	if (isNodeWithChildren(coll)) {
		coll = coll.children;
	}
	if (!isArray(coll)) {
		__debug('GLOB ERROR Invalid input: ' + coll);
		return null;
	}
	__debug('GLOB ' + formatData(coll));
	__debug('GLOB Path (absolute=' + absolute + '): ' + queryPath);
	path = path || [];
	result = result || [];
	var criterium = buildCriterium(queryPath[0]);
	var selections = [];
	_.forEach(coll, function (n, i) {
		if (criterium(n)) {
			__debug('GLOB Select ' + i + ': ' + formatData(n));
			selections.push([path.concat([i]), n]);
		}
	});
	if (_.size(queryPath) == 1) {
		extendResultList(result, _.map(selections, formatResult));
	} else if (_.size(queryPath) > 1) {
		_.forEach(selections, function (s) {
			var p = s[0];
			var e = s[1];
			if (e.children) {
				result = globPath(e.children, _.drop(queryPath),
					absolute, p, result);
			}
		});
	}
	if (!absolute) {
		_.forEach(coll, function (n, i) {
			if (isNodeWithChildren(n)) {
				result = globPath(n.children, queryPath, false,
					path.concat([i]), result);
			}
		});
	}
	return result;
};

var findNodes = function (coll, selector) {
	'use strict';
	var queryPath = parsePath(selector);
	if (queryPath.size === 0) {
		return null;
	}
	var absolute = queryPath[0] === '';
	if (absolute) {
		queryPath = _.drop(queryPath, 1);
	}
	return globPath(coll, queryPath, absolute);
};

var firstMatchingChild = function (node, criterium) {
	'use strict';
	var nodesWithIndex = _.map(
		node.children,
		function (n, i) { return [i, n]; });
	var matchingNodesWithIndex = _.filter(
		nodesWithIndex,
		function (r) { return criterium(r[1]); });
	return _.first(matchingNodesWithIndex);
};

var findRelativeNode = function (coll, refPath, selector) {
	'use strict';
	var queryPath = parsePath(selector);
	var n = resolvePath(coll, refPath);
	__debug('FIND RELATIVE ROOT ' + formatData(n));
	_.forEach(queryPath, function (q) {
		__debug('FIND RELATIVE POS ' + refPath + ': ' + q);
		if (q === '' || q === '.') {
			__debug('FIND RELATIVE skip');
			return;
		} else if (q === '..') {
			__debug('FIND RELATIVE parent');
			refPath = _.dropRight(refPath);
			n = resolvePath(coll, refPath);
		} else {
			var r = firstMatchingChild(n, buildCriterium(q));
			if (r) {
				__debug('FIND RELATIVE children: ' + q + ' -> ' + r[1].name);
				n = r[1];
				refPath.push(r[0]);
			} else {
				__debug('FIND RELATIVE children: ' + q + ' NOT FOUND');
			}
		}
	});
	return n;
};

var cellFromNode = function(node, attribute) {
	'use strict';
	if (!node) {
		return null;
	}
	if (attribute === 'name') {
		var cell = { text: node.name };
		if (node.id) {
			cell.href = '#' + node.id;
		}
		return cell;
	}
	if (attribute === 'value') {
		return { text: node.value ? node.value : null };
	}
	return { text: 'unknown attribute: ' + attribute };
};

var table = function (data, query) {
	'use strict';
	var nodes = findNodes(data, query.selector);
	if (isArray(query.columns)) {
		return {
			columns: _.map(query.columns, function (col) {
				return { text: col.name };
			}),
			rows: _.map(nodes, function (node) {
				return _.map(query.columns, function (col) {
					var cellNode = findRelativeNode(
						data, node.path, col.selector);
					return cellFromNode(cellNode, col.attribute);
				});
			})
		};
	} else {
		if (_.every(nodes, function (n) { return n.value === undefined; })) {
			return {
				columns: [{ text: 'Name' }],
				rows: _.map(nodes, function (n) {
					var cell = { text: n.name };
					if (n.id) {
						cell.href = '#' + n.id;
					}
					return [cell];
				})
			};
		} else {
			return {
				columns: [
					{ text: 'Name' },
					{ text: 'Value' }
				],
				rows: _.map(nodes, function (n) {
					var nameCell = { text: n.name };
					if (n.id) {
						nameCell.href = '#' + n.id;
					}
					var valueCell = { text: n.value ? n.value : null };
					return [nameCell, valueCell];
				})
			};
		}
	}
};

var formatMarkdownCell = function (cell) {
	'use strict';
	if (!cell) {
		return '';
	}
	if (cell.href) {
		return '[' + cell.text + '](' + cell.href + ')';
	}
	return cell.text;
};

var formatMarkdownList = function (table) {
	'use strict';
	var md = '';
	var rowCnt = _.size(table.rows);
	if (_.size(table.columns) == 1) {
		_.forEach(table.rows, function (row, i) {
			md += "* " + formatMarkdownCell(row[0]);
			if (i < rowCnt - 1) {
				md += os.EOL;
			}
		});
	} else if (_.size(table.columns) > 1) {
		_.forEach(table.rows, function (row, i) {
			md += "* " + formatMarkdownCell(row[0]);
			var value = formatMarkdownCell(row[1]);
			if (value) {
				md += ": " + value;
			}
			if (i < rowCnt - 1) {
				md += os.EOL;
			}
		});
	}
	return md;
};

var formatMarkdownTable = function (table) {
	'use strict';
	var md = '| ' +
		_.map(table.columns,
			function (column) {
				return column.text;
			})
			.join(' | ') + ' |' + os.EOL;
	md += '|' +
		_.map(table.columns,
			function (column) {
				return _.repeat('-', _.size(column.text) + 2);
			})
			.join('|') + '|' + os.EOL;
	var rowCnt = _.size(table.rows);
	_.forEach(table.rows, function (cells, i) {
		md += '| ' +
			_.map(cells, formatMarkdownCell)
				.join(' | ') + ' |';
		if (i < rowCnt - 1) {
			md += os.EOL;
		}
	});
	return md;
};

var transformText = function (text) {
	'use strict';
	var parseColumns = function (spec) {
		var lines = _.map(spec.split("\n"),
			function (l) { return _.trim(l); });
		var colRegex = /^#column\s+([^:]+?)\s*:\s+([^\s\(]+)\((.*?)\)$/;
		var columns = _.map(lines, function (l) {
			var m = colRegex.exec(l);
			if (m) {
				return { name: m[1], attribute: m[2], selector: m[3] };
			} else {
				return null;
			}
		});
		return _.filter(columns);
	};
	var data = mdd(text);
	text = text.replace(
		/<!--\s+#data-list\s+([^\n]*?)\s+-->/gm,
		function (m, s) {
			return formatMarkdownList(table(data, { selector: s }));
		});
	text = text.replace(
		/<!--\s+#data-table\s+([^\n]*?)\s+-->/gm,
		function (m, s) {
			return formatMarkdownTable(table(data, { selector: s }));
		});
	text = text.replace(
		/<!--\s+#data-table\s+([^\n]*?)\s*\n\s*([\s\S]*?)\s+-->/gm,
		function (m, s, c) {
			return formatMarkdownTable(table(data,
				{ selector: s, columns: parseColumns(c) }));
		});
	text = text.replace(
		/<!--\s+#data-list\s+([^\n]*?)\s*\n\s*([\s\S]*?)\s+-->/gm,
		function (m, s, c) {
			return formatMarkdownList(table(data,
				{ selector: s, columns: parseColumns(c) }));
		});
	return text;
};

var transformBuffer = function (buffer) {
	'use strict';
	return new Buffer(transformText(buffer.toString('utf8')), 'utf8');
};

var transform = function (fileOrText) {
	'use strict';

	if (fileOrText) {
		if (typeof(fileOrText) === 'string') {
			// transform(text) -> returns the transformed content of the text
			return transformText(fileOrText);
		} else {
			throw new TypeError('Invalid argument, expects a string.');
		}
	}

	// transform() -> gulp transformation step
	return through.obj(function (file, enc, cb) {
		if (file.isNull()) {
			this.push(file);
			cb();
			return;
		}
		if (file.isBuffer()) {
			file.contents = transformBuffer(file.contents, path.dirname(file.path));
			this.push(file);
			cb();
			return;
		}
		if (file.isStream()) {
			throw 'Streams are not supported.';
		}
	});
};

module.exports = {
	select: findNodes,
	query: table,
	formatMarkdownTable: formatMarkdownTable,
	formatMarkdownList: formatMarkdownList,
	transform: transform,
};
<commit_msg>fixed missing requirement again :-(<commit_after>/* global require */

var os = require('os');
var fs = require('fs');
var _ = require('lodash');
var through = require('through2');
var mdd = require('mddata');

var __debug = function (msg) {
	// console.log('# ' + msg);
};

var isText = _.isString;
var isNode = function (x) { return x && isText(x.name); };
var isArray = _.isArray;
var isContainer = function (x) { return x && isArray(x.children); };
var isNodeWithChildren = function (x) { return x && isNode(x) && isContainer(x); };

var formatData = function (data) {
	'use strict';
	if (isText(data)) {
		return 'Text(' + data + ')';
	} else if (isArray(data)) {
		return 'Array (' + _.size(data) + ')';
	} else if (isNodeWithChildren(data)) {
		return 'Node(' + data.name + ', ' + _.size(data) + ')';
	} else if (isNode(data)) {
		return 'Node(' + data.name + ')';
	} else {
		return 'Unknown';
	}
};

var parsePath = function (path) {
	'use strict';
	if (typeof(path) == 'string') {
		return path.split("/");
	} else {
		return path;
	}
};

var buildCriterium = function (pathPart) {
	'use strict';
	var typTest = function () { return true; };
	var separator = pathPart.indexOf(':');
	if (separator > -1) {
		var typ = pathPart.slice(0, separator);
		typTest = function (t) { return typ === '' || t === typ; };
		pathPart = pathPart.slice(separator + 1);
	}

	var regex = new RegExp(
		'^' + _.escapeRegExp(pathPart).
		replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$');
	return function (x) {
		if (isText(x)) {
			return regex.test(x);
		} else if (isNode(x)) {
			return typTest(x.typ) && regex.test(x.name);
		} else {
			return false;
		}
	};
};

var formatResult = function (selection) {
	'use strict';
	if (!selection) {
		return null;
	}
	var path = selection[0];
	var node = selection[1];
	if (isText(node)) {
		return { name: node, path: path };
	} else if (isNode(node)) {
		var result = _.omit(node, 'children');
		result.path = path;
		return result;
	} else {
		return null;
	}
};

var extendResultList = function (resultList, additionals) {
	'use strict';
	function arraysEqual(a, b) {
		if (a === b) return true;
		if (a === null || b === null) return false;
		if (a.length != b.length) return false;
		for (var i = 0; i < a.length; ++i) {
			if (a[i] !== b[i]) return false;
		}
		return true;
	}
	_.forEach(additionals, function (x) {
		if (_.every(resultList, function (y) {
			return !arraysEqual(x.path, y.path);
		})) {
			resultList.push(x);
		}
	});
};

var resolvePath = function (coll, path) {
	'use strict';
	if (_.size(path) === 0) {
		return null;
	}
	var n = { children: coll };
	_.forEach(path, function (i) {
		n = n.children[i];
	});
	if (isText(n)) {
		n = { name: n };
	}
	return n;
};

var globPath = function globPath(coll, queryPath, absolute, path, result) {
	'use strict';
	if (isNodeWithChildren(coll)) {
		coll = coll.children;
	}
	if (!isArray(coll)) {
		__debug('GLOB ERROR Invalid input: ' + coll);
		return null;
	}
	__debug('GLOB ' + formatData(coll));
	__debug('GLOB Path (absolute=' + absolute + '): ' + queryPath);
	path = path || [];
	result = result || [];
	var criterium = buildCriterium(queryPath[0]);
	var selections = [];
	_.forEach(coll, function (n, i) {
		if (criterium(n)) {
			__debug('GLOB Select ' + i + ': ' + formatData(n));
			selections.push([path.concat([i]), n]);
		}
	});
	if (_.size(queryPath) == 1) {
		extendResultList(result, _.map(selections, formatResult));
	} else if (_.size(queryPath) > 1) {
		_.forEach(selections, function (s) {
			var p = s[0];
			var e = s[1];
			if (e.children) {
				result = globPath(e.children, _.drop(queryPath),
					absolute, p, result);
			}
		});
	}
	if (!absolute) {
		_.forEach(coll, function (n, i) {
			if (isNodeWithChildren(n)) {
				result = globPath(n.children, queryPath, false,
					path.concat([i]), result);
			}
		});
	}
	return result;
};

var findNodes = function (coll, selector) {
	'use strict';
	var queryPath = parsePath(selector);
	if (queryPath.size === 0) {
		return null;
	}
	var absolute = queryPath[0] === '';
	if (absolute) {
		queryPath = _.drop(queryPath, 1);
	}
	return globPath(coll, queryPath, absolute);
};

var firstMatchingChild = function (node, criterium) {
	'use strict';
	var nodesWithIndex = _.map(
		node.children,
		function (n, i) { return [i, n]; });
	var matchingNodesWithIndex = _.filter(
		nodesWithIndex,
		function (r) { return criterium(r[1]); });
	return _.first(matchingNodesWithIndex);
};

var findRelativeNode = function (coll, refPath, selector) {
	'use strict';
	var queryPath = parsePath(selector);
	var n = resolvePath(coll, refPath);
	__debug('FIND RELATIVE ROOT ' + formatData(n));
	_.forEach(queryPath, function (q) {
		__debug('FIND RELATIVE POS ' + refPath + ': ' + q);
		if (q === '' || q === '.') {
			__debug('FIND RELATIVE skip');
			return;
		} else if (q === '..') {
			__debug('FIND RELATIVE parent');
			refPath = _.dropRight(refPath);
			n = resolvePath(coll, refPath);
		} else {
			var r = firstMatchingChild(n, buildCriterium(q));
			if (r) {
				__debug('FIND RELATIVE children: ' + q + ' -> ' + r[1].name);
				n = r[1];
				refPath.push(r[0]);
			} else {
				__debug('FIND RELATIVE children: ' + q + ' NOT FOUND');
			}
		}
	});
	return n;
};

var cellFromNode = function(node, attribute) {
	'use strict';
	if (!node) {
		return null;
	}
	if (attribute === 'name') {
		var cell = { text: node.name };
		if (node.id) {
			cell.href = '#' + node.id;
		}
		return cell;
	}
	if (attribute === 'value') {
		return { text: node.value ? node.value : null };
	}
	return { text: 'unknown attribute: ' + attribute };
};

var table = function (data, query) {
	'use strict';
	var nodes = findNodes(data, query.selector);
	if (isArray(query.columns)) {
		return {
			columns: _.map(query.columns, function (col) {
				return { text: col.name };
			}),
			rows: _.map(nodes, function (node) {
				return _.map(query.columns, function (col) {
					var cellNode = findRelativeNode(
						data, node.path, col.selector);
					return cellFromNode(cellNode, col.attribute);
				});
			})
		};
	} else {
		if (_.every(nodes, function (n) { return n.value === undefined; })) {
			return {
				columns: [{ text: 'Name' }],
				rows: _.map(nodes, function (n) {
					var cell = { text: n.name };
					if (n.id) {
						cell.href = '#' + n.id;
					}
					return [cell];
				})
			};
		} else {
			return {
				columns: [
					{ text: 'Name' },
					{ text: 'Value' }
				],
				rows: _.map(nodes, function (n) {
					var nameCell = { text: n.name };
					if (n.id) {
						nameCell.href = '#' + n.id;
					}
					var valueCell = { text: n.value ? n.value : null };
					return [nameCell, valueCell];
				})
			};
		}
	}
};

var formatMarkdownCell = function (cell) {
	'use strict';
	if (!cell) {
		return '';
	}
	if (cell.href) {
		return '[' + cell.text + '](' + cell.href + ')';
	}
	return cell.text;
};

var formatMarkdownList = function (table) {
	'use strict';
	var md = '';
	var rowCnt = _.size(table.rows);
	if (_.size(table.columns) == 1) {
		_.forEach(table.rows, function (row, i) {
			md += "* " + formatMarkdownCell(row[0]);
			if (i < rowCnt - 1) {
				md += os.EOL;
			}
		});
	} else if (_.size(table.columns) > 1) {
		_.forEach(table.rows, function (row, i) {
			md += "* " + formatMarkdownCell(row[0]);
			var value = formatMarkdownCell(row[1]);
			if (value) {
				md += ": " + value;
			}
			if (i < rowCnt - 1) {
				md += os.EOL;
			}
		});
	}
	return md;
};

var formatMarkdownTable = function (table) {
	'use strict';
	var md = '| ' +
		_.map(table.columns,
			function (column) {
				return column.text;
			})
			.join(' | ') + ' |' + os.EOL;
	md += '|' +
		_.map(table.columns,
			function (column) {
				return _.repeat('-', _.size(column.text) + 2);
			})
			.join('|') + '|' + os.EOL;
	var rowCnt = _.size(table.rows);
	_.forEach(table.rows, function (cells, i) {
		md += '| ' +
			_.map(cells, formatMarkdownCell)
				.join(' | ') + ' |';
		if (i < rowCnt - 1) {
			md += os.EOL;
		}
	});
	return md;
};

var transformText = function (text) {
	'use strict';
	var parseColumns = function (spec) {
		var lines = _.map(spec.split("\n"),
			function (l) { return _.trim(l); });
		var colRegex = /^#column\s+([^:]+?)\s*:\s+([^\s\(]+)\((.*?)\)$/;
		var columns = _.map(lines, function (l) {
			var m = colRegex.exec(l);
			if (m) {
				return { name: m[1], attribute: m[2], selector: m[3] };
			} else {
				return null;
			}
		});
		return _.filter(columns);
	};
	var data = mdd(text);
	text = text.replace(
		/<!--\s+#data-list\s+([^\n]*?)\s+-->/gm,
		function (m, s) {
			return formatMarkdownList(table(data, { selector: s }));
		});
	text = text.replace(
		/<!--\s+#data-table\s+([^\n]*?)\s+-->/gm,
		function (m, s) {
			return formatMarkdownTable(table(data, { selector: s }));
		});
	text = text.replace(
		/<!--\s+#data-table\s+([^\n]*?)\s*\n\s*([\s\S]*?)\s+-->/gm,
		function (m, s, c) {
			return formatMarkdownTable(table(data,
				{ selector: s, columns: parseColumns(c) }));
		});
	text = text.replace(
		/<!--\s+#data-list\s+([^\n]*?)\s*\n\s*([\s\S]*?)\s+-->/gm,
		function (m, s, c) {
			return formatMarkdownList(table(data,
				{ selector: s, columns: parseColumns(c) }));
		});
	return text;
};

var transformBuffer = function (buffer) {
	'use strict';
	return new Buffer(transformText(buffer.toString('utf8')), 'utf8');
};

var transform = function (fileOrText) {
	'use strict';

	if (fileOrText) {
		if (typeof(fileOrText) === 'string') {
			// transform(text) -> returns the transformed content of the text
			return transformText(fileOrText);
		} else {
			throw new TypeError('Invalid argument, expects a string.');
		}
	}

	// transform() -> gulp transformation step
	return through.obj(function (file, enc, cb) {
		if (file.isNull()) {
			this.push(file);
			cb();
			return;
		}
		if (file.isBuffer()) {
			file.contents = transformBuffer(file.contents, path.dirname(file.path));
			this.push(file);
			cb();
			return;
		}
		if (file.isStream()) {
			throw 'Streams are not supported.';
		}
	});
};

module.exports = {
	select: findNodes,
	query: table,
	formatMarkdownTable: formatMarkdownTable,
	formatMarkdownList: formatMarkdownList,
	transform: transform,
};
<|endoftext|>


<commit_before>#!/usr/bin/env node

'use strict';

const yargs = require('yargs');
const fs = require('fs');
const path = require('path');

const k8s = require('auto-kubernetes-client');

const logger = require('log4js').getLogger();

const argv = yargs
	.alias('s', 'server').describe('server', 'The address and port of the Kubernetes API server')
	.alias('cacert', 'certificate-authority').describe('certificate-authority', 'Path to a cert. file for the certificate authority')
	.alias('cert', 'client-certificate').describe('client-certificate', 'Path to a client certificate file for TLS')
	.alias('key', 'client-key').describe('client-key', 'Path to a client key file for TLS')
	.boolean('insecure-skip-tls-verify').describe('insecure-skip-tls-verify', 'If true, the server\'s certificate will not be checked for validity. This will make your HTTPS connections insecure')
	.describe('token', 'Bearer token for authentication to the API server')
	.describe('namespace', 'The namespace to watch').demandOption('namespace')
	.help()
	.argv;

/**
 * Creates basic configuration for accessing the Kubernetes API server
 *
 * @param {Object} argv Command line arguments
 * @returns {Object} Kubernetes client configuration
 */
function createK8sConfig(argv) {
	let k8sConfig;
	if (argv.server) {
		// For local development
		k8sConfig = {
			url: argv.server,
			insecureSkipTlsVerify: argv.insecureSkipTlsVerify
		};
		if (argv.certificateAuthority) {
			k8sConfig.ca = fs.readFileSync(argv.certificateAuthority, 'utf8');
		}
		if (argv.token) {
			k8sConfig.auth = { bearer: argv.token };
		} else if (argv.username && argv.password) {
			k8sConfig.auth = { user: argv.username, pass: argv.password };
		} else if (argv.clientCertificate && argv.clientKey) {
			k8sConfig.cert = fs.readFileSync(argv.clientCertificate, 'utf8');
			k8sConfig.key = fs.readFileSync(argv.clientKey, 'utf8');
		}
	} else if (process.env.KUBERNETES_SERVICE_HOST) {
		// Runs in Kubernetes
		const credentialsPath = '/var/run/secrets/kubernetes.io/serviceaccount/';
		k8sConfig = {
			url: `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT}`,
			ca: fs.readFileSync(path.resolve(credentialsPath, 'ca.crt'), 'utf8'),
			auth: { bearer: fs.readFileSync(path.resolve(credentialsPath, 'token'), 'utf8') }
		}
	} else {
		throw new Error('Unknown Kubernetes API server');
	}

	return k8sConfig;
}

const sqsClientOptions = {
	endpoint: process.env.AWS_SQS_ENDPOINT_URL_OVERRIDE,
	region: process.env.AWS_REGION,
};

const SQSQueue = require('./resources/sqs');
const PromisesQueue = require('./promises-queue');

const k8sConfig = createK8sConfig(argv);
k8s(k8sConfig).then(function onConnected(k8sClient) {
	const awsResourcesClient = k8sClient.group('aws.k8s.collaborne.com', 'v1').ns(argv.namespace);

	const resourceDescriptions = [{
		type: 'queues',
		resourceClient: new SQSQueue(sqsClientOptions)
	}];

	function resourceLoop(type, k8sResourceClient, resourceClient, promisesQueue) {
		k8sResourceClient.list()
			.then(list => {
				const resourceVersion = list.metadata.resourceVersion;

				// Treat all resources we see as "update", which will trigger a creation/update of attributes accordingly.
				for (const resource of list.items) {
					const name = resource.metadata.name;
					promisesQueue.enqueue(name, function() {
						logger.info(`[${name}]: Syncing`);
						return resourceClient.update(resource);
					}).then(function(data) {
						logger.info(`[${name}]: ${JSON.stringify(data)}`);
					}, function(err) {
						logger.error(`[${name}]: ${err.message} (${err.code})`);
					});
				}

				return resourceVersion;
			}).then(resourceVersion => {
				// Start watching the resources from that version on
				logger.info(`Watching ${type} at ${resourceVersion}...`);
				k8sResourceClient.watch(resourceVersion)
					.on('data', function(item) {
						const resource = item.object;
						const name = resource.metadata.name;

						let next;

						switch (item.type) {
						case 'ADDED':
							next = function() {
								logger.info(`[${name}]: Creating resource`);
								return resourceClient.create(resource);
							};
							break;
						case 'MODIFIED':
							next = function() {
								logger.info(`[${name}]: Updating resource attributes`);
								return resourceClient.update(resource);
							};
							break;
						case 'DELETED':
							next = function() {
								logger.info(`[${name}]: Deleting resource`);
								return resourceClient.delete(resource);
							};
							break;
						case 'ERROR':
							// Log the message, and continue: usually the stream would end now, but there might be more events
							// in it that we do want to consume.
							logger.warn(`Error while watching: ${item.object.message}, ignoring`);
							return;
						default:
							logger.warn(`Unkown watch event type ${item.type}, ignoring`);
							return;
						}

						// Note that we retain the 'rejected' state here: an existing resource that ended in a rejection
						// will effectively stay rejected.
						const result = promisesQueue.enqueue(name, next).then(function(data) {
							logger.info(`[${name}]: ${JSON.stringify(data)}`);
						}, function(err) {
							logger.error(`[${name}]: ${err.message} (${err.code})`);
						});

						return result;
					})
					.on('end', function() {
						// Restart the watch from the last known version.
						logger.info('Watch ended, restarting');
						return resourceLoop(type, k8sResourceClient, resourceClient, promisesQueue);
					});
			})
	}

	resourceDescriptions.forEach(resourceDescription => {
		const k8sResourceClient = awsResourcesClient[resourceDescription.type];
		if (k8sResourceClient) {
			const promisesQueue = new PromisesQueue();
			resourceLoop(resourceDescription.type, k8sResourceClient, resourceDescription.resourceClient, promisesQueue);
		} else {
			console.error(`Resources of type ${resourceDescription.type} are not defined as Kubernetes ThirdPartyResource. Available ThirdPartyResources ${Object.keys(awsResourcesClient)}.`);
		}
	});
}).catch(function(err) {
	logger.error(`Uncaught error, aborting: ${err.message}`);
	process.exit(1);
});
<commit_msg>Improve error handling when setting up the watches<commit_after>#!/usr/bin/env node

'use strict';

const yargs = require('yargs');
const fs = require('fs');
const path = require('path');

const k8s = require('auto-kubernetes-client');

const logger = require('log4js').getLogger();

const argv = yargs
	.alias('s', 'server').describe('server', 'The address and port of the Kubernetes API server')
	.alias('cacert', 'certificate-authority').describe('certificate-authority', 'Path to a cert. file for the certificate authority')
	.alias('cert', 'client-certificate').describe('client-certificate', 'Path to a client certificate file for TLS')
	.alias('key', 'client-key').describe('client-key', 'Path to a client key file for TLS')
	.boolean('insecure-skip-tls-verify').describe('insecure-skip-tls-verify', 'If true, the server\'s certificate will not be checked for validity. This will make your HTTPS connections insecure')
	.describe('token', 'Bearer token for authentication to the API server')
	.describe('namespace', 'The namespace to watch').demandOption('namespace')
	.help()
	.argv;

/**
 * Creates basic configuration for accessing the Kubernetes API server
 *
 * @param {Object} argv Command line arguments
 * @returns {Object} Kubernetes client configuration
 */
function createK8sConfig(argv) {
	let k8sConfig;
	if (argv.server) {
		// For local development
		k8sConfig = {
			url: argv.server,
			insecureSkipTlsVerify: argv.insecureSkipTlsVerify
		};
		if (argv.certificateAuthority) {
			k8sConfig.ca = fs.readFileSync(argv.certificateAuthority, 'utf8');
		}
		if (argv.token) {
			k8sConfig.auth = { bearer: argv.token };
		} else if (argv.username && argv.password) {
			k8sConfig.auth = { user: argv.username, pass: argv.password };
		} else if (argv.clientCertificate && argv.clientKey) {
			k8sConfig.cert = fs.readFileSync(argv.clientCertificate, 'utf8');
			k8sConfig.key = fs.readFileSync(argv.clientKey, 'utf8');
		}
	} else if (process.env.KUBERNETES_SERVICE_HOST) {
		// Runs in Kubernetes
		const credentialsPath = '/var/run/secrets/kubernetes.io/serviceaccount/';
		k8sConfig = {
			url: `https://${process.env.KUBERNETES_SERVICE_HOST}:${process.env.KUBERNETES_SERVICE_PORT}`,
			ca: fs.readFileSync(path.resolve(credentialsPath, 'ca.crt'), 'utf8'),
			auth: { bearer: fs.readFileSync(path.resolve(credentialsPath, 'token'), 'utf8') }
		}
	} else {
		throw new Error('Unknown Kubernetes API server');
	}

	return k8sConfig;
}

const sqsClientOptions = {
	endpoint: process.env.AWS_SQS_ENDPOINT_URL_OVERRIDE,
	region: process.env.AWS_REGION,
};

const SQSQueue = require('./resources/sqs');
const PromisesQueue = require('./promises-queue');

const k8sConfig = createK8sConfig(argv);
k8s(k8sConfig).then(function onConnected(k8sClient) {
	const awsResourcesClient = k8sClient.group('aws.k8s.collaborne.com', 'v1').ns(argv.namespace);

	const resourceDescriptions = [{
		type: 'queues',
		resourceClient: new SQSQueue(sqsClientOptions)
	}];

	function resourceLoop(type, k8sResourceClient, resourceClient, promisesQueue) {
		return k8sResourceClient.list()
			.then(list => {
				const resourceVersion = list.metadata.resourceVersion;

				// Treat all resources we see as "update", which will trigger a creation/update of attributes accordingly.
				for (const resource of list.items) {
					const name = resource.metadata.name;
					promisesQueue.enqueue(name, function() {
						logger.info(`[${name}]: Syncing`);
						return resourceClient.update(resource);
					}).then(function(data) {
						logger.info(`[${name}]: ${JSON.stringify(data)}`);
					}, function(err) {
						logger.error(`[${name}]: ${err.message} (${err.code})`);
					});
				}

				return resourceVersion;
			}).then(resourceVersion => {
				// Start watching the resources from that version on
				logger.info(`Watching ${type} at ${resourceVersion}...`);
				k8sResourceClient.watch(resourceVersion)
					.on('data', function(item) {
						const resource = item.object;
						const name = resource.metadata.name;

						let next;

						switch (item.type) {
						case 'ADDED':
							next = function() {
								logger.info(`[${name}]: Creating resource`);
								return resourceClient.create(resource);
							};
							break;
						case 'MODIFIED':
							next = function() {
								logger.info(`[${name}]: Updating resource attributes`);
								return resourceClient.update(resource);
							};
							break;
						case 'DELETED':
							next = function() {
								logger.info(`[${name}]: Deleting resource`);
								return resourceClient.delete(resource);
							};
							break;
						case 'ERROR':
							// Log the message, and continue: usually the stream would end now, but there might be more events
							// in it that we do want to consume.
							logger.warn(`Error while watching: ${item.object.message}, ignoring`);
							return;
						default:
							logger.warn(`Unkown watch event type ${item.type}, ignoring`);
							return;
						}

						// Note that we retain the 'rejected' state here: an existing resource that ended in a rejection
						// will effectively stay rejected.
						const result = promisesQueue.enqueue(name, next).then(function(data) {
							logger.info(`[${name}]: ${JSON.stringify(data)}`);
						}, function(err) {
							logger.error(`[${name}]: ${err.message} (${err.code})`);
						});

						return result;
					})
					.on('end', function() {
						// Restart the watch from the last known version.
						logger.info('Watch ended, restarting');
						return resourceLoop(type, k8sResourceClient, resourceClient, promisesQueue);
					});
			});
	}

	const resourceLoopPromises = resourceDescriptions.map(resourceDescription => {
		const k8sResourceClient = awsResourcesClient[resourceDescription.type];
		if (!k8sResourceClient) {
			// XXX: Is this a failure?
			logger.error(`Cannot create client for resources of type ${resourceDescription.type}: Available resources: ${Object.keys(awsResourcesClient)}.`);
			return Promise.reject(new Error(`Missing kubernetes client for ${resourceDescription.type}`));
		}

		const promisesQueue = new PromisesQueue();
		return resourceLoop(resourceDescription.type, k8sResourceClient, resourceDescription.resourceClient, promisesQueue).catch(err => {
			logger.error(`Error when monitoring resources of type ${resourceDescription.type}: ${err.message}`);
			throw err;
		});
	});

	return Promise.all(resourceLoopPromises);
}).catch(function(err) {
	logger.error(`Uncaught error, aborting: ${err.message}`);
	process.exit(1);
});
<|endoftext|>


<commit_before>import compact from 'lodash.compact'
import dropWhile from 'lodash.dropwhile'
import isObject from 'lodash.isobject'
import noopLogger from 'noop-logger'
import pick from 'lodash.pick'
import size from 'lodash.size'

const DEFAULT_HEADERS = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

/**
 * A function that accepts everything, does nothing, and returns undefined.
 * @return {void}
 */
function noop() {}

function requestFactory(superagent, PromiseLib) {
  return function(url, method, props = {}, query = {}, headers, cb) {
    const req = superagent[method](url)
      .set(headers)
      .query(query)
      .send(props)

    if (!PromiseLib) {
      return req.end((err, res) => {
        const out = {
          status: res.status,
          model: res.body
        }
        return cb(err, out)
      })
    }

    return new PromiseLib((resolve, reject) => {
      req.end((err, res) => {
        const out = {
          status: res.status,
          model: res.body
        }
        if (cb) {
          cb(err, out)
        }

        if (err) return reject(err)
        resolve(out)
      })
    })
  }
}

/**
 * Returns a function that defines the response handler for a RESTful verb (i.e., get, create, etc.)
 * @param {Object} opts the config options provided by user amended to include a valid Promise lib and request function
 * @returns {Function}
 */
function verbFactory(opts, request) {
  return function(route, method, properties, query, headers, cb) {
    const processed = function(mod = {}, finish) {
      const head = Object.assign({}, headers, (mod.headers || {}))
      const props = Object.assign({}, properties, (mod.properties || {}))
      const qry = Object.assign({}, query, (mod.query || {}))

      return request(route, method, props, qry, head, finish)
    }

    // if opts.beforeEach is not defined, we execute the default request flow
    if (!opts.beforeEach) {
      if (!cb) {
        return new opts.promise(resolve => {
          return processed({})
        })
      }

      return processed({}, cb)
    }

    const spy = function(err, mod) {
      // we specifically do not return the output of the callback because its value (or its absence) might be considered significant elsewhere within this lib.
      if (err) {
        return cb(err)
      }

      processed(mod, cb)
    }

    let out = opts.beforeEach(properties, query, headers, spy)

    // if out is undefined, we assume that the callback will be used
    if (out === undefined) {
      return
    }

    return opts.promise.resolve(out)
      .then(processed)
  }
}

/**
 * Prototype REST API client endpoint
 * @param  {string} resource name of the resource
 * @param  {Object} opts     dependencies and configuration
 * @param  {Object} [conf.request]     function for making http requests
 * @return {Object}          an object containing methods for interacting with the API
 */
function proto(resource, opts) {
  const {
    altMethodNames = {}, request, headers,
  } = opts
  const {get,
    create,
    update,
    del,
    list,
  } = altMethodNames

  const factory = verbFactory(opts, request)

  const out = {
    [create || 'create']: function(props, cb = noop) {
      const method = resource.props && resource.props.createMethod ||
        'post'
      return factory(this._route(), method, props, {}, headers, cb)
    },
    [del || 'del']: function(cb = noop) {
      return factory(this._route(), 'delete', {}, {}, headers, cb)
    },
    [del || 'delete']: function(cb = noop) {
      return factory(this._route(), 'delete', {}, {}, headers, cb)
    },
    [get || 'get']: function(cb = noop) {
      return factory(this._route(), 'get', {}, {}, headers, cb)
    },
    [list || 'list']: function(query, cb = noop) {
      return factory(this._route(), 'get', {}, query, headers, cb)
    },
    [update || 'update']: function(props, cb = noop) {
      return factory(this._route(), 'put', props, {}, headers, cb)
    },
    getUrl() {
      return this._route()
    }
  }

  return out
}

/**
 * Cleans and normalizes resource strings
 * @param  {Array.<string>} raw
 * @param {Array.<string>} [commonVersions=[]]
 * @return {Array.<Array.<string>>}
 */
function buildResources(endpoints, commonVersions) {
  /**
   * An identity map of all the versions encountered to this point
   * @type {Object.<string, string>}
   */
  const versions = Object.create(null)

  /**
   * Resources that lack a configuration object
   * @type {Array}
   */
  const simpleResources = []

  const eps = endpoints.map(resource => {
    if (!isObject(resource)) {
      const clean = compact(resource.split('/'))
      simpleResources.push(clean)
      commonVersions.forEach(cv => {
        const copy = clean.slice()
        copy.unshift(cv)
        simpleResources.push(copy)
      })
      return clean
    }

    if (resource.versions) {
      [].concat(resource.versions)
        .reduce((iden, v, k) => {
          iden[k] = v
          return iden
        }, versions)
    }

    // configured endpoints are represented similarly to simple endpoints
    // (i.e. an array of resource/subresource paths), but they additionally
    // have an array property.
    const custom = resource.resource.split('/')
    Object.assign(custom, {
      config: resource
    })
    return custom
  })

  return [simpleResources, eps, versions]
}

/**
 * Removes trailing slashes from the root url
 * @param  {string} root
 * @return {string}
 */
function cleanroot(root) {
  if (!root.endsWith('/')) {
    return root.trim()
  }

  return dropWhile(root.trim()
      .split('')
      .reverse(), char => char === '/')
    .reverse()
    .join('')
}

/**
 * Returns a function that recursively builds the rest api object hierarchy.
 *
 * The object built includes the REST resource names as functions.  Those functions have function-properties consisting of the REST methods (i.e. get, create, etc). The resourceful functions, when invoked return a new object hiearchy of their subresources (e.g, api.users(23).posts...).
 * @param  {Object} conf
 * @return {Function}
 */
function build(conf) {
  /**
   * Recursively builds a nested object of REST resource objects.
   * @param  {Array.<Array.<string>>} resources array of an array of resource/subresource paths. the arrays should be sorted such that the longest arrays are towards the beginning of the array container.
   * @param  {Object} accum     output object of REST resources
   * @return {Object} the accumulated object
   */
  return function loop(resources = {}, accum = function() {}) {
    const sz = size(resources)

    // there are no additional resources, so return the built function
    if (!sz) return accum

    const [name, ...rest] = Object.keys(resources)
    const v = resources[name]

    const fn = function(id) {
      // invoking this function creates a new object hierarchy, so we must
      // carry forward upstream route fragments
      if (this._out) return this._out

      const self = this

      // every invocation if this function returns a structurally similar
      // object, so cache it
      const out = this.out = Object.assign({
        _route() {
          // this must be a top-level route
          if (!accum._route) {
            return `${cleanroot(conf.root)}/${name}/${id}`
          }

          return `${self._route()}/${name}/${id}`
        }
      }, proto(name, conf))

      // attach subresources to the output object, and ensure their `this` is
      // the same object
      loop(v, out)

      return out
    }

    fn._route = function() {
      if (!accum._route) {
        return `${cleanroot(conf.root)}/${name}`
      }

      return `${accum._route()}/${name}`
    }

    Object.assign(fn, proto(name, conf))

    if (v !== 1) {
      // this must not be a leaf node
      loop(resources[name], fn)
    }

    accum[name] = fn

    if (rest.length) {
      const next = pick(resources, rest)
      return loop(next, accum)
    }

    return accum
  }
}

/**
 * Validates required initialization parameters provided by the user.
 * @param  {Object} config the configuration object argument provided at
 * initialization
 * @return {void}
 */
function validateConfig(config) {
  const {
    endpoints,
    root = ''
  } = config

  if (!root || !root.trim()) {
    throw new Error('api root is required')
  }

  if (!endpoints || !Array.isArray(endpoints) || !endpoints.length) {
    throw new Error('endpoints must be a non-empty array')
  }
}

/**
 * Choose between user provided dependencies and peer dependencies mentioned
 * in the README.
 * @param  {Object} config
 * @return {(Promise, request)}
 */
function chooseDependencies(config) {
  const {
    logger = noopLogger, promise, request
  } = config

  let agent = request
  let promiseLib = promise

  if (!promise) {
    try {
      promiseLib = require('es6-promise')
        .Promise
    } catch (er) {
      throw new Error(
        'expected Promise to be defined or es6-promise to be installed, but both are missing'
      )
    }
  }

  if (!request) {
    try {
      const superagent = require('superagent')
      agent = requestFactory(superagent, promiseLib)
    } catch (er) {
      throw new Error(
        'expected request function to be defined or superagent to be installed, but both are missing'
      )
    }
  }

  return [promiseLib, agent]
}

/**
 * Recursively creates or updates the object hierarchy in [map] according to the
 * string elements of [path]. former leaf nodes are updated to be internal nodes
 * if a path is followed that implies it contains a leaf node subresource.
 * @param {Object} map an object mapping resource names to either an object that
 * includes subsequent resource names, or '1' for leaf nodes.
 * @param {Array.<string>} paths an array of resource names.  subsequent array
 * elements represent subresources of the preceeding elements.
 */
function place(map, paths) {
  const [head, ...rest] = paths
  let node = map[head]

  if (rest.length) {
    if (!node || node === 1) {
      node = {}
      map[head] = node

      return place(node, rest)
    }

    return place(node, rest)
  }

  if (!node) {
    map[head] = 1
  }

  return map
}

/**
 * Converts a collection of resource paths in array format to an object hierarchy
 * explicitly encoding the hierarchy.
 *
 * @param {Array.<Array.<string>>} ls array of arrays
 * @return {Object} hierarchy of resources
 */
function normalize(ls = [
  []
]) {
  if (!ls.length) return {}

  const out = {}
  ls.forEach(xs => place(out, xs))

  return out
}

export default module.exports = (config) => {
  const { endpoints, versions = [] } = config
  const { logger = noopLogger } = config

  validateConfig(config)

  const [promiseLib, requestLib] = chooseDependencies(config)

  // we want to work with an array of strings, but versions may be either an array or a single stirng
  const commonVersions = compact([].concat(versions))

  let [simpleResources, resources, ] = buildResources(endpoints,
    commonVersions)

  // this removes empty arrays from `resources`.  empty arrays may arise from
  // a user inputing an empty string endpoint.
  let all = resources.filter(ls => ls.length)
    .slice()

  // Array.prototype.push.apply(all, versioned)
  all = all.concat(simpleResources)

  // by placing the deep resource hierarchies first, we ensure that top-level
  // resources are created with the proper fluent behavior.
  all.sort((ls, xs) => -(ls.length - xs.length))

  const opts = Object.assign({}, config, {
    request: requestLib,
    promise: promiseLib,
    logger,
  }, {
    headers: Object.assign({}, DEFAULT_HEADERS, config.headers || {})
  })

  return build(opts)(normalize(all))
}
<commit_msg>use commonjs export<commit_after>import compact from 'lodash.compact'
import dropWhile from 'lodash.dropwhile'
import isObject from 'lodash.isobject'
import noopLogger from 'noop-logger'
import pick from 'lodash.pick'
import size from 'lodash.size'

const DEFAULT_HEADERS = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

/**
 * A function that accepts everything, does nothing, and returns undefined.
 * @return {void}
 */
function noop() {}

function requestFactory(superagent, PromiseLib) {
  return function(url, method, props = {}, query = {}, headers, cb) {
    const req = superagent[method](url)
      .set(headers)
      .query(query)
      .send(props)

    if (!PromiseLib) {
      return req.end((err, res) => {
        const out = {
          status: res.status,
          model: res.body
        }
        return cb(err, out)
      })
    }

    return new PromiseLib((resolve, reject) => {
      req.end((err, res) => {
        const out = {
          status: res.status,
          model: res.body
        }
        if (cb) {
          cb(err, out)
        }

        if (err) return reject(err)
        resolve(out)
      })
    })
  }
}

/**
 * Returns a function that defines the response handler for a RESTful verb (i.e., get, create, etc.)
 * @param {Object} opts the config options provided by user amended to include a valid Promise lib and request function
 * @returns {Function}
 */
function verbFactory(opts, request) {
  return function(route, method, properties, query, headers, cb) {
    const processed = function(mod = {}, finish) {
      const head = Object.assign({}, headers, (mod.headers || {}))
      const props = Object.assign({}, properties, (mod.properties || {}))
      const qry = Object.assign({}, query, (mod.query || {}))

      return request(route, method, props, qry, head, finish)
    }

    // if opts.beforeEach is not defined, we execute the default request flow
    if (!opts.beforeEach) {
      if (!cb) {
        return new opts.promise(resolve => {
          return processed({})
        })
      }

      return processed({}, cb)
    }

    const spy = function(err, mod) {
      // we specifically do not return the output of the callback because its value (or its absence) might be considered significant elsewhere within this lib.
      if (err) {
        return cb(err)
      }

      processed(mod, cb)
    }

    let out = opts.beforeEach(properties, query, headers, spy)

    // if out is undefined, we assume that the callback will be used
    if (out === undefined) {
      return
    }

    return opts.promise.resolve(out)
      .then(processed)
  }
}

/**
 * Prototype REST API client endpoint
 * @param  {string} resource name of the resource
 * @param  {Object} opts     dependencies and configuration
 * @param  {Object} [conf.request]     function for making http requests
 * @return {Object}          an object containing methods for interacting with the API
 */
function proto(resource, opts) {
  const {
    altMethodNames = {}, request, headers,
  } = opts
  const {get,
    create,
    update,
    del,
    list,
  } = altMethodNames

  const factory = verbFactory(opts, request)

  const out = {
    [create || 'create']: function(props, cb = noop) {
      const method = resource.props && resource.props.createMethod ||
        'post'
      return factory(this._route(), method, props, {}, headers, cb)
    },
    [del || 'del']: function(cb = noop) {
      return factory(this._route(), 'delete', {}, {}, headers, cb)
    },
    [del || 'delete']: function(cb = noop) {
      return factory(this._route(), 'delete', {}, {}, headers, cb)
    },
    [get || 'get']: function(cb = noop) {
      return factory(this._route(), 'get', {}, {}, headers, cb)
    },
    [list || 'list']: function(query, cb = noop) {
      return factory(this._route(), 'get', {}, query, headers, cb)
    },
    [update || 'update']: function(props, cb = noop) {
      return factory(this._route(), 'put', props, {}, headers, cb)
    },
    getUrl() {
      return this._route()
    }
  }

  return out
}

/**
 * Cleans and normalizes resource strings
 * @param  {Array.<string>} raw
 * @param {Array.<string>} [commonVersions=[]]
 * @return {Array.<Array.<string>>}
 */
function buildResources(endpoints, commonVersions) {
  /**
   * An identity map of all the versions encountered to this point
   * @type {Object.<string, string>}
   */
  const versions = Object.create(null)

  /**
   * Resources that lack a configuration object
   * @type {Array}
   */
  const simpleResources = []

  const eps = endpoints.map(resource => {
    if (!isObject(resource)) {
      const clean = compact(resource.split('/'))
      simpleResources.push(clean)
      commonVersions.forEach(cv => {
        const copy = clean.slice()
        copy.unshift(cv)
        simpleResources.push(copy)
      })
      return clean
    }

    if (resource.versions) {
      [].concat(resource.versions)
        .reduce((iden, v, k) => {
          iden[k] = v
          return iden
        }, versions)
    }

    // configured endpoints are represented similarly to simple endpoints
    // (i.e. an array of resource/subresource paths), but they additionally
    // have an array property.
    const custom = resource.resource.split('/')
    Object.assign(custom, {
      config: resource
    })
    return custom
  })

  return [simpleResources, eps, versions]
}

/**
 * Removes trailing slashes from the root url
 * @param  {string} root
 * @return {string}
 */
function cleanroot(root) {
  if (!root.endsWith('/')) {
    return root.trim()
  }

  return dropWhile(root.trim()
      .split('')
      .reverse(), char => char === '/')
    .reverse()
    .join('')
}

/**
 * Returns a function that recursively builds the rest api object hierarchy.
 *
 * The object built includes the REST resource names as functions.  Those functions have function-properties consisting of the REST methods (i.e. get, create, etc). The resourceful functions, when invoked return a new object hiearchy of their subresources (e.g, api.users(23).posts...).
 * @param  {Object} conf
 * @return {Function}
 */
function build(conf) {
  /**
   * Recursively builds a nested object of REST resource objects.
   * @param  {Array.<Array.<string>>} resources array of an array of resource/subresource paths. the arrays should be sorted such that the longest arrays are towards the beginning of the array container.
   * @param  {Object} accum     output object of REST resources
   * @return {Object} the accumulated object
   */
  return function loop(resources = {}, accum = function() {}) {
    const sz = size(resources)

    // there are no additional resources, so return the built function
    if (!sz) return accum

    const [name, ...rest] = Object.keys(resources)
    const v = resources[name]

    const fn = function(id) {
      // invoking this function creates a new object hierarchy, so we must
      // carry forward upstream route fragments
      if (this._out) return this._out

      const self = this

      // every invocation if this function returns a structurally similar
      // object, so cache it
      const out = this.out = Object.assign({
        _route() {
          // this must be a top-level route
          if (!accum._route) {
            return `${cleanroot(conf.root)}/${name}/${id}`
          }

          return `${self._route()}/${name}/${id}`
        }
      }, proto(name, conf))

      // attach subresources to the output object, and ensure their `this` is
      // the same object
      loop(v, out)

      return out
    }

    fn._route = function() {
      if (!accum._route) {
        return `${cleanroot(conf.root)}/${name}`
      }

      return `${accum._route()}/${name}`
    }

    Object.assign(fn, proto(name, conf))

    if (v !== 1) {
      // this must not be a leaf node
      loop(resources[name], fn)
    }

    accum[name] = fn

    if (rest.length) {
      const next = pick(resources, rest)
      return loop(next, accum)
    }

    return accum
  }
}

/**
 * Validates required initialization parameters provided by the user.
 * @param  {Object} config the configuration object argument provided at
 * initialization
 * @return {void}
 */
function validateConfig(config) {
  const {
    endpoints,
    root = ''
  } = config

  if (!root || !root.trim()) {
    throw new Error('api root is required')
  }

  if (!endpoints || !Array.isArray(endpoints) || !endpoints.length) {
    throw new Error('endpoints must be a non-empty array')
  }
}

/**
 * Choose between user provided dependencies and peer dependencies mentioned
 * in the README.
 * @param  {Object} config
 * @return {(Promise, request)}
 */
function chooseDependencies(config) {
  const {
    logger = noopLogger, promise, request
  } = config

  let agent = request
  let promiseLib = promise

  if (!promise) {
    try {
      promiseLib = require('es6-promise')
        .Promise
    } catch (er) {
      throw new Error(
        'expected Promise to be defined or es6-promise to be installed, but both are missing'
      )
    }
  }

  if (!request) {
    try {
      const superagent = require('superagent')
      agent = requestFactory(superagent, promiseLib)
    } catch (er) {
      throw new Error(
        'expected request function to be defined or superagent to be installed, but both are missing'
      )
    }
  }

  return [promiseLib, agent]
}

/**
 * Recursively creates or updates the object hierarchy in [map] according to the
 * string elements of [path]. former leaf nodes are updated to be internal nodes
 * if a path is followed that implies it contains a leaf node subresource.
 * @param {Object} map an object mapping resource names to either an object that
 * includes subsequent resource names, or '1' for leaf nodes.
 * @param {Array.<string>} paths an array of resource names.  subsequent array
 * elements represent subresources of the preceeding elements.
 */
function place(map, paths) {
  const [head, ...rest] = paths
  let node = map[head]

  if (rest.length) {
    if (!node || node === 1) {
      node = {}
      map[head] = node

      return place(node, rest)
    }

    return place(node, rest)
  }

  if (!node) {
    map[head] = 1
  }

  return map
}

/**
 * Converts a collection of resource paths in array format to an object hierarchy
 * explicitly encoding the hierarchy.
 *
 * @param {Array.<Array.<string>>} ls array of arrays
 * @return {Object} hierarchy of resources
 */
function normalize(ls = [
  []
]) {
  if (!ls.length) return {}

  const out = {}
  ls.forEach(xs => place(out, xs))

  return out
}

module.exports = (config) => {
  const { endpoints, versions = [] } = config
  const { logger = noopLogger } = config

  validateConfig(config)

  const [promiseLib, requestLib] = chooseDependencies(config)

  // we want to work with an array of strings, but versions may be either an array or a single stirng
  const commonVersions = compact([].concat(versions))

  let [simpleResources, resources, ] = buildResources(endpoints,
    commonVersions)

  // this removes empty arrays from `resources`.  empty arrays may arise from
  // a user inputing an empty string endpoint.
  let all = resources.filter(ls => ls.length)
    .slice()

  // Array.prototype.push.apply(all, versioned)
  all = all.concat(simpleResources)

  // by placing the deep resource hierarchies first, we ensure that top-level
  // resources are created with the proper fluent behavior.
  all.sort((ls, xs) => -(ls.length - xs.length))

  const opts = Object.assign({}, config, {
    request: requestLib,
    promise: promiseLib,
    logger,
  }, {
    headers: Object.assign({}, DEFAULT_HEADERS, config.headers || {})
  })

  return build(opts)(normalize(all))
}
<|endoftext|>


<commit_before>
// -----------------------------------
// Tween search

function tweenComparator(a, b, time) {
  if (a.stop <= time && time < b.stop) {
    return 0
  } else {
    if (time < a.stop) {
      return -1
    } else {
      return 1
    }
  }
}

function tweenSearch(sortedList, time, low, high, comparator) {
  let mid = Math.floor((low + high) / 2)
  let len = sortedList.length

  if (len == 0) return null
  if (mid < 0) return [sortedList[0]]
  if (mid == len-1) return [sortedList[len-1]]

  let a = sortedList[mid]
  let b = sortedList[mid+1]

  let result = comparator(a, b, time)

  if (result == 0) {
    return [a, b]
  } else {
    if (result < 0) {
      return tweenSearch(sortedList, time, low, mid-1, comparator)
    } else {
      return tweenSearch(sortedList, time, mid+1, high, comparator)
    }
  }
}

// -----------------------------------
// Binary search

function binaryComparator(candidate, target) {
  if (target < candidate) {
    return -1
  } else if (target > candidate) {
    return 1
  } else {
    return 0
  }
}

function binarySearch(sortedList, target, low, high, comparator) {
  if (high < low) {
    return null
  }

  const mid = Math.floor((low + high) / 2)
  const result = comparator(sortedList[mid], target)

  if (result === 0) {
    return sortedList[mid]
  } else if (result < 0) {
    return binarySearch(sortedList, target, low, mid-1, comparator)
  } else {
    return binarySearch(sortedList, target, mid+1, high, comparator)
  }
}

// -----------------------------------
// Keyframe interpolation

function timingToInterpFn(timing, lib) {
  let map = {
    'linear': lib.linearInterpolate,
    'smooth': lib.smoothInterpolate
  }
  return map[timing] || map.linear
}

function keyframeInterpolateTween(keyframes, time, timing, lib) {
  // Clamp time to [0, 1]
  time = Math.max(Math.min(time, 1), 0)

  if (!Array.isArray(keyframes) || !keyframes.length) {
    return lib.zero()
  }

  const result = search.tween(keyframes, time)
  const interpolate = timingToInterpFn(timing, lib)

  if (result.length == 1) {
    let value = result[0].value
    return interpolate(value, value, 0)
  }

  const [a, b] = result
  const t_segment = (time - a.stop) / (b.stop - a.stop)
  return interpolate(a.value, b.value, t_segment)
}

function keyframeInterpolateBinary(keyframes, time, timing, lib) {
  const first = 0
  const last = keyframes.length - 1

  // Clamp time to [0, 1]
  time = Math.max(Math.min(time, 1), 0)

  if (!Array.isArray(keyframes) || !keyframes.length) {
    return lib.zero()
  }

  if (keyframes.length === 1) {
    return keyframes[first].value
  }

  if (time < keyframes[first].stop) {
    return keyframes[first].value
  }

  if (time > keyframes[last].stop) {
    return keyframes[last].value
  }

  const pairs = []
  keyframes.reduce((a, b) => {
    pairs.push([a, b])
    return b
  })

  const pair = search.binary(pairs, time, pair => {
    const [a, b] = pair

    if (time < a.stop) { return -1 }
    else if (time > b.stop) { return 1 }
    return 0
  })

  if (pair != null) {
    const [a, b] = pair
    const t_segment = (time - a.stop) / (b.stop - a.stop)
    const interpolate = timingToInterpFn(timing, lib)
    return interpolate(a.value, b.value, t_segment)
  }

  return lib.zero()
}

// -------------------------------------------------
// search library
// -------------------------------------------------

const search = {

  binary(sortedList, target, comparator=binaryComparator) {
    return binarySearch(sortedList, target, 0, sortedList.length-1, comparator)
  },

  tween(sortedList, time, comparator=tweenComparator) {
    return tweenSearch(sortedList, time, 0, sortedList.length - 1, comparator)
  }
}

// -------------------------------------------------
// util library
// -------------------------------------------------

const util = {

  /**
   * Smooth a parameter in the range 0 <= t <= 1 in a manner
   * that approximates cosine.
   * @param {float} t interpolation parameter in the range [0, 1]
   */
  smooth(t) {
    return 3 * Math.pow(t, 2) - 2 * Math.pow(t, 3)
  },

  /**
   * Convert a three-element array
   * @param {number[]} vector a three-element array
   */
  vector3dToHex(vector) {
    const hexColor = '#' + vector.slice(0,3).map((c) => {
      const byte = Math.round(c)
      const hex = byte.toString(16)
      return hex.length == 1 ? '0' + hex : hex
    }).join('')

    return hexColor
  },

  hexToVector3d(hexColor) {
    const hex = hexColor.slice(1)
    const r = parseInt(hex.slice(0, 2), 16)
    const g = parseInt(hex.slice(2, 4), 16)
    const b = parseInt(hex.slice(4, 6), 16)
    return [r, g, b]
  },

  vector3dToRgb(vector) {
    vector = vector.slice(0,3).map(function (byte) {
      return Math.round(byte)
    })
    return 'rgb(' + vector + ')'
  },

  rgbToVector3d(rgbColor) {
    var list = rgbColor.split(/[(), ]+/)
    return list.slice(1,4).map(function (s) {
      return +s
    })
  },

  keyframeSort(keyframes) {
    return keyframes.sort((a, b) => a.stop - b.stop)
  }
}

// -------------------------------------------------
// scalar library
// -------------------------------------------------

const scalar = {

  zero() {
    return 0
  },

  linearInterpolate(a, b, t) {
    return a + t*(b-a)
  },

  smoothInterpolate(a, b, t) {
    return a + util.smooth(t)*(b-a)
  },

  keyframeInterpolate(keyframes, time, timing='linear', lib=scalar) {
    return keyframeInterpolateTween(keyframes, time, timing, lib)
  }
}

// -------------------------------------------------
// vector2d library
// -------------------------------------------------

const vector2d = {

  zero() {
    return [0, 0]
  },

  linearInterpolate(a, b, t) {
    const lerp = scalar.linearInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t)
    ]
  },

  smoothInterpolate(a, b, t) {
    const lerp = scalar.smoothInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t)
    ]
  },

  keyframeInterpolate(keyframes, time, timing='linear', lib=vector2d) {
    return keyframeInterpolateTween(keyframes, time, timing, lib)
  }
}

// -------------------------------------------------
// vector3d library
// -------------------------------------------------

const vector3d = {

  zero() {
    return [0, 0, 0]
  },

  linearInterpolate(a, b, t) {
    const lerp = scalar.linearInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t),
      lerp(a[2], b[2], t)
    ]
  },

  smoothInterpolate(a, b, t) {
    const lerp = scalar.smoothInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t),
      lerp(a[2], b[2], t)
    ]
  },

  keyframeInterpolate(keyframes, time, timing='linear', lib=vector3d) {
    return keyframeInterpolateTween(keyframes, time, timing, lib)
  }
}

exports.util = util
exports.search = search
exports.scalar = scalar
exports.vector2d = vector2d
exports.vector3d = vector3d
<commit_msg>Use alternate value midpoint equation<commit_after>
// -----------------------------------
// Tween search

function tweenComparator(a, b, time) {
  if (a.stop <= time && time < b.stop) {
    return 0
  } else {
    if (time < a.stop) {
      return -1
    } else {
      return 1
    }
  }
}

function tweenSearch(sortedList, time, low, high, comparator) {
  let mid = Math.floor(low + ((high-low) / 2))
  let len = sortedList.length

  if (len == 0) return null
  if (mid < 0) return [sortedList[0]]
  if (mid == len-1) return [sortedList[len-1]]

  let a = sortedList[mid]
  let b = sortedList[mid+1]

  let result = comparator(a, b, time)

  if (result == 0) {
    return [a, b]
  } else {
    if (result < 0) {
      return tweenSearch(sortedList, time, low, mid-1, comparator)
    } else {
      return tweenSearch(sortedList, time, mid+1, high, comparator)
    }
  }
}

// -----------------------------------
// Binary search

function binaryComparator(candidate, target) {
  if (target < candidate) {
    return -1
  } else if (target > candidate) {
    return 1
  } else {
    return 0
  }
}

function binarySearch(sortedList, target, low, high, comparator) {
  if (high < low) {
    return null
  }

  const mid = Math.floor(low + ((high-low) / 2))
  const result = comparator(sortedList[mid], target)

  if (result === 0) {
    return sortedList[mid]
  } else if (result < 0) {
    return binarySearch(sortedList, target, low, mid-1, comparator)
  } else {
    return binarySearch(sortedList, target, mid+1, high, comparator)
  }
}

// -----------------------------------
// Keyframe interpolation

function timingToInterpFn(timing, lib) {
  let map = {
    'linear': lib.linearInterpolate,
    'smooth': lib.smoothInterpolate
  }
  return map[timing] || map.linear
}

function keyframeInterpolateTween(keyframes, time, timing, lib) {
  // Clamp time to [0, 1]
  time = Math.max(Math.min(time, 1), 0)

  if (!Array.isArray(keyframes) || !keyframes.length) {
    return lib.zero()
  }

  const result = search.tween(keyframes, time)
  const interpolate = timingToInterpFn(timing, lib)

  if (result.length == 1) {
    let value = result[0].value
    return interpolate(value, value, 0)
  }

  const [a, b] = result
  const t_segment = (time - a.stop) / (b.stop - a.stop)
  return interpolate(a.value, b.value, t_segment)
}

function keyframeInterpolateBinary(keyframes, time, timing, lib) {
  const first = 0
  const last = keyframes.length - 1

  // Clamp time to [0, 1]
  time = Math.max(Math.min(time, 1), 0)

  if (!Array.isArray(keyframes) || !keyframes.length) {
    return lib.zero()
  }

  if (keyframes.length === 1) {
    return keyframes[first].value
  }

  if (time < keyframes[first].stop) {
    return keyframes[first].value
  }

  if (time > keyframes[last].stop) {
    return keyframes[last].value
  }

  const pairs = []
  keyframes.reduce((a, b) => {
    pairs.push([a, b])
    return b
  })

  const pair = search.binary(pairs, time, pair => {
    const [a, b] = pair

    if (time < a.stop) { return -1 }
    else if (time > b.stop) { return 1 }
    return 0
  })

  if (pair != null) {
    const [a, b] = pair
    const t_segment = (time - a.stop) / (b.stop - a.stop)
    const interpolate = timingToInterpFn(timing, lib)
    return interpolate(a.value, b.value, t_segment)
  }

  return lib.zero()
}

// -------------------------------------------------
// search library
// -------------------------------------------------

const search = {

  binary(sortedList, target, comparator=binaryComparator) {
    return binarySearch(sortedList, target, 0, sortedList.length-1, comparator)
  },

  tween(sortedList, time, comparator=tweenComparator) {
    return tweenSearch(sortedList, time, 0, sortedList.length - 1, comparator)
  }
}

// -------------------------------------------------
// util library
// -------------------------------------------------

const util = {

  /**
   * Smooth a parameter in the range 0 <= t <= 1 in a manner
   * that approximates cosine.
   * @param {float} t interpolation parameter in the range [0, 1]
   */
  smooth(t) {
    return 3 * Math.pow(t, 2) - 2 * Math.pow(t, 3)
  },

  /**
   * Convert a three-element array
   * @param {number[]} vector a three-element array
   */
  vector3dToHex(vector) {
    const hexColor = '#' + vector.slice(0,3).map((c) => {
      const byte = Math.round(c)
      const hex = byte.toString(16)
      return hex.length == 1 ? '0' + hex : hex
    }).join('')

    return hexColor
  },

  hexToVector3d(hexColor) {
    const hex = hexColor.slice(1)
    const r = parseInt(hex.slice(0, 2), 16)
    const g = parseInt(hex.slice(2, 4), 16)
    const b = parseInt(hex.slice(4, 6), 16)
    return [r, g, b]
  },

  vector3dToRgb(vector) {
    vector = vector.slice(0,3).map(function (byte) {
      return Math.round(byte)
    })
    return 'rgb(' + vector + ')'
  },

  rgbToVector3d(rgbColor) {
    var list = rgbColor.split(/[(), ]+/)
    return list.slice(1,4).map(function (s) {
      return +s
    })
  },

  keyframeSort(keyframes) {
    return keyframes.sort((a, b) => a.stop - b.stop)
  }
}

// -------------------------------------------------
// scalar library
// -------------------------------------------------

const scalar = {

  zero() {
    return 0
  },

  linearInterpolate(a, b, t) {
    return a + t*(b-a)
  },

  smoothInterpolate(a, b, t) {
    return a + util.smooth(t)*(b-a)
  },

  keyframeInterpolate(keyframes, time, timing='linear', lib=scalar) {
    return keyframeInterpolateTween(keyframes, time, timing, lib)
  }
}

// -------------------------------------------------
// vector2d library
// -------------------------------------------------

const vector2d = {

  zero() {
    return [0, 0]
  },

  linearInterpolate(a, b, t) {
    const lerp = scalar.linearInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t)
    ]
  },

  smoothInterpolate(a, b, t) {
    const lerp = scalar.smoothInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t)
    ]
  },

  keyframeInterpolate(keyframes, time, timing='linear', lib=vector2d) {
    return keyframeInterpolateTween(keyframes, time, timing, lib)
  }
}

// -------------------------------------------------
// vector3d library
// -------------------------------------------------

const vector3d = {

  zero() {
    return [0, 0, 0]
  },

  linearInterpolate(a, b, t) {
    const lerp = scalar.linearInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t),
      lerp(a[2], b[2], t)
    ]
  },

  smoothInterpolate(a, b, t) {
    const lerp = scalar.smoothInterpolate
    return [
      lerp(a[0], b[0], t),
      lerp(a[1], b[1], t),
      lerp(a[2], b[2], t)
    ]
  },

  keyframeInterpolate(keyframes, time, timing='linear', lib=vector3d) {
    return keyframeInterpolateTween(keyframes, time, timing, lib)
  }
}

exports.util = util
exports.search = search
exports.scalar = scalar
exports.vector2d = vector2d
exports.vector3d = vector3d
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import {
  Dimensions,
  Modal,
  DeviceEventEmitter,
  TouchableWithoutFeedback,
  KeyboardAvoidingView,
} from 'react-native';
import PropTypes from 'prop-types';
import {
  View,
  initializeRegistryWithDefinitions,
  registerAnimation,
  createAnimation,
} from 'react-native-animatable';
import * as ANIMATION_DEFINITIONS from './animations';

import styles from './index.style.js';

// Override default animations
initializeRegistryWithDefinitions(ANIMATION_DEFINITIONS);

// Utility for creating custom animations
const makeAnimation = (name, obj) => {
  registerAnimation(name, createAnimation(obj));
};

const isObject = obj => {
  return obj !== null && typeof obj === 'object';
};

export class ReactNativeModal extends Component {
  static propTypes = {
    animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
    animationInTiming: PropTypes.number,
    animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
    animationOutTiming: PropTypes.number,
    avoidKeyboard: PropTypes.bool,
    backdropColor: PropTypes.string,
    backdropOpacity: PropTypes.number,
    backdropTransitionInTiming: PropTypes.number,
    backdropTransitionOutTiming: PropTypes.number,
    children: PropTypes.node.isRequired,
    isVisible: PropTypes.bool.isRequired,
    onModalShow: PropTypes.func,
    onModalHide: PropTypes.func,
    onBackButtonPress: PropTypes.func,
    onBackdropPress: PropTypes.func,
    style: PropTypes.any,
  };

  static defaultProps = {
    animationIn: 'slideInUp',
    animationInTiming: 300,
    animationOut: 'slideOutDown',
    animationOutTiming: 300,
    avoidKeyboard: false,
    backdropColor: 'black',
    backdropOpacity: 0.7,
    backdropTransitionInTiming: 300,
    backdropTransitionOutTiming: 300,
    onModalShow: () => null,
    onModalHide: () => null,
    isVisible: false,
    onBackdropPress: () => null,
    onBackButtonPress: () => null,
  };

  // We use an internal state for keeping track of the modal visibility: this allows us to keep
  // the modal visibile during the exit animation, even if the user has already change the
  // isVisible prop to false.
  // We also store in the state the device width and height so that we can update the modal on
  // device rotation.
  state = {
    isVisible: false,
    deviceWidth: Dimensions.get('window').width,
    deviceHeight: Dimensions.get('window').height,
  };

  constructor(props) {
    super(props);
    this._buildAnimations(props);
  }

  componentWillReceiveProps(nextProps) {
    if (!this.state.isVisible && nextProps.isVisible) {
      this.setState({ isVisible: true });
    }
    if (
      this.props.animationIn !== nextProps.animationIn ||
      this.props.animationOut !== nextProps.animationOut
    ) {
      this._buildAnimations(nextProps);
    }
  }

  componentWillMount() {
    if (this.props.isVisible) {
      this.setState({ isVisible: true });
    }
  }

  componentDidMount() {
    if (this.state.isVisible) {
      this._open();
    }
    DeviceEventEmitter.addListener('didUpdateDimensions', this._handleDimensionsUpdate);
  }

  componentWillUnmount() {
    DeviceEventEmitter.removeListener('didUpdateDimensions', this._handleDimensionsUpdate);
  }

  componentDidUpdate(prevProps, prevState) {
    // On modal open request, we slide the view up and fade in the backdrop
    if (this.state.isVisible && !prevState.isVisible) {
      this._open();
      // On modal close request, we slide the view down and fade out the backdrop
    } else if (!this.props.isVisible && prevProps.isVisible) {
      this._close();
    }
  }

  // User can define custom react-native-animatable animations, see PR #72
  _buildAnimations = props => {
    let animationIn = props.animationIn;
    let animationOut = props.animationOut;

    if (isObject(animationIn)) {
      makeAnimation('animationIn', animationIn);
      animationIn = 'animationIn';
    }

    if (isObject(animationOut)) {
      makeAnimation('animationOut', animationOut);
      animationOut = 'animationOut';
    }

    this.animationIn = animationIn;
    this.animationOut = animationOut;
  };

  _handleDimensionsUpdate = dimensionsUpdate => {
    // Here we update the device dimensions in the state if the layout changed (triggering a render)
    const deviceWidth = Dimensions.get('window').width;
    const deviceHeight = Dimensions.get('window').height;
    if (deviceWidth !== this.state.deviceWidth || deviceHeight !== this.state.deviceHeight) {
      this.setState({ deviceWidth, deviceHeight });
    }
  };

  _open = () => {
    this.backdropRef.transitionTo(
      { opacity: this.props.backdropOpacity },
      this.props.backdropTransitionInTiming,
    );
    this.contentRef[this.animationIn](this.props.animationInTiming).then(() => {
      this.props.onModalShow();
    });
  };

  _close = async () => {
    this.backdropRef.transitionTo({ opacity: 0 }, this.props.backdropTransitionOutTiming);
    this.contentRef[this.animationOut](this.props.animationOutTiming).then(() => {
      this.setState({ isVisible: false });
      this.props.onModalHide();
    });
  };

  render() {
    const {
      animationIn,
      animationInTiming,
      animationOut,
      animationOutTiming,
      avoidKeyboard,
      backdropColor,
      backdropOpacity,
      backdropTransitionInTiming,
      backdropTransitionOutTiming,
      children,
      isVisible,
      onModalShow,
      onBackdropPress,
      onBackButtonPress,
      style,
      ...otherProps
    } = this.props;
    const { deviceWidth, deviceHeight } = this.state;

    const computedStyle = [
      { margin: deviceWidth * 0.05, transform: [{ translateY: 0 }] },
      styles.content,
      style,
    ];

    const containerView = (
      <View
        ref={ref => (this.contentRef = ref)}
        style={computedStyle}
        pointerEvents={'box-none'}
        {...otherProps}
      >
        {children}
      </View>
    );

    return (
      <Modal
        transparent={true}
        animationType={'none'}
        visible={this.state.isVisible}
        onRequestClose={onBackButtonPress}
        {...otherProps}
      >
        <TouchableWithoutFeedback onPress={onBackdropPress}>
          <View
            ref={ref => (this.backdropRef = ref)}
            style={[
              styles.backdrop,
              {
                backgroundColor: backdropColor,
                width: deviceWidth,
                height: deviceHeight,
              },
            ]}
          />
        </TouchableWithoutFeedback>

        {avoidKeyboard && (
          <KeyboardAvoidingView
            behavior={'padding'}
            pointerEvents={'box-none'}
            style={computedStyle.concat([{ margin: 0 }])}
          >
            {containerView}
          </KeyboardAvoidingView>
        )}

        {!avoidKeyboard && containerView}
      </Modal>
    );
  }
}

export default ReactNativeModal;
<commit_msg>lock transitions and cleanup when they are done<commit_after>import React, { Component } from 'react';
import {
  Dimensions,
  Modal,
  DeviceEventEmitter,
  TouchableWithoutFeedback,
  KeyboardAvoidingView,
} from 'react-native';
import PropTypes from 'prop-types';
import {
  View,
  initializeRegistryWithDefinitions,
  registerAnimation,
  createAnimation,
} from 'react-native-animatable';
import * as ANIMATION_DEFINITIONS from './animations';

import styles from './index.style.js';

// Override default animations
initializeRegistryWithDefinitions(ANIMATION_DEFINITIONS);

// Utility for creating custom animations
const makeAnimation = (name, obj) => {
  registerAnimation(name, createAnimation(obj));
};

const isObject = obj => {
  return obj !== null && typeof obj === 'object';
};

export class ReactNativeModal extends Component {
  static propTypes = {
    animationIn: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
    animationInTiming: PropTypes.number,
    animationOut: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
    animationOutTiming: PropTypes.number,
    avoidKeyboard: PropTypes.bool,
    backdropColor: PropTypes.string,
    backdropOpacity: PropTypes.number,
    backdropTransitionInTiming: PropTypes.number,
    backdropTransitionOutTiming: PropTypes.number,
    children: PropTypes.node.isRequired,
    isVisible: PropTypes.bool.isRequired,
    onModalShow: PropTypes.func,
    onModalHide: PropTypes.func,
    onBackButtonPress: PropTypes.func,
    onBackdropPress: PropTypes.func,
    style: PropTypes.any,
  };

  static defaultProps = {
    animationIn: 'slideInUp',
    animationInTiming: 300,
    animationOut: 'slideOutDown',
    animationOutTiming: 300,
    avoidKeyboard: false,
    backdropColor: 'black',
    backdropOpacity: 0.7,
    backdropTransitionInTiming: 300,
    backdropTransitionOutTiming: 300,
    onModalShow: () => null,
    onModalHide: () => null,
    isVisible: false,
    onBackdropPress: () => null,
    onBackButtonPress: () => null,
  };

  // We use an internal state for keeping track of the modal visibility: this allows us to keep
  // the modal visibile during the exit animation, even if the user has already change the
  // isVisible prop to false.
  // We also store in the state the device width and height so that we can update the modal on
  // device rotation.
  state = {
    isVisible: false,
    deviceWidth: Dimensions.get('window').width,
    deviceHeight: Dimensions.get('window').height,
  };

  transitionLock = null;

  constructor(props) {
    super(props);
    this._buildAnimations(props);
  }

  componentWillReceiveProps(nextProps) {
    if (!this.state.isVisible && nextProps.isVisible) {
      this.setState({ isVisible: true });
    }
    if (
      this.props.animationIn !== nextProps.animationIn ||
      this.props.animationOut !== nextProps.animationOut
    ) {
      this._buildAnimations(nextProps);
    }
  }

  componentWillMount() {
    if (this.props.isVisible) {
      this.setState({ isVisible: true });
    }
  }

  componentDidMount() {
    if (this.state.isVisible) {
      this._open();
    }
    DeviceEventEmitter.addListener('didUpdateDimensions', this._handleDimensionsUpdate);
  }

  componentWillUnmount() {
    DeviceEventEmitter.removeListener('didUpdateDimensions', this._handleDimensionsUpdate);
  }

  componentDidUpdate(prevProps, prevState) {
    // On modal open request, we slide the view up and fade in the backdrop
    if (this.props.isVisible && !prevProps.isVisible) {
      this._open();
    }
    // On modal close request, we slide the view down and fade out the backdrop
    else if (!this.props.isVisible && prevProps.isVisible) {
      this._close();
    }
  }

  // User can define custom react-native-animatable animations, see PR #72
  _buildAnimations = props => {
    let animationIn = props.animationIn;
    let animationOut = props.animationOut;

    if (isObject(animationIn)) {
      makeAnimation('animationIn', animationIn);
      animationIn = 'animationIn';
    }

    if (isObject(animationOut)) {
      makeAnimation('animationOut', animationOut);
      animationOut = 'animationOut';
    }

    this.animationIn = animationIn;
    this.animationOut = animationOut;
  };

  _handleDimensionsUpdate = dimensionsUpdate => {
    // Here we update the device dimensions in the state if the layout changed (triggering a render)
    const deviceWidth = Dimensions.get('window').width;
    const deviceHeight = Dimensions.get('window').height;
    if (deviceWidth !== this.state.deviceWidth || deviceHeight !== this.state.deviceHeight) {
      this.setState({ deviceWidth, deviceHeight });
    }
  };

  _open = () => {
    if (this.transitionLock) return;
    this.transitionLock = true;
    this.backdropRef.transitionTo(
      { opacity: this.props.backdropOpacity },
      this.props.backdropTransitionInTiming,
    );
    this.contentRef[this.animationIn](this.props.animationInTiming).then(() => {
      this.transitionLock = false;
      if (!this.props.isVisible) {
        this._close();
      }
      else {
        this.props.onModalShow();
      }
    });
  };

  _close = () => {
    if (this.transitionLock) return;
    this.transitionLock = true;
    this.backdropRef.transitionTo({ opacity: 0 }, this.props.backdropTransitionOutTiming);
    this.contentRef[this.animationOut](this.props.animationOutTiming).then(() => {
      this.transitionLock = false;
      if (this.props.isVisible) {
        this._open();
      }
      else {
        this.setState({ isVisible: false });
        this.props.onModalHide();
      }
    });
  };

  render() {
    const {
      animationIn,
      animationInTiming,
      animationOut,
      animationOutTiming,
      avoidKeyboard,
      backdropColor,
      backdropOpacity,
      backdropTransitionInTiming,
      backdropTransitionOutTiming,
      children,
      isVisible,
      onModalShow,
      onBackdropPress,
      onBackButtonPress,
      style,
      ...otherProps
    } = this.props;
    const { deviceWidth, deviceHeight } = this.state;

    const computedStyle = [
      { margin: deviceWidth * 0.05, transform: [{ translateY: 0 }] },
      styles.content,
      style,
    ];

    const containerView = (
      <View
        ref={ref => (this.contentRef = ref)}
        style={computedStyle}
        pointerEvents={'box-none'}
        {...otherProps}
      >
        {children}
      </View>
    );

    return (
      <Modal
        transparent={true}
        animationType={'none'}
        visible={this.state.isVisible}
        onRequestClose={onBackButtonPress}
        {...otherProps}
      >
        <TouchableWithoutFeedback onPress={onBackdropPress}>
          <View
            ref={ref => (this.backdropRef = ref)}
            style={[
              styles.backdrop,
              {
                backgroundColor: backdropColor,
                width: deviceWidth,
                height: deviceHeight,
              },
            ]}
          />
        </TouchableWithoutFeedback>

        {avoidKeyboard && (
          <KeyboardAvoidingView
            behavior={'padding'}
            pointerEvents={'box-none'}
            style={computedStyle.concat([{ margin: 0 }])}
          >
            {containerView}
          </KeyboardAvoidingView>
        )}

        {!avoidKeyboard && containerView}
      </Modal>
    );
  }
}

export default ReactNativeModal;
<|endoftext|>


<commit_before>import * as React from 'react';
import loadScript from 'load-script';

import eventNames from './eventNames';

let apiPromise = false;

function loadApi() {
  if (!apiPromise) {
    apiPromise = new Promise((resolve, reject) => {
      loadScript('https://api.dmcdn.net/all.js', (err) => {
        if (err) {
          reject(err);
        } else {
          resolve(window.DM);
        }
      });
    });
  }
  return apiPromise;
}

const sizePropType = React.PropTypes.oneOfType([
  React.PropTypes.number,
  React.PropTypes.string
]);

export default class Dailymotion extends React.Component {
  static propTypes = {
    video: React.PropTypes.string,
    width: sizePropType,
    height: sizePropType,

    // Player parameters
    autoplay: React.PropTypes.bool,
    controls: React.PropTypes.bool,
    showEndScreen: React.PropTypes.bool,
    id: React.PropTypes.string,
    mute: React.PropTypes.bool,
    origin: React.PropTypes.string,
    quality: React.PropTypes.oneOf(['240', '380', '480', '720', '1080', '1440', '2160']),
    sharing: React.PropTypes.bool,
    start: React.PropTypes.number,
    subtitles: React.PropTypes.string,
    syndication: React.PropTypes.string,
    uiHighlightColor: React.PropTypes.string,
    uiShowLogo: React.PropTypes.bool,
    uiShowStartScreenInfo: React.PropTypes.bool,
    theme: React.PropTypes.oneOf(['light', 'dark']),

    // Player properties (not parameters, can only be set using methods)
    volume: React.PropTypes.number,

    // Events
    onAdEnd: React.PropTypes.func,
    onAdPause: React.PropTypes.func,
    onAdPlay: React.PropTypes.func,
    onAdStart: React.PropTypes.func,
    onAdTimeUpdate: React.PropTypes.func,
    onApiReady: React.PropTypes.func,
    onDurationChange: React.PropTypes.func,
    onEnd: React.PropTypes.func,
    onError: React.PropTypes.func,
    onFullscreenChange: React.PropTypes.func,
    onLoadedMetadata: React.PropTypes.func,
    onPause: React.PropTypes.func,
    onPlay: React.PropTypes.func,
    onPlaying: React.PropTypes.func,
    onProgress: React.PropTypes.func,
    onQualitiesAvailable: React.PropTypes.func,
    onQualityChange: React.PropTypes.func,
    onSeeked: React.PropTypes.func,
    onSeeking: React.PropTypes.func,
    onSubtitleChange: React.PropTypes.func,
    onSubtitlesAvailable: React.PropTypes.func,
    onStart: React.PropTypes.func,
    onTimeUpdate: React.PropTypes.func,
    onVideoStart: React.PropTypes.func,
    onVideoEnd: React.PropTypes.func,
    onVolumeChange: React.PropTypes.func,
    onWaiting: React.PropTypes.func,
  };

  static defaultProps = {
    theme: 'dark',
  };

  componentDidMount() {
    this.createPlayer();
  }

  componentDidUpdate(prevProps) {
    const changes = Object.keys(this.props).filter(
      name => this.props[name] !== prevProps[name]
    );

    this.updateProps(changes);
  }

  getPlayerParameters() {
    return {
      autoplay: this.props.autoplay,
      controls: this.props.controls,
      'endscreen-enable': this.props.showEndScreen,
      id: this.props.id,
      mute: this.props.mute,
      origin: this.props.origin,
      quality: this.props.quality,
      'sharing-enable': this.props.sharing,
      start: this.props.start,
      'subtitles-default': this.props.subtitles,
      syndication: this.props.syndication,
      'ui-highlight': this.props.uiHighlightColor,
      'ui-logo': this.props.uiShowLogo,
      'ui-start-screen-info': this.props.uiShowStartScreenInfo,
      'ui-theme': this.props.theme,
    };
  }

  getInitialOptions() {
    return {
      video: this.props.video,
      width: this.props.width,
      height: this.props.height,
      params: this.getPlayerParameters(),
      events: {},
    };
  }

  updateProps(propNames) {
    this.player.then(player => {
      propNames.forEach(name => {
        const value = this.props[name];
        switch (name) {
          case 'muted':
            player.setMuted(value);
            break;
          case 'quality':
            player.setQuality(value);
            break;
          case 'subtitles':
            player.setSubtitle(value);
            break;
          case 'volume':
            player.setVolume(value);
            break;
          case 'video':
            player.load(value, this.getPlayerParameters());
            break;
          default:
            // Nothing
        }
      });
    });
  }

  createPlayer() {
    this.player = loadApi().then(DM =>
      new Promise(resolve => {
        const player = DM.player(this.container, this.getInitialOptions());
        player.addEventListener('apiready', () => {
          resolve(player);
        });

        Object.keys(eventNames).forEach(dmName => {
          const reactName = eventNames[dmName];
          player.addEventListener(dmName, event => {
            if (this.props[reactName]) {
              this.props[reactName](event);
            }
          });
        });
      })
    );

    if (typeof this.props.volume === 'number') {
      this.updateProps(['volume']);
    }
  }

  refContainer = container => {
    this.container = container;
  };

  render() {
    return React.createElement('div', {
      ref: this.refContainer,
    });
  }
}
<commit_msg>reuse Dailymotion SDK if one is already loaded on the page<commit_after>import * as React from 'react';
import loadScript from 'load-script';

import eventNames from './eventNames';

let apiPromise = false;

function loadApi() {
  if (!apiPromise) {
    if (typeof window.DM === 'object' && typeof window.DM.player === 'function') {
      // A Dailymotion SDK is already loaded, so reuse that
      apiPromise = Promise.resolve(window.DM);
    } else {
      apiPromise = new Promise((resolve, reject) => {
        loadScript('https://api.dmcdn.net/all.js', (err) => {
          if (err) {
            reject(err);
          } else {
            resolve(window.DM);
          }
        });
      });
    }
  }
  return apiPromise;
}

const sizePropType = React.PropTypes.oneOfType([
  React.PropTypes.number,
  React.PropTypes.string
]);

export default class Dailymotion extends React.Component {
  static propTypes = {
    video: React.PropTypes.string,
    width: sizePropType,
    height: sizePropType,

    // Player parameters
    autoplay: React.PropTypes.bool,
    controls: React.PropTypes.bool,
    showEndScreen: React.PropTypes.bool,
    id: React.PropTypes.string,
    mute: React.PropTypes.bool,
    origin: React.PropTypes.string,
    quality: React.PropTypes.oneOf(['240', '380', '480', '720', '1080', '1440', '2160']),
    sharing: React.PropTypes.bool,
    start: React.PropTypes.number,
    subtitles: React.PropTypes.string,
    syndication: React.PropTypes.string,
    uiHighlightColor: React.PropTypes.string,
    uiShowLogo: React.PropTypes.bool,
    uiShowStartScreenInfo: React.PropTypes.bool,
    theme: React.PropTypes.oneOf(['light', 'dark']),

    // Player properties (not parameters, can only be set using methods)
    volume: React.PropTypes.number,

    // Events
    onAdEnd: React.PropTypes.func,
    onAdPause: React.PropTypes.func,
    onAdPlay: React.PropTypes.func,
    onAdStart: React.PropTypes.func,
    onAdTimeUpdate: React.PropTypes.func,
    onApiReady: React.PropTypes.func,
    onDurationChange: React.PropTypes.func,
    onEnd: React.PropTypes.func,
    onError: React.PropTypes.func,
    onFullscreenChange: React.PropTypes.func,
    onLoadedMetadata: React.PropTypes.func,
    onPause: React.PropTypes.func,
    onPlay: React.PropTypes.func,
    onPlaying: React.PropTypes.func,
    onProgress: React.PropTypes.func,
    onQualitiesAvailable: React.PropTypes.func,
    onQualityChange: React.PropTypes.func,
    onSeeked: React.PropTypes.func,
    onSeeking: React.PropTypes.func,
    onSubtitleChange: React.PropTypes.func,
    onSubtitlesAvailable: React.PropTypes.func,
    onStart: React.PropTypes.func,
    onTimeUpdate: React.PropTypes.func,
    onVideoStart: React.PropTypes.func,
    onVideoEnd: React.PropTypes.func,
    onVolumeChange: React.PropTypes.func,
    onWaiting: React.PropTypes.func,
  };

  static defaultProps = {
    theme: 'dark',
  };

  componentDidMount() {
    this.createPlayer();
  }

  componentDidUpdate(prevProps) {
    const changes = Object.keys(this.props).filter(
      name => this.props[name] !== prevProps[name]
    );

    this.updateProps(changes);
  }

  getPlayerParameters() {
    return {
      autoplay: this.props.autoplay,
      controls: this.props.controls,
      'endscreen-enable': this.props.showEndScreen,
      id: this.props.id,
      mute: this.props.mute,
      origin: this.props.origin,
      quality: this.props.quality,
      'sharing-enable': this.props.sharing,
      start: this.props.start,
      'subtitles-default': this.props.subtitles,
      syndication: this.props.syndication,
      'ui-highlight': this.props.uiHighlightColor,
      'ui-logo': this.props.uiShowLogo,
      'ui-start-screen-info': this.props.uiShowStartScreenInfo,
      'ui-theme': this.props.theme,
    };
  }

  getInitialOptions() {
    return {
      video: this.props.video,
      width: this.props.width,
      height: this.props.height,
      params: this.getPlayerParameters(),
      events: {},
    };
  }

  updateProps(propNames) {
    this.player.then(player => {
      propNames.forEach(name => {
        const value = this.props[name];
        switch (name) {
          case 'muted':
            player.setMuted(value);
            break;
          case 'quality':
            player.setQuality(value);
            break;
          case 'subtitles':
            player.setSubtitle(value);
            break;
          case 'volume':
            player.setVolume(value);
            break;
          case 'video':
            player.load(value, this.getPlayerParameters());
            break;
          default:
            // Nothing
        }
      });
    });
  }

  createPlayer() {
    this.player = loadApi().then(DM =>
      new Promise(resolve => {
        const player = DM.player(this.container, this.getInitialOptions());
        player.addEventListener('apiready', () => {
          resolve(player);
        });

        Object.keys(eventNames).forEach(dmName => {
          const reactName = eventNames[dmName];
          player.addEventListener(dmName, event => {
            if (this.props[reactName]) {
              this.props[reactName](event);
            }
          });
        });
      })
    );

    if (typeof this.props.volume === 'number') {
      this.updateProps(['volume']);
    }
  }

  refContainer = container => {
    this.container = container;
  };

  render() {
    return React.createElement('div', {
      ref: this.refContainer,
    });
  }
}
<|endoftext|>


<commit_before>import Shape from 'clipper-js';

export default function(paths, point, { offset = 1.0, scale = 1.0 } = {}) {
  offset *= scale;
  // convert point to uppercase
  point = { X: point.x * scale, Y: point.y * scale };
  // instanciate new shape
  const shape = new Shape(paths, false, true);

  const shapes = shape
    // scale up for precision
    .scaleUp(scale)
    // convert lines to polygons (this gives lines width)
    .offset(offset, { joinType: 'jtSquare', endType: 'etOpenButt' })
    // union all overlapping paths
    .removeOverlap()
    // make all holes outlines and all outlines holes
    .reverse()
    // seperate all shapes into (these all all plausible fills)
    .seperateShapes();

  for (const fill of shapes) { // loop trough all
    if (fill.pointInShape(point)) { // check if point has hit with shape
      return fill // return fill
        // offset result to account for converting to polygon
        .offset(offset, { joinType: 'jtSquare', endType: 'etClosedPolygon' })
        // scale down to accout for scale up
        .scaleDown(scale)
        // convert uppercase points to lowercase points
        .mapToLower();
    }
  }

  // if no fill has been found return empty shape
  return [];
}
<commit_msg>added miter limit argument<commit_after>import Shape from 'clipper-js';

export default function(paths, point, { offset = 1.0, scale = 1.0, miterLimit = 2.0 } = {}) {
  offset *= scale;
  // convert point to uppercase
  point = { X: point.x * scale, Y: point.y * scale };
  // instanciate new shape
  const shape = new Shape(paths, false, true);

  const shapes = shape
    // scale up for precision
    .scaleUp(scale)
    // convert lines to polygons (this gives lines width)
    .offset(offset, { jointType: 'jtMiter', endType: 'etOpenButt', miterLimit })
    // union all overlapping paths
    .removeOverlap()
    // make all holes outlines and all outlines holes
    .reverse()
    // seperate all shapes into (these all all plausible fills)
    .seperateShapes();

  for (const fill of shapes) { // loop trough all
    if (fill.pointInShape(point)) { // check if point has hit with shape
      return fill // return fill
        // offset result to account for converting to polygon
        .offset(offset, { jointType: 'jtMiter', endType: 'etClosedPolygon', miterLimit })
        // scale down to accout for scale up
        .scaleDown(scale)
        // convert uppercase points to lowercase points
        .mapToLower();
    }
  }

  // if no fill has been found return empty shape
  return [];
}
<|endoftext|>