Skip to content

Commit

Permalink
Added new configure middleware that can configured a JSGI stack based…
Browse files Browse the repository at this point in the history
… on array, and modify configurations
  • Loading branch information
kriszyp committed Sep 29, 2014
1 parent 852548b commit 5bb6e5a
Show file tree
Hide file tree
Showing 13 changed files with 314 additions and 83 deletions.
36 changes: 35 additions & 1 deletion README.md
Expand Up @@ -467,13 +467,47 @@ manipulate these JSGI modules unless you want to customize or alter the middlewa
stack. The pintura module provides all these middleware components with a default working
setup that be immediately used without any knowledge of the middleware components
described below. However, understanding the middleware modules can be important
in understanding the full capabilities of Pintura.
in understanding the full capabilities of Pintura, and for reconfiguring the middleware.

Once you have defined a JSGI app, you can start your server with that app like:

require("pintura/start-node").start(app);

The middleware modules in Pintura
are found in the "jsgi" folder. Most of these modules directly a function that can be
used as the middleware function, and typically take configuration information as the
first parameter and the next application as the second. Below are the syntax and description of these modules:

## configure

The Pintura middleware stack is defined by using the `configure` module. This is a function
that takes an array of middleware definitions and creates the stack for you. Once the middleware
stack has been created, you can also modify the stack, using standard array methods. For example,
to create a stack of middleware, we could write:

require('pintura/jsgi/configure');
configuredApp = configure([
{ // we can define middleware by referencing the module
module: 'pintura/jsgi/auth',
// and providing a config
config: security
},
'pintura/jsgi/csrf' // or just use a module id directly
]);

Each middleware entry in the stack can be defined as one of these:

* An object with a module id in the `module` property, or a factory function in the `factory` property. You can optionally include a `config` property if the middleware takes a config in its arguments.
* A plain module id.
* A factory function.

The return app will then have standard array methods available for modifying the stack. Any changes to the stack will cause it automatically rebuild itself. In addition, there are several extra methods:
* get(id) - Get the configuration for a middleware by the module's last segment (in the case of 'pintura/jsgi/csrf', the id would 'csrf'), or the function name.
* set(id, config) - Update the configuration of one of the middleware.
* delete(id) - Remove a middleware component from the stack.

The `indexOf` and `lastIndexOf` methods also support a string id as the argument, in which case it will search for a config with that id.

## auth

app = require('pintura/jsgi/auth')(security, nextApp);
Expand Down
151 changes: 151 additions & 0 deletions jsgi/configure.js
@@ -0,0 +1,151 @@
/**
* Middleware for configuring middleware with a fluent API
*/
var NotFoundError = require('perstore/errors').NotFoundError;

