diff --git a/lib/and.js b/lib/and.js new file mode 100644 index 0000000..3b5c13e --- /dev/null +++ b/lib/and.js @@ -0,0 +1,20 @@ +module.exports = require('./variadic')(and) + +function and(x, rest) { + if (isEmpty(arguments)) return true + + var result = val(x) + + if (isnt(result) || isnt(rest)) return result + + rest.every(function(x) { + return is(result = val(x)) + }) + + return result +} + +const isEmpty = require('./isEmpty') + , isnt = require('./isnt') + , val = require('./val') + , is = require('./is') \ No newline at end of file diff --git a/lib/apply.js b/lib/apply.js index ceb9476..06de333 100644 --- a/lib/apply.js +++ b/lib/apply.js @@ -2,8 +2,7 @@ module.exports = apply function apply(fn, args) { if (isnt(Function, fn)) throw new TypeError('First argument must be a function.') - - fn.apply(this, args) + return fn.apply(this, args) } const isnt = require('./isnt') \ No newline at end of file diff --git a/lib/assert.js b/lib/assert.js new file mode 100644 index 0000000..b016f25 --- /dev/null +++ b/lib/assert.js @@ -0,0 +1,15 @@ +module.exports = assert + +function assert(x, message, type) { + if (isnt(x)) { + message = val(message) + isnt(message) && (message = "Assertion failed.") + throw is(Error, message)? message : Error(message) + } + + return x +} + +const isnt = require("./isnt") + , val = require("./val") + , is = require("./is") \ No newline at end of file diff --git a/lib/defprop.js b/lib/defprop.js new file mode 100644 index 0000000..b6a40a7 --- /dev/null +++ b/lib/defprop.js @@ -0,0 +1,52 @@ +module.exports = defprop + +function defprop(target, fields, opts) { + if (isnt(target)) throw TypeError("Target must exist.") + if (isnt(fields)) throw TypeError("Fields must exist.") + + opts || (opts = {}) + + const names = Object.getOwnPropertyNames(fields) + + const isMutable = get(opts, "^writable", false) + , isEnum = get(opts, "^enumerable", false) + , isConf = get(opts, "^configurable", false) + , scope = get(opts, "^bind", false) + + return Object.defineProperties(target, + names.reduce(function(p, k) { + const prop = describe(fields, k) + , opt = $(get, opts[k]) + + if (isnt(prop.get) && isnt(prop.set)) { + prop.writable = opt("writable", isMutable) + + when(is(Function, prop.value) && opt("bind", scope), function(scope) { + prop.value = prop.value.bind(scope) + }) + } else { + when(prop.get && opt("writable") && !prop.set, function() { + throw TypeError("The accessor `"+k+"` can't be writable without a setter.") + }) + + when(opt("bind", scope), function(scope) { + prop.get && (prop.get = prop.get.bind(scope)) + prop.set && (prop.set = prop.set.bind(scope)) + }) + } + + prop.enumerable = opt("enumerable", isEnum) + prop.configurable = opt("configurable", isConf) + + return (p[k] = prop), p + }, {}) + ) +} + +const describe = Object.getOwnPropertyDescriptor + , isEmpty = require("./isEmpty") + , isnt = require("./isnt") + , when = require("./when") + , get = require("./get") + , is = require("./is") + , $ = require("./partial") \ No newline at end of file diff --git a/lib/describe.js b/lib/describe.js new file mode 100644 index 0000000..7434248 --- /dev/null +++ b/lib/describe.js @@ -0,0 +1,28 @@ +module.exports = require("./variadic")(describe) + +function describe(obj, fields) { + assert(is(Object.prototype, obj), TypeError("Can't describe non-object: " + src(obj))) + + const descprop = $(Object.getOwnPropertyDescriptor, obj) + + if (is(fields)) { + const hasprop = obj.hasOwnProperty.bind(obj) + , props = [] + + return fields.map(function(name) { + assert(hasprop(name), TypeError("Can't describe non-existant property: " + name)) + return [name, descprop(name)] + }) + } else { + return names(obj).map(function(name) { + return [name, descprop(name)] + }, {}) + } +} + +const assert = require("./assert") + , names = Object.getOwnPropertyNames + , each = require("./each") + , src = require("./src") + , is = require("./is") + , $ = require("./partial") \ No newline at end of file diff --git a/lib/extend.js b/lib/extend.js new file mode 100644 index 0000000..02a7a74 --- /dev/null +++ b/lib/extend.js @@ -0,0 +1,23 @@ +module.exports = require("./variadic")(extend) + +function extend(base, rest) { + assert(isExtendable(base), TypeError("Base must be an extendable object.")) + assert(is(rest), TypeError("At least one extension object must be defined.")) + + const traits = describe(apply(merge, rest)).reduce(function(traits, prop) { + traits[prop[0]] = prop[1] + return traits + }, {}) + + return Object.create(base, traits) +} + +const describe = require("./describe") + , assert = require("./assert") + , apply = require("./apply") + , merge = require("./merge") + , comp = require("./comp") + , is = require("./is") + , $ = require("./partial") + +const isExtendable = $(is, Object.prototype) \ No newline at end of file diff --git a/lib/get.js b/lib/get.js new file mode 100644 index 0000000..dc277b6 --- /dev/null +++ b/lib/get.js @@ -0,0 +1,11 @@ +module.exports = get + +function get(map, key, notFound) { + if (isnt(map)) return notFound + if (!map.hasOwnProperty(key)) return notFound + + return map[key] +} + +const isnt = require('./isnt') + , is = require('./is') \ No newline at end of file diff --git a/lib/is.js b/lib/is.js index 3c89ea9..2b491a6 100644 --- a/lib/is.js +++ b/lib/is.js @@ -5,6 +5,8 @@ function is(c, x) { return !not(c) } + if (c === x) return true + const cs = lowerCase(c) if (cs === 'null') { @@ -18,7 +20,7 @@ function is(c, x) { } else if (type(c) === 'string') { return cs === type(x) } else { - return is(x) && x.constructor === c || c.isPrototypeOf(x) + return (x != null && x.constructor === c) || c.isPrototypeOf(x) } } diff --git a/lib/merge.js b/lib/merge.js new file mode 100644 index 0000000..b887828 --- /dev/null +++ b/lib/merge.js @@ -0,0 +1,18 @@ +module.exports = require("./variadic")(merge) + +function merge(objects) { + if (isEmpty(objects)) return + + return create(Object.prototype, objects.reduce(function(fields, t) { + names(t).forEach(function(k) { + fields[k] = describe(t, k) + }) + + return fields + }, {})) +} + +const describe = Object.getOwnPropertyDescriptor + , isEmpty = require("./isEmpty") + , create = Object.create + , names = Object.getOwnPropertyNames \ No newline at end of file diff --git a/lib/partial.js b/lib/partial.js index 30c83f2..af5084a 100644 --- a/lib/partial.js +++ b/lib/partial.js @@ -1,12 +1,17 @@ -module.exports = partial +const $ = module.exports = require("./variadic")(partial) -function partial(fun, args) { - if (arguments.length === 1) return fun +function partial(fun, part) { + if (isnt(part)) return fun - const part = [].slice.call(arguments, 1) + return variadic(function(rest) { + const args = part.map(function(arg) { + return arg === $? rest.shift() : arg + }) - return function partial() { - const rest = [].slice.call(arguments) - return fun.apply(this, part.concat(rest)) - } -} \ No newline at end of file + return fun.apply(this, args.concat(rest)) + }) +} + +const variadic = require("./variadic") + , each = require("./each") + , isnt = require("./isnt") \ No newline at end of file diff --git a/lib/thunk.js b/lib/thunk.js index 4d253ff..f64c530 100644 --- a/lib/thunk.js +++ b/lib/thunk.js @@ -1,19 +1,12 @@ module.exports = require('./variadic')(thunk) function thunk(fn, rest) { - if (isnt(Function, fn)) { - rest = [fn] - fn = identity - } + if (isnt(Function, fn)) throw new TypeError('Expected `fn` to be a function.') - if (rest.length > 1) { + if (is(rest)) { return function thunk() { return fn.apply(this, rest) } - } else if (rest.length) { - return function thunk() { - return fn.call(this, rest[0]) - } } else { return function thunk() { return fn.call(this) @@ -23,4 +16,5 @@ function thunk(fn, rest) { const identity = require('./identity') , slice = require('./slice') - , isnt = require('./isnt') \ No newline at end of file + , isnt = require('./isnt') + , is = require('./is') \ No newline at end of file diff --git a/lib/val.js b/lib/val.js new file mode 100644 index 0000000..d7a9c49 --- /dev/null +++ b/lib/val.js @@ -0,0 +1,8 @@ +module.exports = require('./variadic')(val) + +function val(x, rest) { + return is(Function, x)? apply(x, rest) : x +} + +const apply = require('./apply') + , is = require('./is') \ No newline at end of file diff --git a/lib/variadic.js b/lib/variadic.js index f2d2e27..4634a17 100644 --- a/lib/variadic.js +++ b/lib/variadic.js @@ -2,23 +2,49 @@ module.exports = variadic function variadic(fn) { if (isnt(Function, fn)) throw new TypeError('Parameter `fn` must be a function.') + + const argc = fn.length - 1 - const argc = fn.length - 1 - - if (argc) { - return function variadic() { - const head = slice(arguments, 0, argc) - , tail = slice(arguments, argc) - - return fn.apply(this, head.concat([tail])) - } - } else { - return function variadic() { - const rest = slice(arguments) - return fn.call(this, rest) - } + switch (argc) { + case -1 : return fn + case 0 : return function (rest) { return apply(fn, this, argc, slice(arguments)) } + case 1 : return function (a, rest) { return apply(fn, this, argc, slice(arguments)) } + case 2 : return function (a, b, rest) { return apply(fn, this, argc, slice(arguments)) } + case 3 : return function (a, b, c, rest) { return apply(fn, this, argc, slice(arguments)) } + case 4 : return function (a, b, c, d, rest) { return apply(fn, this, argc, slice(arguments)) } + case 5 : return function (a, b, c, d, e, rest) { return apply(fn, this, argc, slice(arguments)) } + case 6 : return function (a, b, c, d, e, f, rest) { return apply(fn, this, argc, slice(arguments)) } + case 7 : return function (a, b, c, d, e, f, g, rest) { return apply(fn, this, argc, slice(arguments)) } + case 8 : return function (a, b, c, d, e, f, g, h, rest) { return apply(fn, this, argc, slice(arguments)) } + case 9 : return function (a, b, c, d, e, f, g, h, i, rest) { return apply(fn, this, argc, slice(arguments)) } + case 10 : return function (a, b, c, d, e, f, g, h, i, j, rest) { return apply(fn, this, argc, slice(arguments)) } + case 11 : return function (a, b, c, d, e, f, g, h, i, j, k, rest) { return apply(fn, this, argc, slice(arguments)) } + case 12 : return function (a, b, c, d, e, f, g, h, i, j, k, l, rest) { return apply(fn, this, argc, slice(arguments)) } + case 13 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, rest) { return apply(fn, this, argc, slice(arguments)) } + case 14 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, rest) { return apply(fn, this, argc, slice(arguments)) } + case 15 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, rest) { return apply(fn, this, argc, slice(arguments)) } + case 16 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, rest) { return apply(fn, this, argc, slice(arguments)) } + case 17 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, rest) { return apply(fn, this, argc, slice(arguments)) } + case 18 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, rest) { return apply(fn, this, argc, slice(arguments)) } + case 19 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, rest) { return apply(fn, this, argc, slice(arguments)) } + case 20 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, rest) { return apply(fn, this, argc, slice(arguments)) } + case 21 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, rest) { return apply(fn, this, argc, slice(arguments)) } + case 22 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, rest) { return apply(fn, this, argc, slice(arguments)) } + case 23 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, rest) { return apply(fn, this, argc, slice(arguments)) } + case 24 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, rest) { return apply(fn, this, argc, slice(arguments)) } + case 25 : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, rest) { return apply(fn, this, argc, slice(arguments)) } + default : return function (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, rest) { return apply(fn, this, argc, slice(arguments)) } } } -const slice = require('./slice') - , isnt = require('./isnt') \ No newline at end of file +function apply(fn, host, argc, argv) { + const rest = slice(argv, argc) + argv = slice(argv, 0, argc) + return isEmpty(rest)? fn.apply(host, argv) : fn.apply(host, argv.concat([rest])) +} + +const isEmpty = require('./isEmpty') + , slice = require('./slice') + , isnt = require('./isnt') + , min = require('./min') + , max = Math.max \ No newline at end of file diff --git a/lib/when.js b/lib/when.js new file mode 100644 index 0000000..c2fcab9 --- /dev/null +++ b/lib/when.js @@ -0,0 +1,9 @@ +module.exports = when + +function when(cond, body) { + const it = val(cond) + return is(it)? val(body, it) : null +} + +const val = require('./val') + , is = require('./is') diff --git a/test/and.js b/test/and.js new file mode 100644 index 0000000..fee5e6e --- /dev/null +++ b/test/and.js @@ -0,0 +1,50 @@ +const constantly = require('../lib/constantly') + , expect = require('chai').expect + , and = require('../lib/and') + +describe('`and`', function() { + describe('given zero arguments', function() { + it('should return `true`', function() { + expect(and()).to.equal(true) + }) + }) + + describe('given one argument', function() { + it('should return the value of that argument', function() { + expect(and(1)).to.equal(1) + expect(and(0)).to.equal(0) + expect(and(false)).to.equal(false) + }) + + describe('and when that argument is a function', function() { + it('should return the value of calling that function', function() { + expect(and(constantly('wibble'))).to.equal('wibble') + }) + }) + }) + + describe('given two or more arguments', function() { + describe('when the values of all arguments are logically true', function() { + it('should return the last supplied value', function() { + expect(and(0, 1, 2, 3)).to.equal(3) + }) + }) + + describe('when any one argument is logically false', function() { + it('should return the value that was logically false', function() { + expect(and(0, null, 1)).to.equal(null) + expect(and(false, true, 1)).to.equal(false) + expect(and(0, true, undefined)).to.equal(undefined) + }) + }) + + describe('when an argument is a function', function() { + it('should call the function and evaluate its return value', function() { + var called = false + + expect(and(0, function() { return (called = true) }, 1, 'yup')).to.equal('yup') + expect(called).to.be.true + }) + }) + }) +}) \ No newline at end of file diff --git a/test/apply.js b/test/apply.js index 3fc100b..c5362cf 100644 --- a/test/apply.js +++ b/test/apply.js @@ -1,6 +1,12 @@ -describe('apply', function() { - describe('when not given a function', function() { - it('should throw a TypeError', function() { +const expect = require("chai").expect + , slice = require("../lib/slice") + , apply = require("../lib/apply") + , each = require("../lib/each") + , $ = require("../lib/partial") + +describe("`apply`", function() { + describe("given a non-function", function() { + it("should throw a TypeError", function() { each( [ 0 , 1 @@ -12,68 +18,77 @@ describe('apply', function() { , undefined ] , function(nonfn) { - expect(function() { - apply(nonfn) - }).to.throw(TypeError) + expect($(apply, nonfn)).to.throw(TypeError) } ) }) }) - describe('when given a function `fn`', function() { - describe('and no arguments', function(done) { - it('should call the function without arguments', function(done) { - const fn = function() { - expect(arguments.length).to.equal(0) - done() - } + describe("given a function `fn`", function() { + describe("and no arguments", function(done) { + describe("when called", function() { + it("should not pass any arguments", function(done) { + apply(fn) - apply(fn) - }) + function fn() { + expect(arguments.length).to.equal(0) + done() + } + }) - describe('and `fn` is bound', function() { - it('should not affect the binding', function(done) { - const owner = {} + it("should return the correct return value", function() { + expect(apply(fn)).to.equal("wibble") - const fn = function() { + function fn() { expect(arguments.length).to.equal(0) - expect(this).to.equal(owner) - done() + return "wibble" } + }) - apply(fn.bind(owner)) + it("should not affect the binding of `fn`", function() { + const that = { wibble: "wobble" } + expect(apply(fn.bind(that))).to.equal(that) + + function fn() { + expect(arguments.length).to.equal(0) + expect(this).to.equal(that) + return this + } }) }) }) - describe('and when given arguments', function(done) { - it('should call the function with arguments', function(done) { - const fn = function() { - expect(slice(arguments)).to.eql([1, true, 'wibble']) - done() - } + describe("and some arguments", function(done) { + describe("when called", function() { + it("should pass the arguments", function(done) { + apply(fn, [1, true, "wibble"]) - apply(fn, [1, true, 'wibble']) - }) + function fn() { + expect(slice(arguments)).to.eql([1, true, "wibble"]) + done() + } + }) - describe('and `fn` is bound', function() { - it('should not affect the binding', function(done) { - const owner = {} + it("should return the correct return value", function() { + expect(apply(fn, [1, true, "wibble"])).to.equal("bob") - const fn = function() { - expect(slice(arguments)).to.eql([1, true, 'wibble']) - expect(this).to.equal(owner) - done() + function fn() { + expect(slice(arguments)).to.eql([1, true, "wibble"]) + return "bob" } + }) + + it("should not affect the binding of `fn`", function() { + const that = { wibble: "wobble" } + expect(apply(fn.bind(that), [1, true, "wibble"])).to.equal("bob") - apply(fn.bind(owner), [1, true, 'wibble']) + function fn() { + expect(this).to.equal(that) + expect(slice(arguments)).to.eql([1, true, "wibble"]) + return "bob" + } }) }) }) }) -}) - -const expect = require('chai').expect - , slice = require('../lib/slice') - , apply = require('../lib/apply') - , each = require('../lib/each') \ No newline at end of file +}) \ No newline at end of file diff --git a/test/defprop.js b/test/defprop.js new file mode 100644 index 0000000..d816d7a --- /dev/null +++ b/test/defprop.js @@ -0,0 +1,142 @@ +const defprop = require("../lib/defprop") + , expect = require("chai").expect + , desc = Object.getOwnPropertyDescriptor + , each = require("../lib/each") + , $ = require("../lib/partial") + +describe("`defprop`", function() { + describe("given a target object", function() { + describe("which doesn't exist", function() { + it("should throw a TypeError", function() { + expect($(defprop, null)).to.throw(TypeError) + }) + }) + + describe("and an invalid set of fields", function() { + it("should throw a TypeError", function() { + each([null, undefined, false], function(fields) { + expect($(defprop, {}, fields)).to.throw(TypeError) + }) + }) + }) + + describe("and a valid set of fields", function() { + it("should mutate the target object", function() { + const target = {} + + expect(target).to.not.have.property('foo') + defprop(target, { foo: 'wobble' }) + expect(target).to.have.property('foo', 'wobble') + }) + + it("should return the mutated target object", function() { + const target = {} + + expect(target).to.not.have.property('foo') + expect(defprop(target, { foo: 'wobble' })).to.equal(target) + expect(target).to.have.property('foo', 'wobble') + }) + + it("should not make the fields configurable", function() { + const target = defprop({}, { foo: 'wobble' }) + expect(desc(target, 'foo')).to.have.property('configurable', false) + }) + + describe("when a field is a getter and/or setter, or the field is a function", function() { + describe("and the `^bind` option is set to an object", function() { + it("should bind the field to the object", function() { + const scope = {} + const target = defprop({}, + { get prop() { return this.wibble } + , set prop(x) { this.wibble = x } + , get scope() { return this } + , fun: function() { return this } + } + , { "^bind" : scope } + ) + + expect(target).to.not.equal(scope) + expect(target.scope).to.equal(scope) + + expect(target.prop).to.equal(undefined) + expect(scope).to.not.have.property('wibble') + expect(target).to.not.have.property('wibble') + + target.prop = 'wobble' + expect(target.prop).to.equal('wobble') + expect(scope).to.have.property('wibble', 'wobble') + expect(target).to.not.have.property('wibble') + + expect(target.fun()).to.equal(scope) + }) + }) + }) + + describe("when a field has no options", function() { + it("should make the field immutable", function() { + const target = defprop({}, { foo: 1 }) + expect(desc(target, 'foo')).to.have.property('writable', false) + }) + + it("should not make the field enumerable", function() { + const target = defprop({}, { foo: 1 }) + expect(desc(target, 'foo')).to.have.property('enumerable', false) + }) + }) + + describe("when a field is set to be writable", function() { + describe("and is just a getter", function() { + it("should throw a TypeError", function() { + const field = { get field() {} } + , opt = { field: { writable : true } } + + expect($(defprop, {}, field, opt)).to.throw(TypeError) + }) + }) + + describe("but when it is any other value", function() { + it("should make the field mutable", function() { + each( + [ { get field() {}, set field() {} } + , { set field() {} } + , { field: 1 } + , { field: 'wibble' } + , { field: function() {} } + ] + , function(field) { + const target = {} + defprop(target, field, { field: { writable: true } }) + + const prop = desc(target, 'field') + + if (prop.value) { + expect(prop).to.have.property('writable', true) + } else { + expect(prop.set).to.be.ok + } + } + ) + }) + }) + }) + + describe("when a field is set to be enumerable", function() { + it("should make the field enumerable", function() { + each( + [ { get field() {}, set field() {} } + , { set field() {} } + , { field: 1 } + , { field: 'wibble' } + , { field: function() {} } + ] + , function(field) { + const target = {} + defprop(target, field, { field: { enumerable: true } }) + expect(target).to.have.key('field') + } + ) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/describe.js b/test/describe.js new file mode 100644 index 0000000..8b3015a --- /dev/null +++ b/test/describe.js @@ -0,0 +1,92 @@ +const expect = require("chai").expect + , desc = require("../lib/describe") + , each = require("../lib/each") + , $ = require("../lib/partial") + +describe("`describe`", function() { + describe("when given a non-object", function() { + it("should throw a TypeError", function() { + each( + [ -1, 0, 3.14 + , null, undefined + , true, false + , "", "wibble" + ] + , function(x) { + expect($(desc, x)).to.throw("TypeError") + } + ) + }) + }) + + describe("when given an array", function() { + it("should return a description of all of its properties", function() { + const arrdesc = desc([0, 1, 2]) + + expect(arrdesc).to.eql( + [ [ "0", { value: 0, writable: true, enumerable: true, configurable: true } ] + , [ "1", { value: 1, writable: true, enumerable: true, configurable: true } ] + , [ "2", { value: 2, writable: true, enumerable: true, configurable: true } ] + , [ "length", { value: 3, writable: true, enumerable: false, configurable: false } ] + ] + ) + }) + + describe("and when also given a property name", function() { + it("should return a description of just that property", function() { + const arrdesc = desc([0, 1, 2], 1) + + expect(arrdesc).to.eql( + [ [ 1, { value: 1, writable: true, enumerable: true, configurable: true } ] ] + ) + }) + }) + + describe("and when given multiple property names", function() { + it("should return a description of each given name", function() { + const arrdesc = desc([0, 1, 2], 0, 2) + + expect(arrdesc).to.eql( + [ [ 0, { value: 0, writable: true, enumerable: true, configurable: true } ] + , [ 2, { value: 2, writable: true, enumerable: true, configurable: true } ] + ] + ) + }) + }) + }) + + describe("when given a map", function() { + it("should return a description of all of its properties", function() { + const mapdesc = desc({ foo: "bar", wibble: "wobble" }) + + expect(mapdesc).to.eql( + [ [ "foo", { value: "bar", writable: true, enumerable: true, configurable: true } ] + , [ "wibble", { value: "wobble", writable: true, enumerable: true, configurable: true } ] + ] + ) + }) + + describe("and when also given a property name", function() { + it("should return a description of just that property", function() { + const mapdesc = desc({ foo: "bar", wibble: "wobble" }, "foo") + + expect(mapdesc).to.eql( + [ [ "foo", { value: "bar", writable: true, enumerable: true, configurable: true } ] ] + ) + }) + }) + + describe("and when given multiple property names", function() { + it("should return a description of each given name", function() { + const arrdesc = desc({ foo: "bar", baz: "nope", wibble: "wobble" }, "foo", "wibble") + + + expect(arrdesc).to.eql( + [ [ "foo", { value: "bar", writable: true, enumerable: true, configurable: true } ] + , [ "wibble", { value: "wobble", writable: true, enumerable: true, configurable: true } ] + ] + ) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/extend.js b/test/extend.js new file mode 100644 index 0000000..34a42b7 --- /dev/null +++ b/test/extend.js @@ -0,0 +1,63 @@ +const expect = require("chai").expect + , extend = require("../lib/extend") + , each = require("../lib/each") + , $ = require("../lib/partial") + +describe("`extend`", function() { + describe("given a base object", function() { + describe("when it is not extendable", function() { + it("should throw a TypeError", function() { + each( + [ -1, 0, 1, 3.14 + , true, false + , null, undefined + ] + , function(base) { + expect($(extend, base, {})).to.throw("TypeError") + } + ) + }) + }) + + describe("when it is extendable", function() { + describe("and when given an extension object", function() { + it("should return a new object derived from the base and with the properties of the extension", function() { + const A = { foo: 1 } + , B = { foo: 2, bar: "wibble" } + , C = extend(A, B) + + expect(A.isPrototypeOf(C)).to.be.true + expect(C).to.have.property("foo", 2) + expect(C).to.have.property("bar", "wibble") + }) + }) + + describe("and when given more than one extension object", function() { + it("should return a new object derived from the base and with the merged properties of all extensions", function() { + const A = { foo: 1 } + , B = { foo: 2, bar: "wibble" } + , C = { baz: "wobble" } + , D = extend(A, B, C) + + expect(A.isPrototypeOf(D)).to.be.true + expect(D).to.have.property("foo", 2) + expect(D).to.have.property("bar", "wibble") + expect(D).to.have.property("baz", "wobble") + }) + + describe("and when the extension objects have identical property names", function() { + it("should choose the rightmost property for the merged extension", function() { + const A = { foo: 1 } + , B = { foo: 2, bar: "wibble" } + , C = { foo: "moo", bar: "wobble" } + , D = extend(A, B, C) + + expect(A.isPrototypeOf(D)).to.be.true + expect(D).to.have.property("foo", "moo") + expect(D).to.have.property("bar", "wobble") + }) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/get.js b/test/get.js new file mode 100644 index 0000000..ca9774e --- /dev/null +++ b/test/get.js @@ -0,0 +1,74 @@ +const partial = require('../lib/partial') + , expect = require('chai').expect + , each = require('../lib/each') + , get = require('../lib/get') + +describe('`get`', function() { + describe('when given two parameters: `map` and `key`;', function() { + describe('and when `map` has `key`', function() { + it('should return its value', function () { + const map = { foo : 'wibble' } + expect(get(map, 'foo')).to.equal('wibble') + }) + }) + + describe('and when the value of `key` is logically false', function() { + it('should still return the value of `key`', function() { + each([null, undefined, false], function(x) { + const map = { foo: x } + expect(get(map, 'foo')).to.equal(x) + }) + }) + }) + + describe('but when `map` does not have `key`', function() { + it('should return `undefined`', function () { + const map = { foo : 'wibble' } + expect(get(map, 'bar')).to.equal(undefined) + }) + }) + + describe('or when `map` is logically false', function() { + it('should return `undefined`', function() { + each([null, false, undefined], function(nil) { + expect(get(nil, 'whatever')).to.equal(undefined) + }) + }) + }) + }) + + describe('when given three parameters: `map`, `key`, and `notFound`;', function() { + const nope = '¯\(º_o)/¯' + + describe('and when `map` has `key`', function() { + it('should return its value', function () { + const map = { foo : 'wibble' } + expect(get(map, 'foo', nope)).to.equal('wibble') + }) + }) + + describe('and when the value of `key` is logically false', function() { + it('should still return the value of `key`', function() { + each([null, undefined, false], function(x) { + const map = { foo: x } + expect(get(map, 'foo', nope)).to.equal(x) + }) + }) + }) + + describe('but when `map` does not have `key`', function() { + it('should return `notFound`', function () { + const map = { foo : 'wibble' } + expect(get(map, 'bar', nope)).to.equal(nope) + }) + }) + + describe('or when `map` is logically false', function() { + it('should return `notFound`', function() { + each([null, false, undefined], function(nil) { + expect(get(nil, 'whatever', nope)).to.equal(nope) + }) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/is.js b/test/is.js index 72a7d7e..36f60ce 100644 --- a/test/is.js +++ b/test/is.js @@ -29,6 +29,56 @@ describe('is', function() { }) }) + describe('when given two arguments that are identical', function() { + it('should return `true`', function() { + each( + [ true, false, + , -1, 0, 1, 3.14 + , '', 'wibble' + , [], [1, 2, 3] + , {}, { foo: 1 } + , Function + , null, undefined + ] + , function(x) { + expect(is(x, x)).to.be.true + } + ) + }) + }) + + describe('when testing for booleans', function() { + each([ true, false ], function(x) { + describe('and when `x = '+x+'`', function() { + it('should return true', function() { + expect(is(Boolean, x)).to.be.true + expect(is('boolean', x)).to.be.true + }) + }) + } + ) + + each( + [ NaN + , '', 'wobble', 'true', 'false' + , {} + , [] + , -1, 0, 1, 3.14 + , function() {} + , undefined, null + ] + , + function(x) { + describe('and when `x = '+src(x)+'`', function() { + it('should return false', function() { + expect(is(Boolean, x)).to.be.false + expect(is('boolean', x)).to.be.false + }) + }) + } + ) + }) + describe('when testing for numbers', function() { each( [ 0 diff --git a/test/partial.js b/test/partial.js index dad03c6..5fbf961 100644 --- a/test/partial.js +++ b/test/partial.js @@ -1,27 +1,71 @@ -const partial = require('../lib/partial') - , expect = require('chai').expect +const expect = require("chai").expect + , $ = require("../lib/partial") -describe('partial', function() { - describe('when given a function and no other arguments', function() { - it('should just return the same function', function() { - const fun = partial(test) - - expect(fun).to.equal(test) - - function test(a, b) {} +describe("`partial`", function() { + describe("given a function", function() { + describe("and no other parameters", function() { + it("should just return the given function", function() { + const fun = $(test) + expect(fun).to.equal(test) + function test(a, b) {} + }) }) }) - describe('when given a function and some parameters', function() { - it('should return a partially applied function', function() { - expect(test(2, 2)).to.equal(4) + describe("and some parameters", function() { + it("should return a left-to-right partially applied function", function() { + expect(test(2, 5)).to.equal(7) - const fun = partial(test, 2) + const fun = $(test, 2) + + expect(fun(5)).to.equal(7) + + function test(a, b) { + expect(a).to.equal(2) + expect(b).to.equal(5) + return a + b + } + }) + + describe("when a parameter is identical to the `partial` function", function() { + describe("and when the partially applied function is called", function() { + it("should replace the `partial` parameter with the first argument passed", function() { + const π = Math.PI + + expect(test(1, "wibble", 3)).to.equal(π) + + const fun = $(test, 1, $, 3) + + expect(fun("wibble")).to.equal(π) + + function test(a, b, c) { + expect(a).to.equal(1) + expect(b).to.equal("wibble") + expect(c).to.equal(3) + return π + } + }) + }) + }) + + describe("when there are more than one `partial` parameter", function() { + describe("and when the partially applied function is called", function() { + it("should replace the `partial` parameters in a left-to-right fashion", function() { + expect(test(9, "7", false, null)).to.equal("wibble") + + const fun = $(test, $, "7", $, null) - expect(fun.name).to.equal('partial') - expect(fun(2)).to.equal(4) + expect(fun(9, false)).to.equal("wibble") - function test(a, b) { return a + b } + function test(a, b, c, d) { + expect(a).to.equal(9) + expect(b).to.equal("7") + expect(c).to.equal(false) + expect(d).to.equal(null) + return "wibble" + } + }) + }) }) }) }) \ No newline at end of file diff --git a/test/thunk.js b/test/thunk.js index 0d0e47f..7ae9c3f 100644 --- a/test/thunk.js +++ b/test/thunk.js @@ -214,35 +214,18 @@ describe('thunk', function() { }) describe('when given a non-function `val` as the first parameter', function() { - it('should return a zero arity thunk', function() { - const th = thunk(function() {}) - - expect(th).to.be.a('function') - expect(th.length).to.equal(0) - expect(th.name).to.equal('thunk') - }) - - describe('and when called', function() { - it('should return `val` and nothing else', function() { - each( - [ 0 - , 1 - , true - , false - , '' - , 'hello' - , [] - , {} - , null - , undefined - ] - , - function(val) { - const th = thunk(val) - expect(th()).to.equal(val) - } - ) - }) + it('should throw a TypeError', function() { + each( + [ 0, 1 + , true, false + , '', 'hello' + , [], {} + , null, undefined + ] + , function(val) { + expect(thunk(thunk, val)).to.throw(TypeError) + } + ) }) }) }) \ No newline at end of file diff --git a/test/val.js b/test/val.js new file mode 100644 index 0000000..0e1e129 --- /dev/null +++ b/test/val.js @@ -0,0 +1,26 @@ +const constantly = require('../lib/constantly') + , expect = require('chai').expect + , type = require('../lib/type') + , val = require('../lib/val') + +describe('`val`', function() { + describe('given a value `x`', function() { + describe('when `x` is a function', function() { + it('should return the value of calling `x`', function() { + const three = constantly(3) + + expect(type(three)).to.equal('function') + expect(val(three)).to.equal(3) + }) + }) + + describe('when `x` is not a function', function() { + it('should return `x`', function() { + const x = {} + + expect(type(x)).to.not.equal('function') + expect(val(x)).to.equal(x) + }) + }) + }) +}) \ No newline at end of file diff --git a/test/variadic.js b/test/variadic.js index 5fe7def..9db8d6f 100644 --- a/test/variadic.js +++ b/test/variadic.js @@ -1,60 +1,96 @@ const variadic = require('../lib/variadic') , expect = require('chai').expect + , thunk = require('../lib/thunk') + , range = require('../lib/range') , slice = require('../lib/slice') , each = require('../lib/each') , src = require('../lib/src') -describe('variadic', function() { - describe('when called with a non-function parameter', function() { - it('should throw a TypeError', function() { - each( - [ 1, 0 - , '', 'hello' - , true, false - , null, undefined - ] - , function(test) { - expect(function() { variadic(test) }).to.throw(TypeError) - } - ) +describe('`variadic`', function() { + describe('given an argument `fn`', function() { + describe('when it is not a function', function() { + it('should throw a `TypeError`', function() { + each( + [ 1, 0 + , '', 'hello' + , true, false + , null, undefined + ] + , function(fn) { + + expect(function() { variadic(fn) }).to.throw(TypeError) + } + ) + }) }) - }) - var verify + describe('when it is a function', function() { + describe('of arity 0', function() { + it('should just return `fn`', function() { + const fn = function() {} + expect(variadic(fn)).to.equal(fn) + }) + }) - each( - [ [function(rest) { verify(arguments) }, [1, 2, 3], ['foo', false], [function() {}], []] - , [function foo(a, b, rest) { verify(arguments) }, [1, 2, 'hello', 'world!'], ['foo', 'bar', 'baz'], [1, 2]] - ] - , - function(test) { - const fn = test[0] + each(range(1, 26), function(n) { + describe('of arity '+n, function() { + const sig = slice('abcdefghijklmnopqrstuvwxyz', 0, n - 1).concat('rest') - describe('when given `' + src(fn) + '`', function() { - const varfn = variadic(fn) + it('should return a '+n+' arity function', function() { + const fn = Function.apply(Function, sig.concat('return')) - each(test.slice(1), function(args) { - const argsrc = args.length? '`' + src(args).slice(1, -1) + '`' : 'nothing' + expect(fn).to.have.length(n) + expect(variadic(fn)).to.have.length(n) + }) - describe('and when called with ' + argsrc, function() { - const expHead = slice(args, 0, fn.length - 1) - , expRest = slice(args, fn.length - 1) + describe('that when called with fewer than '+n+' arguments', function() { + it('should leave `rest` undefined', function() { + var args = slice(sig, 0, -1) - it('should set rest to `' + src(expRest) + '`', function(done) { - verify = function(actual) { - const head = slice(actual, 0, -1) - , rest = actual[actual.length - 1] + while (args.length) { + var fn = Function.apply(Function, sig.concat( + [ 'this.expect(arguments).to.have.length('+args.length+')' + , 'this.expect(rest).to.be.undefined' + ].join('\n') + )) - expect(head).to.eql(expHead) - expect(rest).to.eql(expRest) - done() + variadic(fn).apply({ expect: expect }, args) + args = slice(args, 0, -1) } + }) + }) + + describe('but when called with '+n+' arguments', function() { + it('should put the last argument in `rest`', function() { + const args = slice(sig, 0, -1).concat('wibble') - varfn.apply(null, args) + const fn = Function.apply(Function, sig.concat( + [ 'this.expect(arguments).to.have.length('+n+')' + , 'this.expect(rest).to.have.length(1)' + , 'this.expect(rest).to.contain("wibble")' + ].join('\n') + )) + + variadic(fn).apply({ expect: expect }, args) + }) + }) + + describe('and when called with more than '+n+' arguments', function() { + it('should put all variadic arguments in `rest`', function() { + const args = slice(sig, 0, -1).concat('wibble', 'wobble', 'bob') + + const fn = Function.apply(Function, sig.concat( + [ 'this.expect(arguments).to.have.length('+n+')' + , 'this.expect(rest).to.have.length(3)' + , 'this.expect(rest).to.eql(["wibble", "wobble", "bob"])' + ].join('\n') + )) + + variadic(fn).apply({ expect: expect }, args) }) }) }) }) - } - ) + }) + }) }) \ No newline at end of file diff --git a/test/when.js b/test/when.js new file mode 100644 index 0000000..64f9817 --- /dev/null +++ b/test/when.js @@ -0,0 +1,51 @@ +const constantly = require('../lib/constantly') + , expect = require('chai').expect + , when = require('../lib/when') + +describe('`when`', function() { + describe('given a `test` and a `body`', function() { + describe('when `test` is a function', function() { + describe('and when calling `test` returns logically true', function () { + describe('when `body` is a function', function() { + it('then return the value of calling `body`', function() { + const tru = constantly(true) + , wbl = constantly('wibble') + + expect(when(tru, wbl)).to.equal('wibble') + }) + }) + + describe('when `body` is not a function', function() { + it('then return `body`', function() { + const tru = constantly(true) + expect(when(tru, 'wibble')).to.equal('wibble') + }) + }) + }) + + describe('but when calling `test` returns logically false', function() { + it('then return `undefined`', function() { + const no = constantly(null) + expect(when(no, 'wibble')).to.equal.undefined + }) + }) + }) + + describe('when `test` is not a function', function() { + describe('but when the value is logically true', function() { + describe('when `body` is a function', function() { + it('then return the value of calling `body`', function() { + const wbl = constantly('wibble') + expect(when(0, wbl)).to.equal('wibble') + }) + }) + + describe('when `body` is not a function', function() { + it('then return `body`', function() { + expect(when(1, 'wibble')).to.equal('wibble') + }) + }) + }) + }) + }) +}) \ No newline at end of file