From 6e3d7c66677f84b384d03c1803ac74fe74c4865d Mon Sep 17 00:00:00 2001 From: Andrew Stewart Date: Fri, 20 Feb 2015 18:49:31 -0800 Subject: [PATCH] Add helpers to replace some lodash functionality Not a 1:1, and probably much slower than Lodash's ultra-optimised implementations, but we do enough I/O that it's not really relevant, is it? --- lib/adaptor.js | 12 +- lib/basestar.js | 5 +- lib/cylon.js | 50 ++------ lib/driver.js | 10 +- lib/initializer.js | 23 ++-- lib/logger.js | 6 +- lib/robot.js | 113 +++++------------ lib/utils.js | 59 ++------- lib/utils/helpers.js | 156 +++++++++++++++++++++++ spec/lib/utils.spec.js | 33 ----- spec/lib/utils/helpers.spec.js | 223 +++++++++++++++++++++++++++++++++ 11 files changed, 457 insertions(+), 233 deletions(-) create mode 100644 lib/utils/helpers.js create mode 100644 spec/lib/utils/helpers.spec.js diff --git a/lib/adaptor.js b/lib/adaptor.js index 91e536e..6a2b7ff 100644 --- a/lib/adaptor.js +++ b/lib/adaptor.js @@ -9,7 +9,8 @@ "use strict"; var Basestar = require("./basestar"), - Utils = require("./utils"); + Utils = require("./utils"), + _ = require("./utils/helpers"); // Public: Creates a new Adaptor // @@ -33,14 +34,11 @@ var Adaptor = module.exports = function Adaptor(opts) { // misc. details provided in args hash this.details = {}; - for (var name in opts) { - var opt = opts[name], - banned = ["robot", "name", "adaptor", "events"]; - - if (!~banned.indexOf(name)) { + _.each(opts, function(opt, name) { + if (!~["robot", "name", "adaptor", "events"].indexOf(name)) { this.details[name] = opt; } - } + }, this); }; Utils.subclass(Adaptor, Basestar); diff --git a/lib/basestar.js b/lib/basestar.js index 92912e9..7e05292 100644 --- a/lib/basestar.js +++ b/lib/basestar.js @@ -10,7 +10,8 @@ var EventEmitter = require("events").EventEmitter; -var Utils = require("./utils"); +var Utils = require("./utils"), + _ = require("./utils/helpers"); // Basestar is a base class to be used when writing external Cylon adaptors and // drivers. It provides some useful base methods and functionality @@ -83,7 +84,7 @@ Basestar.prototype.defineDriverEvent = function(opts) { }; Basestar.prototype._proxyEvents = function(opts, source, target) { - opts = (typeof opts === "string") ? { eventName: opts } : opts; + opts = _.isString(opts) ? { eventName: opts } : opts; opts.source = source; opts.target = target; diff --git a/lib/cylon.js b/lib/cylon.js index b7f78c3..21e311c 100644 --- a/lib/cylon.js +++ b/lib/cylon.js @@ -13,7 +13,8 @@ var Async = require("async"); var Logger = require("./logger"), Robot = require("./robot"), Config = require("./config"), - Utils = require("./utils"); + Utils = require("./utils"), + _ = require("./utils/helpers"); var EventEmitter = require("events").EventEmitter; @@ -73,19 +74,15 @@ Cylon.robot = function robot(opts) { // // Returns nothing Cylon.api = function api(Server, opts) { - var isObject = function(arg) { return typeof arg === "object"; }, - isFunction = function(arg) { return typeof arg === "function"; }, - isString = function(arg) { return typeof arg === "string"; }; - // if only passed options (or nothing), assume HTTP server - if (Server == null || isObject(Server) && !isFunction(Server)) { + if (Server == null || _.isObject(Server) && !_.isFunction(Server)) { opts = Server; Server = "http"; } opts = opts || {}; - if (isString(Server)) { + if (_.isString(Server)) { var req = "cylon-api-" + Server; try { @@ -106,9 +103,7 @@ Cylon.api = function api(Server, opts) { ]; } - messages.forEach(function(str) { - Logger.error(str); - }); + _.each(messages, _.arity(Logger.error, 1)); return; } else { throw e; @@ -126,22 +121,13 @@ Cylon.api = function api(Server, opts) { // // Returns nothing Cylon.start = function start() { - var starters = [], - name; - - for (name in this.robots) { - var bot = this.robots[name]; - starters.push(bot.start.bind(bot)); - } + var starters = _.pluck(this.robots, "start"); Async.parallel(starters, function() { var mode = Utils.fetch(Config, "workMode", "async"); if (mode === "sync") { - for (name in this.robots) { - var bot = this.robots[name]; - bot.startWork.call(bot); - } + _.invoke(this.robots, "startWork"); } }.bind(this)); }; @@ -153,11 +139,11 @@ Cylon.start = function start() { // Returns the current config Cylon.config = function(opts) { var loggingChanged = ( - opts.logging && Config.logging !== Utils.merge(Config.logging, opts.logging) + opts.logging && Config.logging !== _.extend(Config.logging, opts.logging) ); - if (typeof opts === "object" && !Array.isArray(opts)) { - Config = Utils.merge(Config, opts); + if (_.isObject(opts)) { + Config = _.extend(Config, opts); } if (loggingChanged) { @@ -175,12 +161,7 @@ Cylon.config = function(opts) { Cylon.halt = function halt(callback) { callback = callback || function() {}; - var fns = []; - - for (var name in this.robots) { - var bot = this.robots[name]; - fns.push(bot.halt.bind(bot)); - } + var fns = _.pluck(this.robots, "halt"); // if robots can"t shut down quickly enough, forcefully self-terminate var timeout = Config.haltTimeout || 3000; @@ -190,15 +171,8 @@ Cylon.halt = function halt(callback) { }; Cylon.toJSON = function() { - var robots = []; - - for (var name in this.robots) { - var bot = this.robots[name]; - robots.push(bot.toJSON.call(bot)); - } - return { - robots: robots, + robots: _.invoke(this.robots, "toJSON"), commands: Object.keys(this.commands), events: this.events }; diff --git a/lib/driver.js b/lib/driver.js index 21725ec..abd8328 100644 --- a/lib/driver.js +++ b/lib/driver.js @@ -9,7 +9,8 @@ "use strict"; var Basestar = require("./basestar"), - Utils = require("./utils"); + Utils = require("./utils"), + _ = require("./utils/helpers"); // Public: Creates a new Driver // @@ -35,14 +36,13 @@ var Driver = module.exports = function Driver(opts) { this.details = {}; - for (var name in opts) { - var opt = opts[name], - banned = ["robot", "name", "connection", "driver", "events"]; + _.each(opts, function(opt, name) { + var banned = ["robot", "name", "connection", "driver", "events"]; if (!~banned.indexOf(name)) { this.details[name] = opt; } - } + }, this); }; Utils.subclass(Driver, Basestar); diff --git a/lib/initializer.js b/lib/initializer.js index 0f75ce9..eb414c1 100644 --- a/lib/initializer.js +++ b/lib/initializer.js @@ -9,14 +9,15 @@ "use strict"; var Registry = require("./registry"), - Config = require("./config"); + Config = require("./config"), + _ = require("./utils/helpers"); function testMode() { return process.env.NODE_ENV === "test" && Config.testMode; } module.exports = function Initializer(type, opts) { - var mod, name, prop; + var mod; mod = Registry.findBy(type, opts[type]); @@ -32,28 +33,24 @@ module.exports = function Initializer(type, opts) { var obj = mod[type](opts); - for (name in obj) { - prop = obj[name]; - + _.each(obj, function(prop, name) { if (name === "constructor") { - continue; + return; } - if (typeof prop === "function") { + if (_.isFunction(prop)) { obj[name] = prop.bind(obj); } - } + }); if (testMode()) { var test = Registry.findBy(type, "test")[type](opts); - for (name in obj) { - prop = obj[name]; - - if (typeof prop === "function" && !test[name]) { + _.each(obj, function(prop, name) { + if (_.isFunction(prop) && !test[name]) { test[name] = function() { return true; }; } - } + }); return test; } diff --git a/lib/logger.js b/lib/logger.js index 5c1dc50..e79b6b2 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -13,12 +13,12 @@ var levels = ["debug", "info", "warn", "error", "fatal"]; var BasicLogger = require("./logger/basic_logger"), NullLogger = require("./logger/null_logger"), Config = require("./config"), - Utils = require("./utils"); + _ = require("./utils/helpers"); var Logger = module.exports = { setup: function(opts) { - if (typeof opts === "object") { - Config.logging = Utils.merge(Config.logging, opts); + if (_.isObject(opts)) { + Config.logging = _.extend(Config.logging, opts); } var logger = Config.logging.logger, diff --git a/lib/robot.js b/lib/robot.js index 7a8213d..2796e67 100644 --- a/lib/robot.js +++ b/lib/robot.js @@ -11,7 +11,8 @@ var initializer = require("./initializer"), Logger = require("./logger"), Utils = require("./utils"), - Config = require("./config"); + Config = require("./config"), + _ = require("./utils/helpers"); var Async = require("async"), EventEmitter = require("events").EventEmitter; @@ -48,30 +49,28 @@ var Robot = module.exports = function Robot(opts) { this.initConnections(opts); this.initDevices(opts); - for (var name in opts) { - var opt = opts[name]; - + _.each(opts, function(opt, name) { if (this[name] !== undefined) { - continue; + return; } this[name] = opt; - if (opts.commands == null && typeof opt === "function") { + if (opts.commands == null && _.isFunction(opt)) { this.commands[name] = opt; } - } + }, this); if (opts.commands) { var cmds; - if (typeof opts.commands === "function") { + if (_.isFunction(opts.commands)) { cmds = opts.commands.call(this); } else { cmds = opts.commands; } - if (typeof cmds === "object" && !Array.isArray(cmds)) { + if (_.isObject(cmds)) { this.commands = cmds; } else { var err = "#commands must be an object "; @@ -101,26 +100,12 @@ Robot.randomName = function() { // // Returns an Object containing Robot data Robot.prototype.toJSON = function() { - var connections = [], - devices = [], - n; - - for (n in this.connections) { - var conn = this.connections[n]; - connections.push(conn.toJSON.call(conn)); - } - - for (n in this.devices) { - var device = this.devices[n]; - devices.push(device.toJSON.call(device)); - } - return { name: this.name, - connections: connections, - devices: devices, + connections: _.invoke(this.connections, "toJSON"), + devices: _.invoke(this.devices, "toJSON"), commands: Object.keys(this.commands), - events: Array.isArray(this.events) ? this.events : [] + events: _.isArray(this.events) ? this.events : [] }; }; @@ -182,30 +167,28 @@ Robot.prototype.initConnections = function(opts) { return this.connections; } - if (typeof opts.connections === "object") { - if (Array.isArray(opts.connections)) { + if (_.isObjectLoose(opts.connections)) { + if (_.isArray(opts.connections)) { this.performArraySetup(opts.connections, "connection", "connections"); return this.connections; } - for (var key in opts.connections) { - var conn = opts.connections[key], - name = typeof key === "string" ? key : conn.name; + _.each(opts.connections, function(conn, key) { + var name = _.isString(key) ? key : conn.name; if (conn.devices) { opts.devices = opts.devices || {}; - for (var d in conn.devices) { - var device = conn.devices[d]; + _.each(conn.devices, function(device, d) { device.connection = name; opts.devices[d] = device; - } + }); delete conn.devices; } this.connection(name, conn); - } + }, this); } return this.connections; @@ -226,7 +209,7 @@ Robot.prototype.device = function(name, device) { this.log("warn", str); } - if (typeof device.connection === "string") { + if (_.isString(device.connection)) { if (this.connections[device.connection] == null) { str = "No connection found with the name " + device.connection + ".\n"; this.log("fatal", str); @@ -269,18 +252,16 @@ Robot.prototype.initDevices = function(opts) { return this.devices; } - if (typeof opts.devices === "object") { - if (Array.isArray(opts.devices)) { + if (_.isObjectLoose(opts.devices)) { + if (_.isArray(opts.devices)) { this.performArraySetup(opts.devices, "device", "devices"); return this.devices; } - for (var key in opts.devices) { - var device = opts.devices[key], - name = typeof key === "string" ? key : device.name; - + _.each(opts.devices, function(device, key) { + var name = _.isString(key) ? key : device.name; this.device(name, device); - } + }, this); } return this.devices; @@ -314,7 +295,7 @@ Robot.prototype.start = function(callback) { this.log("fatal", err); this.halt(function() { - if (typeof(this.error) === "function") { + if (_.isFunction(this.error)) { this.error.call(this, err); } @@ -324,7 +305,7 @@ Robot.prototype.start = function(callback) { }.bind(this)); } - if (typeof callback === "function") { + if (_.isFunction(callback)) { callback(err, results); } }.bind(this)); @@ -353,11 +334,7 @@ Robot.prototype.startWork = function() { Robot.prototype.startConnections = function(callback) { this.log("info", "Starting connections."); - var starters = []; - - var createStarter = function(name) { - var conn = this.connections[name]; - + var starters = _.map(this.connections, function(conn, name) { this[name] = conn; return function(cb) { @@ -372,11 +349,7 @@ Robot.prototype.startConnections = function(callback) { this.log("debug", str + "."); return conn.connect.call(conn, cb); }.bind(this); - }.bind(this); - - for (var name in this.connections) { - starters.push(createStarter(name)); - } + }, this); return Async.parallel(starters, callback); }; @@ -391,11 +364,7 @@ Robot.prototype.startDevices = function(callback) { log("info", "Starting devices."); - var starters = []; - - var createStarter = function(name) { - var device = this.devices[name]; - + var starters = _.map(this.devices, function(device, name) { this[name] = device; return function(cb) { @@ -408,12 +377,7 @@ Robot.prototype.startDevices = function(callback) { log("debug", str + "."); return device.start.call(device, cb); }; - - }.bind(this); - - for (var name in this.devices) { - starters.push(createStarter(name)); - } + }, this); return Async.parallel(starters, callback); }; @@ -432,19 +396,8 @@ Robot.prototype.halt = function(callback) { return callback(); } - var devices = [], - connections = [], - n; - - for (n in this.devices) { - var device = this.devices[n]; - devices.push(device.halt.bind(device)); - } - - for (n in this.connections) { - var conn = this.connections[n]; - connections.push(conn.disconnect.bind(conn)); - } + var devices = _.pluck(this.devices, "halt"), + connections = _.pluck(this.connections, "disconnect"); try { Async.parallel(devices, function() { @@ -481,7 +434,7 @@ Robot.prototype.performArraySetup = function(things, typeOfThing, arrayName) { this.log("warn", str); things.forEach(function(t, key) { - var name = typeof key === "string" ? key : t.name; + var name = _.isString(key) === "string" ? key : t.name; this[typeOfThing](name, t); }, this); }; diff --git a/lib/utils.js b/lib/utils.js index 577f8d9..fc0a411 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -8,6 +8,8 @@ "use strict"; +var _ = require("./utils/helpers"); + var monkeyPatches = require("./utils/monkey-patches"); var Utils = module.exports = { @@ -106,13 +108,11 @@ var Utils = module.exports = { }, proxyFunctions: function proxyFunctions(source, target) { - for (var key in source) { - var prop = source[key]; - - if (typeof prop === "function" && !target[key]) { + _.each(source, function(prop, key) { + if (_.isFunction(prop) && !target[key]) { target[key] = prop.bind(source); } - } + }); }, // Public: Proxies a list of methods from one object to another. It will not @@ -133,7 +133,7 @@ var Utils = module.exports = { force = force || false; methods.forEach(function(method) { - if (typeof base[method] === "function" && !force) { + if (_.isFunction(base[method]) && !force) { return; } @@ -187,7 +187,7 @@ var Utils = module.exports = { throw new Error("key not found: \"" + property + "\""); } - if (typeof(fallback) === "function") { + if (_.isFunction(fallback)) { var value = fallback(property); if (value === void 0) { @@ -200,51 +200,6 @@ var Utils = module.exports = { return fallback; }, - // Public: Merges two arrays/objects together, recursively. - // - // base - what should be merged onto - // source - what should be merged into it - // - // Returns a merged object/array - merge: function(base, source) { - var isArray = Array.isArray(source); - - if (base == null) { - base = isArray ? [] : {}; - } - - // merge objects - if (isArray) { - source.forEach(function(e, i) { - if (typeof base[i] === "undefined") { - base[i] = e; - } else if (typeof e === "object") { - base[i] = Utils.merge(base[i], e); - } else { - if (!~base.indexOf(e)) { - base.push(e); - } - } - }); - } else { - var key; - - for (key in source) { - if (typeof source[key] !== "object" || !source[key]) { - base[key] = source[key]; - } else { - if (base[key]) { - Utils.merge(base[key], source[key]); - } else { - base[key] = source[key]; - } - } - } - } - - return base; - }, - // Public: Given a name, and an array of existing names, returns a unique // name. // diff --git a/lib/utils/helpers.js b/lib/utils/helpers.js new file mode 100644 index 0000000..9c93b37 --- /dev/null +++ b/lib/utils/helpers.js @@ -0,0 +1,156 @@ +// A collection of useful helper functions, used internally but not exported +// with the rest of Cylon. + +"use strict"; + +var __slice = Array.prototype.slice; + +var H = module.exports = {}; + +function identity(value) { + return value; +} + +function extend(base, source) { + var isArray = Array.isArray(source); + + if (base == null) { + base = isArray ? [] : {}; + } + + if (isArray) { + source.forEach(function(e, i) { + if (typeof base[i] === "undefined") { + base[i] = e; + } else if (typeof e === "object") { + base[i] = extend(base[i], e); + } else { + if (!~base.indexOf(e)) { + base.push(e); + } + } + }); + } else { + var key; + + for (key in source) { + if (typeof source[key] !== "object" || !source[key]) { + base[key] = source[key]; + } else { + if (base[key]) { + extend(base[key], source[key]); + } else { + base[key] = source[key]; + } + } + } + } + + return base; +} + +extend(H, { + identity: identity, + extend: extend +}); + +function kind(thing) { + return Object.prototype.toString.call(thing).slice(8, -1); +} + +function isA(type) { + return function(thing) { + return kind(thing) === type; + }; +} + +extend(H, { + isObject: isA("Object"), + isObjectLoose: function(thing) { return typeof thing === "object"; }, + isFunction: isA("Function"), + isArray: isA("Array"), + isString: isA("String"), + isNumber: isA("Number"), + isArguments: isA("Arguments"), + isUndefined: isA("Undefined") +}); + +function iterate(thing, fn, thisVal) { + if (H.isArray(thing)) { + thing.forEach(fn, thisVal); + return; + } + + if (H.isObject(thing)) { + Object.keys(thing).forEach(function(key) { + var value = thing[key]; + fn.call(thisVal, value, key); + }, thisVal); + } + + return []; +} + +function pluck(collection, key) { + var keys = []; + + iterate(collection, function(object) { + if (H.isObject(object)) { + if (H.isFunction(object[key])) { + keys.push(object[key].bind(object)); + } else { + keys.push(object[key]); + } + } + }); + + return keys; +} + +function map(collection, fn, thisVal) { + var vals = []; + + if (fn == null) { + fn = identity; + } + + iterate(collection, function(object, index) { + vals.push(fn.call(thisVal, object, index)); + }); + + return vals; +} + +function invoke(collection, fn) { + var args = __slice.call(arguments, 2), + vals = []; + + iterate(collection, function(object) { + if (H.isFunction(fn)) { + vals.push(fn.apply(object, args)); + return; + } + + vals.push(object[fn].apply(object, arguments)); + }); + + return vals; +} + +extend(H, { + pluck: pluck, + each: iterate, + map: map, + invoke: invoke +}); + +function arity(fn, n) { + return function() { + var args = __slice.call(arguments, 0, n); + return fn.apply(null, args); + }; +} + +extend(H, { + arity: arity +}); diff --git a/spec/lib/utils.spec.js b/spec/lib/utils.spec.js index 530f5a2..4977907 100644 --- a/spec/lib/utils.spec.js +++ b/spec/lib/utils.spec.js @@ -252,37 +252,4 @@ describe("Utils", function() { }); }); }); - - describe("#merge", function() { - var merge = utils.merge; - - var base = { - fruits: ["apple"], - vegetables: ["beet"], - thing: null, - otherThing: "hello!", - data: [{ "user": "barney" }, { "user": "fred" }] - }; - - var source = { - fruits: ["banana"], - vegetables: ["carrot"], - thing: "hello!", - otherThing: null, - data: [{ "age": 36 }, { "age": 40 }] - }; - - var expected = { - data: [ { age: 36, user: "barney" }, { age: 40, user: "fred" } ], - fruits: [ "apple", "banana" ], - vegetables: [ "beet", "carrot" ], - thing: "hello!", - otherThing: null - }; - - it("merges two objects", function() { - var merged = merge(base, source); - expect(merged).to.be.eql(expected); - }); - }); }); diff --git a/spec/lib/utils/helpers.spec.js b/spec/lib/utils/helpers.spec.js new file mode 100644 index 0000000..45843ab --- /dev/null +++ b/spec/lib/utils/helpers.spec.js @@ -0,0 +1,223 @@ +"use strict"; + +var _ = source("utils/helpers"); + +describe("Helpers", function() { + describe("extend", function() { + var extend = _.extend; + + var base = { + fruits: ["apple"], + vegetables: ["beet"], + thing: null, + otherThing: "hello!", + data: [{ "user": "barney" }, { "user": "fred" }] + }; + + var source = { + fruits: ["banana"], + vegetables: ["carrot"], + thing: "hello!", + otherThing: null, + data: [{ "age": 36 }, { "age": 40 }] + }; + + var expected = { + data: [ { age: 36, user: "barney" }, { age: 40, user: "fred" } ], + fruits: [ "apple", "banana" ], + vegetables: [ "beet", "carrot" ], + thing: "hello!", + otherThing: null + }; + + it("extends two objects", function() { + var extended = extend(base, source); + expect(extended).to.be.eql(expected); + }); + }); + + describe("isObject", function() { + var fn =_.isObject; + + it("checks if a value is an Object", function() { + var Klass = function() {}, + instance = new Klass(); + + expect(fn({})).to.be.eql(true); + expect(fn(instance)).to.be.eql(true); + + expect(fn([])).to.be.eql(false); + expect(fn(function() {})).to.be.eql(false); + expect(fn(10)).to.be.eql(false); + expect(fn("")).to.be.eql(false); + }); + }); + + describe("isFunction", function() { + var fn =_.isFunction; + + it("checks if a value is a Function", function() { + expect(fn(function() {})).to.be.eql(true); + + expect(fn({})).to.be.eql(false); + expect(fn([])).to.be.eql(false); + expect(fn(10)).to.be.eql(false); + expect(fn("")).to.be.eql(false); + }); + }); + + describe("isArray", function() { + var fn = _.isArray; + + it("checks if a value is an Array", function() { + expect(fn([])).to.be.eql(true); + + expect(fn(function() {})).to.be.eql(false); + expect(fn({})).to.be.eql(false); + expect(fn(10)).to.be.eql(false); + expect(fn("")).to.be.eql(false); + }); + }); + + describe("isNumber", function() { + var fn = _.isNumber; + + it("checks if a value is a Number", function() { + expect(fn(10)).to.be.eql(true); + + expect(fn(function() {})).to.be.eql(false); + expect(fn({})).to.be.eql(false); + expect(fn([])).to.be.eql(false); + expect(fn("")).to.be.eql(false); + }); + }); + + describe("isString", function() { + var fn = _.isString; + + it("checks if a value is a String", function() { + expect(fn("")).to.be.eql(true); + + expect(fn(10)).to.be.eql(false); + expect(fn(function() {})).to.be.eql(false); + expect(fn({})).to.be.eql(false); + expect(fn([])).to.be.eql(false); + }); + }); + + describe("#pluck", function() { + var object = { a: { item: "hello" }, b: { item: "world" } }, + array = [ { item: "hello" }, { item: "world" } ]; + + it("plucks values from a collection", function() { + expect(_.pluck(object, "item")).to.be.eql(["hello", "world"]); + expect(_.pluck(array, "item")).to.be.eql(["hello", "world"]); + }); + }); + + describe("#map", function() { + var object = { a: { item: "hello" }, b: { item: "world" } }, + array = [ { item: "hello" }, { item: "world" } ]; + + var fn = function(value, key) { + return [value, key]; + }; + + it("runs a function over items in a collection", function() { + expect(_.map(object, fn)).to.be.eql([ + [{ item: "hello" }, "a"], + [{ item: "world" }, "b"] + ]); + + expect(_.map(array, fn)).to.be.eql([ + [{ item: "hello" }, 0], + [{ item: "world" }, 1] + ]); + }); + + it("defaults to the identity function", function() { + expect(_.map(array)).to.be.eql(array); + expect(_.map(object)).to.be.eql(array); + }); + }); + + describe("#invoke", function() { + var array = [ + { + name: "bob", + toString: function() { + return "Hi from " + this.name; + } + }, + { + name: "dave", + toString: function() { + return "hello from " + this.name; + } + } + ]; + + var object = { + bob: { + name: "bob", + toString: function() { + return "Hi from " + this.name; + } + }, + dave: { + name: "dave", + toString: function() { + return "hello from " + this.name; + } + } + }; + + it("runs a instance function over items in a collection", function() { + expect(_.invoke(object, "toString")).to.be.eql([ + "Hi from bob", + "hello from dave" + ]); + + expect(_.invoke(array, "toString")).to.be.eql([ + "Hi from bob", + "hello from dave" + ]); + + expect(_.invoke([1, 2, 3, 4, 5], Number.prototype.toString)).to.be.eql([ + "1", "2", "3", "4", "5" + ]); + }); + }); + + describe("#each", function() { + var object = { a: { item: "hello" }, b: { item: "world" } }, + array = [ { item: "hello" }, { item: "world" } ]; + + var fn = function(value, key) { + return [value, key]; + }; + + it("runs a function over items in a collection", function() { + fn = spy(); + _.map(object, fn); + + expect(fn).to.be.calledWith(object.a, "a"); + expect(fn).to.be.calledWith(object.b, "b"); + + fn = spy(); + _.map(array, fn); + + expect(fn).to.be.calledWith(array[0], 0); + expect(fn).to.be.calledWith(array[1], 1); + }); + }); + + describe("#arity", function() { + it("creates a function that only takes a certain # of args", function() { + var fn = spy(); + var one = _.arity(fn, 1); + one("one", "two", "three"); + expect(fn).to.be.calledWith("one"); + }); + }); +});