module.exports = function(configuration){
var root;
function configuredApp(request){
// delegate to the root, so we can rebuild the root,
// if and when we reconfigure
return root(request);
}
function end(request){
throw new NotFoundError('unhandled request');
}
var configurationMap;
function build(){
configurationMap = {};
var current = end;
var appConfig;
try{
for(var i = configuration.length; i-- > 0;){
appConfig = configuration[i];
var factory = null;
var configArgument = null;
if(typeof appConfig === 'function'){
factory = appConfig;
appConfig = {
factory: factory
};
}else if(typeof appConfig === 'string'){
factory = require(appConfig);
appConfig = {
factory: factory,
module: appConfig
};
}else if(typeof appConfig.module === 'string'){
factory = require(appConfig.module);
configArgument = appConfig.config;
}else if(typeof appConfig.factory === 'function'){
factory = appConfig.factory;
configArgument = appConfig.config;
}else{
throw new Error('Invalid configuration, no app or module provided ' + JSON.stringify(appConfig));
}
configuration[i] = appConfig;
configurationMap[getId(appConfig)] = appConfig;
current = appConfig.app = configArgument === null ?
factory(current) :
// it needs a configuration
factory(configArgument, current);
if(typeof current !== 'function'){
throw new Error('JSGI app factory did not return a function');
}
}
}catch(error){
console.error('failed to configure JSGI app', appConfig, error);
throw error;
}
root = current;
}

function getId(config){
var module = typeof config === 'string' ?
config : config.module;
if(module){
return module.match(/[^\/]+$/)[0];
}
return (config.factory || config.app).name;
}

build();
function reconfigure(newConfiguration){
configuration = newConfiguration;
build();
}
configuredApp.reconfigure = reconfigure;

// create delegate array methods
function arrayMethod(mutates){
return function(method){
configuredApp[method] = function(){
// delegate the method to the configuration array
var result = [][method].apply(configuration, arguments);
if(mutates){
// rebuild after any change was made
build();
}
return result;
};
};
}
['push', 'pop', 'unshift', 'shift', 'splice', ''].forEach(arrayMethod(true));
['slice', 'forEach', 'map', 'reduce', 'reduceRight'].forEach(arrayMethod());
['filter', 'every', 'some'].forEach(function(method){
configuredApp[method] = function(callback, thisObject){
if(typeof callback == 'string'){
// convert string search to a search by name
var nameToSearch = callback;
callback = function(item){
return getId(item) === nameToSearch;
};
}
// delegate the method to the configuration array
return configuration[method](callback, thisObject);
};
});
configuredApp.indexOf = function(searchFor, starting){
if(typeof searchFor === 'string'){
for(var i = starting || 0, l = configuration.length; i < l; i++){
if(getId(configuration[i]) === searchFor){
return i;
}
}
return -1;
}
return configuration.indexOf(searchFor, starting);
};
configuredApp.lastIndexOf = function(searchFor, starting){
if(typeof searchFor === 'string'){
for(var i = starting || configuration.length; i-- > 0;){
if(getId(configuration[i]) === searchFor){
return i;
}
}
return -1;
}
return configuration.lastIndexOf(searchFor, starting);
};
configuredApp.get = function(id){
return configuredApp.filter(id)[0];
};
configuredApp.set = function(id, config){
var index = configuredApp.indexOf(id);
if(index > -1){
configuredApp.splice(index, 1, config);
}else{
configuredApp.push(config);
}
};
configuredApp.delete = function(id){
var index = configuredApp.indexOf(id);
if(index > -1){
configuredApp.splice(index, 1);
}
};
configuredApp.asArray = function(){
return configuration;
};
return configuredApp;
};
3 changes: 2 additions & 1 deletion jsgi/csrf.js
Expand Up @@ -5,8 +5,9 @@
var CSRFDetect = function(customHeader, nextApp){
if(typeof customHeader == "function"){
nextApp = customHeader;
// default to this common header
customHeader = "x-requested-with";
}
customHeader = customHeader || "x-requested-with";
return function(request){
var headers = request.headers;
if(!(headers[customHeader] || /application\/j/.test(headers.accept) ||
Expand Down
4 changes: 3 additions & 1 deletion jsgi/error.js
Expand Up @@ -9,7 +9,7 @@ var METHOD_HAS_BODY = require("./methods").METHOD_HAS_BODY,
print = require("promised-io/process").print,
when = require("promised-io/promise").when;

exports.ErrorHandler = function(nextApp){
exports = module.exports = function(nextApp){
return function(request){
try{
return when(nextApp(request), function(response){
Expand Down Expand Up @@ -73,3 +73,5 @@ exports.ErrorHandler = function(nextApp){
}
};
};

exports.ErrorHandler = exports;
4 changes: 3 additions & 1 deletion jsgi/http-params.js
Expand Up @@ -5,7 +5,7 @@
* setting window.location to download data).
*/
var httpParamRegex = /^http[_-]/;
exports.HttpParams = function(nextApp){
exports = module.exports = function(nextApp){
return function(request){
var parts = request.queryString.split("&");

Expand Down Expand Up @@ -33,3 +33,5 @@ exports.HttpParams = function(nextApp){
return nextApp(request);
};
};
// back-compat property reference
exports.HttpParams = exports;
6 changes: 4 additions & 2 deletions jsgi/metadata.js
Expand Up @@ -2,7 +2,7 @@
* Applies metadata headers if the responseValue includes a getMetadata function
*/
var when = require("promised-io/promise").when;
exports.Metadata = function(nextApp){
module.exports = function(nextApp){
return function(request){
var metadata;
var input = request.body || '';
Expand Down Expand Up @@ -53,7 +53,9 @@ exports.Metadata = function(nextApp){
return response;
});
};
}
};
// back-compat property access
module.exports.Metadata = module.exports;

/*
var lastModified = callIfPossible(store, "getLastModified", value);
Expand Down
5 changes: 3 additions & 2 deletions jsgi/rest-store.js
Expand Up @@ -10,8 +10,7 @@ var METHOD_HAS_BODY = require("./methods").METHOD_HAS_BODY,
Response = require("./response").Response,
settings = require("perstore/util/settings");

function dir(){var sys=require('sys');for(var i=0,l=arguments.length;i<l;i++)sys.debug(sys.inspect(arguments[i]));}
exports.RestStore = function(options){
module.exports = function(options){
return function(request){
// N.B. in async, options.getDataModel() can be a promise, so have to wait for it
return when(options.getDataModel(request), function(model){
Expand Down Expand Up @@ -163,3 +162,5 @@ exports.RestStore = function(options){
});
};
};
// back-compat property access
module.exports.RestStore = module.exports;
5 changes: 3 additions & 2 deletions jsgi/routes.js
Expand Up @@ -14,7 +14,7 @@ var when = require('promised-io/promise').when,
var routes = [];

//function dir(){var sys=require('sys');for(var i=0,l=arguments.length;i<l;i++)sys.debug(sys.inspect(arguments[i]));}
exports.Routes = function(customRoutes, nextApp){
exports = module.exports = function(customRoutes, nextApp){
// append custom routes
for (var i = 0, l = customRoutes.length; i < l; ++i) {
declare(customRoutes[i]);
Expand Down Expand Up @@ -57,7 +57,8 @@ exports.Routes = function(customRoutes, nextApp){
return nextApp(request);
};
};

// back-compat property access
exports.Routes = exports;
function declare(method, regexp, handler, args){
if (typeof regexp === 'string') {
var named_param_regex = /[\/\.]:(\w+)/g;
Expand Down
5 changes: 3 additions & 2 deletions jsgi/session.js
Expand Up @@ -11,7 +11,7 @@ var promiseModule = require("promised-io/promise"),
sessionModel,
sha1 = require("../util/sha1").hex_sha1;

exports.Session = function(options, nextApp){
exports = module.exports = function(options, nextApp){
// assign defaults
if (!options) options = {};
if(options.model){
Expand Down Expand Up @@ -73,7 +73,8 @@ exports.Session = function(options, nextApp){
});
};
};

// back-compat property reference
exports.Session = exports;
// gets a session, creating a new one if necessary
exports.forceSession = function(request, expires){
var session = request.session;
Expand Down

0 comments on commit 5bb6e5a

Please sign in to comment.