diff --git a/README.md b/README.md index a32256e..860f271 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,9 @@ Each `*/ensure` utility, accepts following options (eventually passed with secon - **Iterable** - [`iterable/is`](docs/iterable.md#iterableis) - [`iterable/ensure`](docs/iterable.md#iterableensure) +- **Set** + - [`set/is`](docs/set.md#setis) + - [`set/ensure`](docs/set.md#setensure) - **Map** - [`map/is`](docs/map.md#mapis) - [`map/ensure`](docs/map.md#mapensure) diff --git a/docs/set.md b/docs/set.md new file mode 100644 index 0000000..c5fa875 --- /dev/null +++ b/docs/set.md @@ -0,0 +1,27 @@ +# Set + +_Set_ instance + +## `set/is` + +Confirms if given object is a native set\_ + +```javascript +const isSet = require("type/set/is"); + +isSet(new Set()); // true +isSet(new Set()); // false +isSet({}); // false +``` + +## `Set/ensure` + +If given argument is a _set_, it is returned back. Otherwise `TypeError` is thrown. + +```javascript +const ensureSet = require("type/set/ensure"); + +const set = new Set(); +ensureSet(set); // set +eensureSet({}); // Thrown TypeError: [object Object] is not a set +``` diff --git a/set/ensure.js b/set/ensure.js new file mode 100644 index 0000000..c5b6a41 --- /dev/null +++ b/set/ensure.js @@ -0,0 +1,12 @@ +"use strict"; + +var resolveException = require("../lib/resolve-exception") + , is = require("./is"); + +module.exports = function (value /*, options*/) { + if (is(value)) return value; + var options = arguments[1]; + var errorMessage = + options && options.name ? "Expected a set for %n, received %v" : "%v is not a set"; + return resolveException(value, errorMessage, options); +}; diff --git a/set/is.js b/set/is.js new file mode 100644 index 0000000..7491e54 --- /dev/null +++ b/set/is.js @@ -0,0 +1,27 @@ +"use strict"; + +var isPrototype = require("../prototype/is"); + +// In theory we could rely on Symbol.toStringTag directly, +// still early native implementation (e.g. in FF) predated symbols +var objectToString = Object.prototype.toString, objectTaggedString = objectToString.call(new Set()); + +module.exports = function (value) { + if (!value) return false; + + // Sanity check (reject objects which do not expose common Promise interface) + try { + if (typeof value.add !== "function") return false; + if (typeof value.has !== "function") return false; + if (typeof value.clear !== "function") return false; + } catch (error) { + return false; + } + + // Ensure its native Promise object (has [[SetData]] slot) + // Note: it's not 100% precise as string tag may be overriden + // and other objects could be hacked to expose it + if (objectToString.call(value) !== objectTaggedString) return false; + + return !isPrototype(value); +}; diff --git a/test/set/ensure.js b/test/set/ensure.js new file mode 100644 index 0000000..4329c6b --- /dev/null +++ b/test/set/ensure.js @@ -0,0 +1,29 @@ +"use strict"; + +var assert = require("chai").assert + , ensureSet = require("../../set/ensure"); + +describe("set/ensure", function () { + it("Should return input value", function () { + var value = new Set(); + assert.equal(ensureSet(value), value); + }); + it("Should crash on no value", function () { + try { + ensureSet({}); + throw new Error("Unexpected"); + } catch (error) { + assert.equal(error.name, "TypeError"); + assert.equal(error.message, "[object Object] is not a set"); + } + }); + it("Should provide alternative error message when name option is passed", function () { + try { + ensureSet(null, { name: "name" }); + throw new Error("Unexpected"); + } catch (error) { + assert.equal(error.name, "TypeError"); + assert.equal(error.message, "Expected a set for name, received null"); + } + }); +}); diff --git a/test/set/is.js b/test/set/is.js new file mode 100644 index 0000000..16118af --- /dev/null +++ b/test/set/is.js @@ -0,0 +1,47 @@ +"use strict"; + +var assert = require("chai").assert + , isSet = require("../../set/is"); + +describe("set/is", function () { + if (typeof Set === "function") { + it("Should return true on set", function () { assert.equal(isSet(new Set()), true); }); + } + it("Should return false on object that mimicks map", function () { + assert.equal( + isSet({ + add: function () { /* noop */ }, + has: function () { /* noop */ }, + clear: function () { /* noop */ } + }), + false + ); + }); + + it("Should return false on plain object", function () { assert.equal(isSet({}), false); }); + it("Should return false on function", function () { + assert.equal(isSet(function () { return true; }), false); + }); + it("Should return false on array", function () { assert.equal(isSet([]), false); }); + if (typeof Object.create === "function") { + it("Should return false on object with no prototype", function () { + assert.equal(isSet(Object.create(null)), false); + }); + } + it("Should return false on string", function () { assert.equal(isSet("foo"), false); }); + it("Should return false on empty string", function () { assert.equal(isSet(""), false); }); + it("Should return false on number", function () { assert.equal(isSet(123), false); }); + it("Should return false on NaN", function () { assert.equal(isSet(NaN), false); }); + it("Should return false on boolean", function () { assert.equal(isSet(true), false); }); + if (typeof Symbol === "function") { + it("Should return false on symbol", function () { + assert.equal(isSet(Symbol("foo")), false); + }); + } + if (typeof Map === "function") { + it("Should return false on map", function () { assert.equal(isSet(new Map()), false); }); + } + + it("Should return false on null", function () { assert.equal(isSet(null), false); }); + it("Should return false on undefined", function () { assert.equal(isSet(void 0), false); }); +});