diff --git a/lib/deprecated.js b/lib/deprecated.js index b3ae61f8..c16a3f86 100644 --- a/lib/deprecated.js +++ b/lib/deprecated.js @@ -68,6 +68,27 @@ everyauth( ) ); +var fb = module.exports = +oauthModule.submodule('facebook') + // TODO submodule should + // set fb.name = 'facebook' + .apiHost('https://graph.facebook.com') + .setters('scope') + .routeStep('authRequest') + .uri('/auth/facebook') + .step('authRequest') + .accepts('req res abc') + .returns('*') + .get('/auth/facebook') + // Should be able to access module + // properties from within a step + // definition + .step('authCallback') + .get('/auth/facebook/callback') + .step('addToSession') + .define( function (sess, auth) { + + }) /** OAuth **/ diff --git a/lib/everymodule.alt.js b/lib/everymodule.alt.js new file mode 100644 index 00000000..289982bd --- /dev/null +++ b/lib/everymodule.alt.js @@ -0,0 +1,149 @@ +var OAuth = require('oauth').OAuth2 + , url = require('url') + , MaterializedSequence = require('./sequence') + , clone = require('./utils').clone; + +function route (method) { + return function (alias) { + this._routes[method][alias]; + this.configurable(alias); + this._currentRoute = method + '.' + alias; + return this; + }; +} + +// TODO Add in check for missing step definitions +// from the set of step declarations +var everyModule = module.exports = { + definit: function (fn) { + // Remove any prior `init` that was assigned + // directly to the object via definit and not + // assigned via prototypal inheritance + if (this.hasOwnProperty('init')) + delete this.init; + + var super = this.init; + // since this.hasOwnProperty('init') is false + + this.init = function () { + this.super = super; + fn.apply(this, arguments); + delete this.super; + + // Do module compilation here, too + }; + } + , get: route('get') + , post: route('post') + , configurable: function (property) { + if (property.indexOf(' ') !== -1) { + // e.g., property === 'apiHost appId appSecret' + var self = this; + property.split(/\s+/).forEach( function (_property) { + self.configurable(_property); + }); + return this; + } + + // Else we have a single property name + if (!this[property]) + this[property] = function (setTo) { + var k = '_' + property; + if (!arguments.length) return this[k]; + this[k] = setTo; + return this; + }; + return this; + } + , step: function (name, opts) { + // TODO Use opts + var steps = this._steps; + if (!steps[name]) { + steps[name] = {}; + steps._order.push(name); + + // For configuring what the actual business + // logic is: + // fb.step('fetchOAuthUser') generates the method + // fb.fetchOAuthUser that can be used like + // fb.fetchOAuthUser( function (...) { + // // Business logic goes here + // } ); + this.configurable(name); + } + this._currentStep = steps[name]; + return this; + } + , accepts: function (input) { + var step = this._currentStep; + step.accepts = input + ? input.split(/\s+/) + : null; + return this; + } + , promises: function (output) { + var step = this._currentStep; + step.promises = output + ? output.split(/\s+/) + : null; + return this; + } + + /** + * Creates a new submodule using prototypal + * inheritance + */ + , submodule: function (name) { + var submodule = Object.create(this) + , self = this; + ['_routes', '_steps'].forEach( + function (toClone) { + submodule[toClone] = clone(self[toClone]); + } + ); + submodule.name = name; + return submodule; + } + + /** + * Decorates the app with the routes required of the + * module + */ + , routeApp: function (app) { + for (var method in this._routes) { + for (var routeAlias in this._routes[method]) { + app[method](this[routeAlias](), this.routeHandler(method, routeAlias)); + } + } + } + + // Returns the route handler + // This is also where a lot of the magic happens + , routeHandler: function (method, routeAlias) { + var stepsHash = this._steps[method + '.' + routeAlias] + // Move orderedSteps to a getter? + , orderedSteps = stepsHash._order.map( function (stepName) { + return stepsHash[stepName]; + }) + , seq = new MaterializedSequence(orderedSteps); + + return seq.routeHandler(); + // This kicks off a sequence of steps based on a + // route + // Creates a new chain of promises and exposes the leading promise + // to the incoming (req, res) pair from the route handler + } + + // _order keeps the order of the steps by name + // _steps maps method.routes to step names to step objects + // and an ordering of the step names ('_order') + // A step object is { accepts: [...], promises: [...] } + , _steps: {} + , _routes: { + get: {} + , post: {} + } +}.configurable('init'); + +everyModule + .step('findOrCreateUser'); diff --git a/lib/facebook.alt.js b/lib/facebook.alt.js index 74852fc7..bbd6e5ab 100644 --- a/lib/facebook.alt.js +++ b/lib/facebook.alt.js @@ -12,26 +12,4 @@ oauthModule.submodule('facebook') var oauthUser = JSON.parse(data); p.fulfill(oauthUser); }) - }) - -var fb = module.exports = -oauthModule.submodule('facebook') - // TODO submodule should - // set fb.name = 'facebook' - .apiHost('https://graph.facebook.com') - .setters('scope') - .routeStep('authRequest') - .uri('/auth/facebook') - .step('authRequest') - .accepts('req res abc') - .returns('*') - .get('/auth/facebook') - // Should be able to access module - // properties from within a step - // definition - .step('authCallback') - .get('/auth/facebook/callback') - .step('addToSession') - .define( function (sess, auth) { - - }) + }); diff --git a/lib/oauth.alt.js b/lib/oauth.alt.js index 2fb1d036..d8f1c6f0 100644 --- a/lib/oauth.alt.js +++ b/lib/oauth.alt.js @@ -8,7 +8,7 @@ var everyModule = require('./everymodule') var oauth = module.exports = everyModule.submodule('oauth') - .init( function () { + .definit( function () { this.oauth = new OAuth(this.appId(), this.appSecret(), this.apiHost()); }) .configurable('apiHost appId appSecret myHostname redirectPath') @@ -28,15 +28,18 @@ everyModule.submodule('oauth') .promises('accessToken refreshToken') .step('fetchOAuthUser') .accepts('accessToken refreshToken') - .promises('user') - .step('compile') - .accepts('accessToken refreshToken user') - .promises('auth') + .promises('oauthUser') .step('getSession') .accepts('req') .promises('session') + .step('findOrCreateUser') + .accepts('session accessToken oauthUser') + .promises('user') + .step('compile') + .accepts('accessToken refreshToken oauthUser user') + .promises('auth') .step('addToSession') - .accepts('session') + .accepts('session auth') .promises(null) .step('sendResponse') .accepts('res') diff --git a/lib/promise.js b/lib/promise.js new file mode 100644 index 00000000..95f1a033 --- /dev/null +++ b/lib/promise.js @@ -0,0 +1,40 @@ +var Promise = module.exports = function (values) { + this._callbacks = []; + this._errbacks = []; + if (arguments.length > 0) { + this.fulfill.apply(this, values); + } +}; + +Promise.prototype = { + callback: function (fn, scope) { + if (this.values) { + return fn.apply(scope, this.values); + } + this._callback.push([fn, scope]); + return this; + } + , errback: function (fn, scope) { + if (this.err) { + return fn.call(scope, this.err); + } + this._errbacks.push([fn, scope]); + return this; + } + , fulfill: function () { + var callbacks = this._callbacks; + this.values = arguments; + for (var i = 0, l = callbacks.length; i < l; i++) { + callbacks[i][0].apply(callbacks[i][1], arguments); + } + return this; + } + , fail: function (err) { + var errbacks = this._errbacks; + this.err = err; + for (var i = 0, l = errbacks.length; i < l; i++) { + errbacks[i][0].call(errbacks[i][1], err); + } + return this; + } +}; diff --git a/lib/sequence.js b/lib/sequence.js new file mode 100644 index 00000000..b8be01f9 --- /dev/null +++ b/lib/sequence.js @@ -0,0 +1,100 @@ +var Promise = require('./promise'); + +function MaterializedSequence (steps) { + // Our sequence state is encapsulated in seq.values + this.values = {}; + this.steps = steps; +} + +MaterializedSequence.prototype = { + routeHandler: function () { + var seq = this + , steps = this.steps + , firstStep = steps[0]; + + var promise; + for (var i = 0, l = steps.length, step; i < l; i++) { + step = steps[i]; + } + var promises = this.steps.map( function (step) { + var accepts = step.accepts + , promises = step.promises + , block = step.fn; + }); + + + // This kicks off a sequence of steps based on a + // route + // Creates a new chain of promises and exposes the leading promise + // to the incoming (req, res) pair from the route handler + return function () { + var args = Array.prototype.slice.call(arguments, 0); + firstStep.accepts.forEach( function (valName, i) { + // Map the incoming arguments to the named parameters + // that the first step is expected to accept. + seq.values[valName] = args[i]; + }); + seq.start(); + + }; + } + /** + * @param {Object} step + * @returns {Promise} + */ + , applyStep: function (step) { + var accepts = step.accepts; + , promises = step.promises; + , block = step.block; + // Unwrap values based on step.accepts + var args = accepts.reduce( function (args, accept) { + args.push(seq.values[accept]); + return args; + }, []); + // Apply the step logic + // TODO Set this.module + ret = block.apply(this.module, args); + + // Convert return value into a Promise + // if it's not yet a Promise + if (! (ret instanceof Promise)) { + if (Array.isArray(ret)) + ret = new Promise(ret); + else + ret = new Promise([ret]); + } + + var seq = this; + ret.callback( function () { + // Store the returned values + // in the sequence's state via seq.values + var returned = arguments + , vals = seq.values; + promises.forEach( function (valName, i) { + vals[valName] = returned[i]; + }); + }); + + return ret; + } + , _bind: function (priorPromise, step) { + var nextPromise = new Promise() + , seq = this; + priorPromise.callback( function () { + seq.applyStep(step).callback( function () { + nextPromise.fulfill(); + }); + }); + return nextPromise; + } + , start: function () { + var steps = this.steps; + + // Pipe through the steps + var priorStepPromise = this.applyStep(steps[0]); + for (var i = 1, l = steps.length; i < l; i++) { + priorStepPromise = bind(priorStepPromise, steps[i]); + } + return priorStepPromise; + } +};