From a813d18cdaf66070ca6132b789fbeaf90fd030b1 Mon Sep 17 00:00:00 2001 From: Martin Cooper Date: Mon, 23 Jan 2012 22:23:22 -0800 Subject: [PATCH] Initial commit. --- .gitignore | 1 + LICENSE | 24 ++++++++ README.md | 86 ++++++++++++++++++++++++++ lib/sidedoor.js | 60 ++++++++++++++++++ package.json | 30 +++++++++ test/fixtures/exposeDefault.js | 9 +++ test/fixtures/exposeMultiple.js | 17 +++++ test/fixtures/exposeNamed.js | 9 +++ test/fixtures/exposeNone.js | 17 +++++ test/run.js | 3 + test/test-sidedoor.js | 106 ++++++++++++++++++++++++++++++++ 11 files changed, 362 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/sidedoor.js create mode 100644 package.json create mode 100644 test/fixtures/exposeDefault.js create mode 100644 test/fixtures/exposeMultiple.js create mode 100644 test/fixtures/exposeNamed.js create mode 100644 test/fixtures/exposeNone.js create mode 100755 test/run.js create mode 100644 test/test-sidedoor.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..26e4435 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ + Copyrights for code authored by Yahoo! Inc. is licensed under the following + terms: + + MIT License + + Copyright (c) 2011 Yahoo! Inc. All Rights Reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to + deal in the Software without restriction, including without limitation the + rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + sell copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9233098 --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# Sidedoor - Exposing a secondary API for your Node.js modules + +As you've written unit tests for your Node.js modules, you've almost certainly +been forced to think about exposing some internal methods for the sole purpose +of enabling more comprehensive tests or verifying such tests. You've wrestled +with polluting the public API of your module with functions you really don't +want people to see, let alone depend on. + +What if, instead, you could expose a secondary API for your module, such that +the additional methods are available to your tests, but without polluting the +public API? + +This is just what Sidedoor enables. In your module, a simple call to Sidedoor +exposes whatever secondary API you need, while a similarly simple call from a +client module provides access to that secondary API. + +## Installation + +Just use npm: + + npm install sidedoor + +## Exposing an API + +You expose your secondary API by telling Sidedoor which functions to expose, +and, optionally, the name under which you want to expose them. For example: + + function doSomethingImportant() { + ... + } + + function doSomethingTestRelated() { + ... + } + + // Expose the primary API + module.exports = { + doSomething: doSomethingImportant + }; + + // Expose the secondary API + sidedoor.expose(module, 'test', { + testHelper: doSomethingTestRelated + }); + +Here, in addition to our module's primary API, we've exposed a secondary API +named 'test' that contains the single function `doSomethingTestRelated` exposed +under the name `testHelper`. + +The arguments to `expose` are as follows: + +* _module_, the module from which you are exposing the secondary API. This will +almost always be just 'module'. +* _name_, the name of the secondary API you are exposing. If omitted, a default +unnamed API is exposed. +* _exports_, the equivalent of the `exports` object, being the API that is to +be exposed. + +Note that, because a secondary API may be named, it is possible to expose any +number of such APIs from any module. + +## Accessing an API + +You access a secondary API by asking Sidedoor to obtain it for you, optionally +providing the name of the API you want, and then simply calling it as you would +a regularly exported API. For example, : + + var public_api = require('my_module'), + test_api = sidedoor.get('my_module', 'test'); + + // Call the public API as normal + public_api.doSomething(); + + // Call the secondary API obtained via Sidedoor + test_api.testHelper(); + +The arguments to `get` are as follows: + +* _modpath_, the name or path of the module for which you want to access the API. +This is the same as what you would pass to `require` to obtain the primary API. +* _name_, the name of the secondary API you are accessing. If omitted, the default +unnamed API is obtained. + +## License + +Sidedoor is licensed under the [MIT License](http://github.com/mfncooper/sidedoor/raw/master/LICENSE). diff --git a/lib/sidedoor.js b/lib/sidedoor.js new file mode 100644 index 0000000..16793fa --- /dev/null +++ b/lib/sidedoor.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2011 Yahoo! Inc. All rights reserved. + */ + +/* + * A library that allows the controlled exposure of functions from a module + * without polluting the primary exports mechanism. This is useful mainly + * when exposing test-only functions, since those functions are made available + * to tests while being withheld from the public module API. (The functions are + * still available to any client, but an explicit effort must be made to obtain + * them.) + */ + +var SIDEDOOR_KEY = "sidedoor", + DEFAULT_GROUP = "default"; + +/* + * Expose a set of functions from a module as a group. The group name may be + * elided, in which case the functions are considered to be within a default + * group. + */ +function expose(mod, group, fns) { + // Check for a defaulted group + if (typeof group !== "string") { + fns = group; + group = DEFAULT_GROUP; + } + + if (!mod.hasOwnProperty(SIDEDOOR_KEY)) { + mod[SIDEDOOR_KEY] = {}; + } + mod[SIDEDOOR_KEY][group] = fns; +} + +/* + * Retrieve a set of functions from a module as a group. If the group name is + * not specified, the default group is retrieved. If the group of functions + * does not exist, undefined is returned. + */ +function get(modname, group) { + var mod = require.cache[require.resolve(modname)], + fns = null; + + if (!mod) { + require(modname); + mod = require.cache[require.resolve(modname)]; + } + if (!mod) { + throw new Error("bad stuff"); + } + + if (mod.hasOwnProperty(SIDEDOOR_KEY)) { + fns = mod[SIDEDOOR_KEY][group || DEFAULT_GROUP]; + } + + return fns; +} + +exports.expose = expose; +exports.get = get; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3dfa7d4 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "author": "Martin Cooper ", + "name": "sidedoor", + "description": "Exposing a secondary API for your Node.js modules", + "version": "0.1.0", + "repository": { + "type": "git", + "url": "git://github.com/mfncooper/sidedoor.git" + }, + "bugs": { + "url" : "http://github.com/mfncooper/sidedoor/issues" + }, + "main": "lib/sidedoor.js", + "engines": { + "node": ">=0.4.5" + }, + "dependencies": {}, + "devDependencies": { + "nodeunit" : "0.6.x" + }, + "scripts": { + "test": "test/run.js" + }, + "licenses" : [ + { + "type" : "MIT", + "url" : "http://github.com/mfncooper/mockery/raw/master/LICENSE" + } + ] +} diff --git a/test/fixtures/exposeDefault.js b/test/fixtures/exposeDefault.js new file mode 100644 index 0000000..7847240 --- /dev/null +++ b/test/fixtures/exposeDefault.js @@ -0,0 +1,9 @@ +var sidedoor = require("../../lib/sidedoor.js"); + +function exposee() { + return "-exposed-"; +} + +sidedoor.expose(module, { + exposee: exposee +}); diff --git a/test/fixtures/exposeMultiple.js b/test/fixtures/exposeMultiple.js new file mode 100644 index 0000000..28afa79 --- /dev/null +++ b/test/fixtures/exposeMultiple.js @@ -0,0 +1,17 @@ +var sidedoor = require("../../lib/sidedoor.js"); + +function group1fn() { + return "-group1fn-exposed-"; +} + +function group2fn() { + return "-group2fn-exposed-"; +} + +sidedoor.expose(module, "mygroup1", { + group1fn: group1fn +}); + +sidedoor.expose(module, "mygroup2", { + group2fn: group2fn +}); diff --git a/test/fixtures/exposeNamed.js b/test/fixtures/exposeNamed.js new file mode 100644 index 0000000..9614cd7 --- /dev/null +++ b/test/fixtures/exposeNamed.js @@ -0,0 +1,9 @@ +var sidedoor = require("../../lib/sidedoor.js"); + +function exposee() { + return "-exposed-"; +} + +sidedoor.expose(module, "mygroup", { + exposee: exposee +}); diff --git a/test/fixtures/exposeNone.js b/test/fixtures/exposeNone.js new file mode 100644 index 0000000..28afa79 --- /dev/null +++ b/test/fixtures/exposeNone.js @@ -0,0 +1,17 @@ +var sidedoor = require("../../lib/sidedoor.js"); + +function group1fn() { + return "-group1fn-exposed-"; +} + +function group2fn() { + return "-group2fn-exposed-"; +} + +sidedoor.expose(module, "mygroup1", { + group1fn: group1fn +}); + +sidedoor.expose(module, "mygroup2", { + group2fn: group2fn +}); diff --git a/test/run.js b/test/run.js new file mode 100755 index 0000000..956e4de --- /dev/null +++ b/test/run.js @@ -0,0 +1,3 @@ +#!/usr/bin/env node +var test_reporter = require('nodeunit').reporters.nested; +test_reporter.run(['./test/test-sidedoor.js']); diff --git a/test/test-sidedoor.js b/test/test-sidedoor.js new file mode 100644 index 0000000..73ef1d5 --- /dev/null +++ b/test/test-sidedoor.js @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2011 Yahoo! Inc. All rights reserved. + */ + +var testCase = require('nodeunit').testCase, + sidedoor = require('../lib/sidedoor'); + +function fixtureMod(name) { + return "../test/fixtures/" + name; +} + +module.exports = testCase({ + + "when nothing is exposed": testCase({ + "getting the default group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeNone")); + test.strictEqual(sd, undefined); + test.done(); + }, + "getting a named group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeNone"), "mygroup"); + test.strictEqual(sd, undefined); + test.done(); + } + }), + + "when the default group is exposed": testCase({ + "getting the default group returns the functions": function (test) { + var sd = sidedoor.get(fixtureMod("exposeDefault")); + test.notEqual(sd, undefined); + test.equal(typeof sd, "object"); + test.equal(typeof sd.exposee, "function"); + test.equal(sd.exposee(), "-exposed-"); + test.done(); + }, + "getting a named group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeDefault"), "mygroup"); + test.strictEqual(sd, undefined); + test.done(); + } + }), + + "when a named group is exposed": testCase({ + "getting the default group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeNamed")); + test.strictEqual(sd, undefined); + test.done(); + }, + "getting the exposed group returns the functions": function (test) { + var sd = sidedoor.get(fixtureMod("exposeNamed"), "mygroup"); + test.notEqual(sd, null); + test.equal(typeof sd, "object"); + test.equal(typeof sd.exposee, "function"); + test.equal(sd.exposee(), "-exposed-"); + test.done(); + }, + "getting another named group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeNamed"), "another"); + test.strictEqual(sd, undefined); + test.done(); + } + }), + + "when multiple groups are exposed": testCase({ + "getting the default group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeMultiple")); + test.strictEqual(sd, undefined); + test.done(); + }, + "getting an exposed group returns the functions": function (test) { + var sd = sidedoor.get(fixtureMod("exposeMultiple"), "mygroup1"); + test.notEqual(sd, null); + test.equal(typeof sd, "object"); + test.equal(typeof sd.group1fn, "function"); + test.equal(sd.group1fn(), "-group1fn-exposed-"); + test.done(); + }, + "getting another exposed group returns the functions": function (test) { + var sd = sidedoor.get(fixtureMod("exposeMultiple"), "mygroup2"); + test.notEqual(sd, null); + test.equal(typeof sd, "object"); + test.equal(typeof sd.group2fn, "function"); + test.equal(sd.group2fn(), "-group2fn-exposed-"); + test.done(); + }, + "getting another named group returns undefined": function (test) { + var sd = sidedoor.get(fixtureMod("exposeMultiple"), "another"); + test.strictEqual(sd, undefined); + test.done(); + }, + "functions are exposed only in their assigned groups": function (test) { + var sd1 = sidedoor.get(fixtureMod("exposeMultiple"), "mygroup1"), + sd2 = sidedoor.get(fixtureMod("exposeMultiple"), "mygroup2"); + test.notEqual(sd1, null); + test.notEqual(sd2, null); + test.equal(typeof sd1, "object"); + test.equal(typeof sd2, "object"); + test.equal(typeof sd1.group1fn, "function"); + test.equal(typeof sd2.group2fn, "function"); + test.equal(typeof sd1.group2fn, "undefined"); + test.equal(typeof sd2.group1fn, "undefined"); + test.done(); + } + }) + +});