diff --git a/proxy.js b/proxy.js new file mode 100644 index 0000000..4a5fee7 --- /dev/null +++ b/proxy.js @@ -0,0 +1,65 @@ +'use strict'; + +const processFn = (fn, opts) => function (...args) { + const P = opts.promiseModule; + + return new P((resolve, reject) => { + args.push((err, result, ...results) => { + if (err) { + reject(err); + } else if (opts.multiArgs) { + resolve([result, ...results]); + } else { + resolve(result); + } + }); + + fn.apply(this, args); + }); +}; + +module.exports = (obj, opts) => { + opts = Object.assign({ + exclude: [/.+Sync$/], + promiseModule: Promise + }, opts); + + const filter = key => { + const match = pattern => typeof pattern === 'string' ? key === pattern : pattern.test(key); + return opts.include ? opts.include.some(match) : !opts.exclude.some(match); + }; + + const cache = new Map(); + + const handler = { + get: (target, key) => { + let cached = cache.get(key); + + if (!cached) { + const x = target[key]; + + cached = typeof x === 'function' && filter(key) ? processFn(x, opts) : x; + cache.set(key, cached); + } + + return cached; + } + }; + + if (!opts.excludeMain) { + const main = Symbol('main'); + + handler.apply = (target, thisArg, argumentsList) => { + let cached = cache.get(main); + + if (!cached) { + cached = processFn(target, opts); + cache.set(main, cached); + } + + return cached.apply(thisArg, argumentsList); + }; + } + + return new Proxy(obj, handler); +}; diff --git a/test-proxy.js b/test-proxy.js new file mode 100644 index 0000000..4b79181 --- /dev/null +++ b/test-proxy.js @@ -0,0 +1,111 @@ +import fs from 'fs'; +import test from 'ava'; +import pinkiePromise from 'pinkie-promise'; +import fn from './proxy'; + +const fixture = cb => setImmediate(() => cb(null, 'unicorn')); +const fixture2 = (x, cb) => setImmediate(() => cb(null, x)); +const fixture3 = cb => setImmediate(() => cb(null, 'unicorn', 'rainbow')); +const fixture4 = cb => setImmediate(() => { + cb(null, 'unicorn'); + return 'rainbow'; +}); + +fixture4.meow = cb => { + setImmediate(() => { + cb(null, 'unicorn'); + }); +}; + +const fixture5 = () => 'rainbow'; + +const fixtureModule = { + method1: fixture, + method2: fixture, + method3: fixture5 +}; + +test('main', async t => { + t.is(typeof fn(fixture)().then, 'function'); + t.is(await fn(fixture)(), 'unicorn'); +}); + +test('pass argument', async t => { + t.is(await fn(fixture2)('rainbow'), 'rainbow'); +}); + +test('custom Promise module', async t => { + t.is(await fn(fixture, {promiseModule: pinkiePromise})(), 'unicorn'); +}); + +test('multiArgs option', async t => { + t.deepEqual(await fn(fixture3, {multiArgs: true})(), ['unicorn', 'rainbow']); +}); + +test('wrap core method', async t => { + t.is(JSON.parse(await fn(fs.readFile)('package.json')).name, 'pify'); +}); + +test('module support', async t => { + t.is(JSON.parse(await fn(fs).readFile('package.json')).name, 'pify'); +}); + +test('module support - doesn\'t transform *Sync methods by default', t => { + t.is(JSON.parse(fn(fs).readFileSync('package.json')).name, 'pify'); +}); + +test('module support - preserves non-function members', t => { + const module = { + method: () => {}, + nonMethod: 3 + }; + + t.deepEqual(Object.keys(module), Object.keys(fn(module))); +}); + +test('module support - transforms only members in options.include', t => { + const pModule = fn(fixtureModule, { + include: ['method1', 'method2'] + }); + + t.is(typeof pModule.method1().then, 'function'); + t.is(typeof pModule.method2().then, 'function'); + t.not(typeof pModule.method3().then, 'function'); +}); + +test('module support - doesn\'t transform members in options.exclude', t => { + const pModule = fn(fixtureModule, { + exclude: ['method3'] + }); + + t.is(typeof pModule.method1().then, 'function'); + t.is(typeof pModule.method2().then, 'function'); + t.not(typeof pModule.method3().then, 'function'); +}); + +test('module support - options.include over options.exclude', t => { + const pModule = fn(fixtureModule, { + include: ['method1', 'method2'], + exclude: ['method2', 'method3'] + }); + + t.is(typeof pModule.method1().then, 'function'); + t.is(typeof pModule.method2().then, 'function'); + t.not(typeof pModule.method3().then, 'function'); +}); + +test('module support — function modules', t => { + const pModule = fn(fixture4); + + t.is(typeof pModule().then, 'function'); + t.is(typeof pModule.meow().then, 'function'); +}); + +test('module support — function modules exclusion', t => { + const pModule = fn(fixture4, { + excludeMain: true + }); + + t.is(typeof pModule.meow().then, 'function'); + t.not(typeof pModule(() => {}).then, 'function'); +});