diff --git a/lib/has.js b/lib/has.js new file mode 100644 index 0000000..89d2c38 --- /dev/null +++ b/lib/has.js @@ -0,0 +1,8 @@ +module.exports = has + +function has(x, key) { + return is(x) && hasprop.call(x, key) +} + +const hasprop = Object.prototype.hasOwnProperty + , is = require("./is") \ No newline at end of file diff --git a/lib/meta.js b/lib/meta.js new file mode 100644 index 0000000..beae0c9 --- /dev/null +++ b/lib/meta.js @@ -0,0 +1,28 @@ +module.exports = meta + +const protoOf = Object.getPrototypeOf + , mutable = Object.isExtensible + , define = Object.defineProperty + , create = Object.create + , names = Object.getOwnPropertyNames + , isnt = require("./isnt") + , has = require("./has") + +function meta(x, data) { + if (isnt(x)) return null + + if (arguments.length < 2) return (x[meta.key] || null) + + if (!mutable(x)) throw TypeError("Can't add metadata to non-extensible object.") + + const metadata = x[meta.key] || define(x, meta.key, { value: create(null) })[meta.key] + + names(data).forEach(function(name) { + if (has(metadata, name)) throw TypeError("Can't change immutable metadata: "+name) + define(metadata, name, { value: data[name], enumerable: true }) + }) + + return metadata +} + +define(meta, "key", { value: "^metadata:{}" }) \ No newline at end of file diff --git a/lib/not.js b/lib/not.js index 0f5b812..1684f24 100644 --- a/lib/not.js +++ b/lib/not.js @@ -2,6 +2,4 @@ module.exports = not function not(x) { return x === false || x == null -} - -const isnt = require('./isnt') \ No newline at end of file +} \ 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/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/test/meta.js b/test/meta.js new file mode 100644 index 0000000..71d3c56 --- /dev/null +++ b/test/meta.js @@ -0,0 +1,85 @@ +const expect = require("chai").expect + , each = require("../lib/each") + , meta = require("../lib/meta") + , $ = require("../lib/partial") + +describe("`meta`", function() { + describe("given an object `x`", function() { + describe("when `x` is logically false", function() { + it("should return `null`", function() { + each([null, false, undefined], function(x) { + expect(meta(x)).to.be.null + }) + }) + }) + + describe("when called with no other parameters", function() { + describe("and when `x` is associated with metadata", function() { + it("should return the metadata object", function() { + const m = { foo: "wibble" } + , x = {} + + meta(x, m) + expect(meta(x)).to.eql(m) + }) + }) + + describe("but when `x` is not associated with metadata", function() { + it("should return null", function() { + expect(meta({})).to.be.null + }) + }) + }) + + describe("and also given a map `data`", function() { + describe("when `x` is not extensible", function() { + it("should throw an error", function() { + each( + [ Object.preventExtensions({}) + , Object.freeze({}) + , Object.seal({}) + ] + , function(x) { + expect($(meta, x, {})).to.throw() + } + ) + }) + }) + + describe("when `x` is not associated with any metadata", function() { + it("should set all values of `data` and return the full metadata object", function() { + const m = { foo: "wibble" } + , x = {} + + expect(meta(x, m)).to.eql(m) + expect(meta(x)).to.eql(m) + }) + }) + + describe("when `x` is associated with metadata", function() { + describe("and when no properties of `data` overlap", function() { + it("should set all values of `data` and return the full metadata object", function() { + const a = { foo: "wibble" } + , b = { bob: "wobble" } + , c = { foo: "wibble", bob: "wobble" } + , x = {} + + expect(meta(x, a)).to.eql(a) + expect(meta(x, b)).to.eql(c) + }) + }) + + describe("but when any one property of `data` overlap", function() { + it("should throw an error", function() { + const a = { foo: "wibble" } + , b = { foo: "wobble" } + , x = {} + + expect(meta(x, a)).to.eql(a) + expect($(meta, x, b)).to.throw() + }) + }) + }) + }) + }) +}) \ 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/variadic.js b/test/variadic.js index 5fe7def..7d4cb69 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, 28), 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