Skip to content

Commit

Permalink
Prerequisites and server methods service pack. Closes #1450, closes #…
Browse files Browse the repository at this point in the history
…1449, closes #1448, closes #1447, closes #1446, closes #1445
  • Loading branch information
Eran Hammer committed Feb 22, 2014
1 parent 8fa375f commit f926411
Show file tree
Hide file tree
Showing 12 changed files with 784 additions and 367 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ infrastructure. The framework supports a powerful plugin architecture for pain-f

For the latest updates and release information follow [@hapijs](https://twitter.com/hapijs) on twitter.

Current version: **2.5.x**
Current version: **2.6.x**

Node version: **0.10** required

Expand Down
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## Current Documentation
[v2.5.x](https://github.com/spumko/hapi/blob/master/docs/Reference.md)
[v2.6.x](https://github.com/spumko/hapi/blob/master/docs/Reference.md)

## Previous Documentation
[v2.5.x](https://github.com/spumko/hapi/blob/v2.5.0/docs/Reference.md)
[v2.4.x](https://github.com/spumko/hapi/blob/v2.4.0/docs/Reference.md)
[v2.3.x](https://github.com/spumko/hapi/blob/v2.3.0/docs/Reference.md)
[v2.2.x](https://github.com/spumko/hapi/blob/89b12a2/docs/Reference.md)
Expand Down
15 changes: 10 additions & 5 deletions docs/Reference.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 2.5.x API Reference
# 2.6.x API Reference

- [`Hapi.Server`](#hapiserver)
- [`new Server([host], [port], [options])`](#new-serverhost-port-options)
Expand Down Expand Up @@ -357,7 +357,7 @@ The following options are available when adding a route:
Matching is done against the hostname part of the header only (excluding the port). Defaults to all hosts.

- `handler` - (required) the function called to generate the response after successful authentication and validation. The handler function is
described in [Route handler](#route-handler). Alternatively, `handler` can be assigned an object with one of:
described in [Route handler](#route-handler). If set to a string, the value is parsed the same way a prerequisite server method string shortcut is processed. Alternatively, `handler` can be assigned an object with one of:
- <a name="route.config.file"></a>`file` - generates a static file endpoint for serving a single file. `file` can be set to:
- a relative or absolute file path string (relative paths are resolved based on the server [`files`](#server.config.files) configuration).
- a function with the signature `function(request)` which returns the relative or absolute file path.
Expand Down Expand Up @@ -1086,13 +1086,18 @@ handlers without having to create a common module.
Methods are registered via `server.method(name, fn, [options])` where:

- `name` - a unique method name used to invoke the method via `server.methods[name]`. When configured with caching enabled,
`server.methods[name].cache.drop(arg1, arg2, ..., argn, callback)` can be used to clear the cache for a given key.
`server.methods[name].cache.drop(arg1, arg2, ..., argn, callback)` can be used to clear the cache for a given key. Supports using
nested names such as `utils.users.get` which will automatically create the missing path under `server.methods` and can be accessed
for the previous example via `server.methods.utils.users.get`.
- `fn` - the method function with the signature is `function(arg1, arg2, ..., argn, next)` where:
- `arg1`, `arg2`, etc. - the method function arguments.
- `next` - the function called when the method is done with the signature `function(err, result)` where:
- `next` - the function called when the method is done with the signature `function(err, result, isUncacheable)` where:
- `err` - error response if the method failed.
- `result` - the return value.
- `isUncacheable` - `true` if result is valid but cannot be cached. Defaults to `false`.
- `options` - optional configuration:
- `bind` - an object passed back to the provided method function (via `this`) when called. Defaults to `null` unless added via a plugin, in which
case it defaults to the plugin bind object.
- `cache` - cache configuration as described in [**catbox** module documentation](https://github.com/spumko/catbox#policy) with a few additions:
- `expiresIn` - relative expiration expressed in the number of milliseconds since the item was saved in the cache. Cannot be used
together with `expiresAt`.
Expand Down Expand Up @@ -1578,7 +1583,7 @@ Transmits a file from the file system. The 'Content-Type' header defaults to the
- `options` - optional settings:
- `filePath` - a relative or absolute file path string (relative paths are resolved based on the server [`files`](#server.config.files) configuration).
- `options` - optional configuration:
- `filename` - optional filename to send if 'Content-Disposition' header is sent, defaults to basename of `path`
- `filename` - optional filename to send if the 'Content-Disposition' header is sent. Defaults to basename of `path`.
- `mode` - value of the HTTP 'Content-Disposition' header. Allowed values:
- `'attachment'`
- `'inline'`
Expand Down
50 changes: 35 additions & 15 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ var File = require('./file');
var Directory = require('./directory');
var Proxy = require('./proxy');
var Views = require('./views');
var Methods = require('./methods');


// Declare internals
Expand Down Expand Up @@ -214,6 +215,11 @@ exports.configure = function (handler, route) {
}
}

if (typeof handler === 'string') {
var parsed = internals.fromString(handler, route.server);
return parsed.method;
}

return handler;
};

Expand All @@ -227,7 +233,7 @@ exports.prerequisites = function (config, server) {
/*
[
[
function (request, next) { },
function (request, reply) { },
{
method: function (request, reply) { }
assign: key1
Expand Down Expand Up @@ -260,7 +266,9 @@ exports.prerequisites = function (config, server) {
}

if (typeof item.method === 'string') {
internals.preString(item, server);
var parsed = internals.fromString(item.method, server);
item.method = parsed.method;
item.assign = item.assign || parsed.name;
}

set.push(internals.pre(item));
Expand All @@ -275,29 +283,41 @@ exports.prerequisites = function (config, server) {
};


internals.preString = function (pre, server) {
internals.fromString = function (notation, server) {

var preMethodParts = pre.method.match(/^(\w+)(?:\s*)\((\s*\w+(?:\.\w+)*\s*(?:\,\s*\w+(?:\.\w+)*\s*)*)?\)$/);
Utils.assert(preMethodParts, 'Invalid prerequisite string syntax:', pre.method);
// 1:name 2:( 3:arguments
var methodParts = notation.match(/^([\w\.]+)(?:\s*)(?:(\()(?:\s*)(\w+(?:\.\w+)*(?:\s*\,\s*\w+(?:\.\w+)*)*)?(?:\s*)\))?$/);
Utils.assert(methodParts, 'Invalid server method string notation:', notation);

var name = preMethodParts[1];
var helper = server.methods[name] || server.helpers[name];
Utils.assert(helper, 'Unknown server helper or method in prerequisite string:', pre.method);
var name = methodParts[1];
Utils.assert(name.match(Methods.methodNameRx), 'Invalid server method name:', name);

pre.assign = pre.assign || name;
var helperArgs = preMethodParts[2].split(/\s*\,\s*/);
var method = Utils.reach(server.methods, name, { functions: false }) || server.helpers[name];
Utils.assert(method, 'Unknown server helper or method in string notation:', notation);

pre.method = function (request, reply) {
var result = { name: name };
var argsNotation = !!methodParts[2];
var methodArgs = (argsNotation ? (methodParts[3] || '').split(/\s*\,\s*/) : null);

result.method = function (request, reply) {

if (!argsNotation) {
return method.call(null, request, reply);
}

var args = [];
for (var i = 0, il = helperArgs.length; i < il; ++i) {
var arg = helperArgs[i];
args.push(Utils.reach(request, arg));
for (var i = 0, il = methodArgs.length; i < il; ++i) {
var arg = methodArgs[i];
if (arg) {
args.push(Utils.reach(request, arg));
}
}

args.push(reply);
helper.apply(null, args);
method.apply(null, args);
};

return result;
};


Expand Down
231 changes: 231 additions & 0 deletions lib/methods.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Load modules

var Boom = require('boom');
var Utils = require('./utils');
var Schema = require('./schema');


// Declare internals

var internals = {};


exports = module.exports = internals.Methods = function (pack) {

this.pack = pack;
this.methods = {};
this.helpers = {};
};


internals.Methods.prototype.add = function (/* name, fn, options, env | {}, env | [{}, {}], env */) {

if (typeof arguments[0] === 'string') {
return internals.Methods.prototype._add.apply(this, arguments);
}

var items = [].concat(arguments[0]);
var env = arguments[1];
for (var i = 0, il = items.length; i < il; ++i) {
var item = items[i];
this._add(item.name, item.fn, item.options, env);
}
};


exports.methodNameRx = /^[a-zA-Z]\w*(?:\.[a-zA-Z]\w*)*$/;


internals.Methods.prototype._add = function (name, fn, options, env) {

var self = this;

Utils.assert(typeof fn === 'function', 'fn must be a function');
Utils.assert(typeof name === 'string', 'name must be a string');
Utils.assert(name.match(exports.methodNameRx), 'Invalid name:', name);
Utils.assert(!Utils.reach(this.methods, name, { functions: false }) && !this.helpers[name], 'Helper or method function name already exists');

var schemaError = Schema.method(options);
Utils.assert(!schemaError, 'Invalid method options for', name, ':', schemaError);

var settings = Utils.clone(options || {});
settings.generateKey = settings.generateKey || internals.generateKey;

var bind = (env && env.bind) || settings.bind || null;

// Create method

var cache = null;
if (settings.cache) {
cache = this.pack._provisionCache(settings.cache, 'method', name, settings.cache.segment);
}

var method = function (/* arguments, methodNext */) {

if (!cache) {
return fn.apply(bind, arguments);
}

var args = arguments;
var lastArgPos = args.length - 1;
var methodNext = args[lastArgPos];

var generateFunc = function (next) {

args[lastArgPos] = next; // function (err, result, isUncacheable)
fn.apply(bind, args);
};

var key = settings.generateKey.apply(bind, args);
if (key === null) { // Value can be ''
self.pack.log(['hapi', 'method', 'key', 'error'], { name: name, args: args });
}

cache.getOrGenerate(key, generateFunc, methodNext);
};

if (cache) {
method.cache = {
drop: function (/* arguments, callback */) {

var dropCallback = arguments[arguments.length - 1];

var key = settings.generateKey.apply(null, arguments);
if (key === null) { // Value can be ''
return Utils.nextTick(dropCallback)(Boom.badImplementation('Invalid method key'));
}

return cache.drop(key, dropCallback);
}
};
}

// create method path

var path = name.split('.');
var ref = this.methods;
for (var i = 0, il = path.length; i < il; ++i) {
if (!ref[path[i]]) {
ref[path[i]] = (i + 1 === il ? method : {});
}

ref = ref[path[i]];
}
};


internals.generateKey = function () {

var key = 'h';
for (var i = 0, il = arguments.length - 1; i < il; ++i) { // 'arguments.length - 1' to skip 'next'
var arg = arguments[i];
if (typeof arg !== 'string' &&
typeof arg !== 'number' &&
typeof arg !== 'boolean') {

return null;
}

key += ':' + encodeURIComponent(arg.toString());
}

return key;
};


// Backwards compatibility - remove in v3.0

internals.Methods.prototype.addHelper = function (/* name, method, options | {} | [{}, {}] */) {

var helper = (typeof arguments[0] === 'string' ? { name: arguments[0], method: arguments[1], options: arguments[2] } : arguments[0]);
var helpers = [].concat(helper);

for (var i = 0, il = helpers.length; i < il; ++i) {
var item = helpers[i];
this._addHelper(item.name, item.method, item.options);
}
};


internals.Methods.prototype._addHelper = function (name, method, options) {

var self = this;

Utils.assert(typeof method === 'function', 'method must be a function');
Utils.assert(typeof name === 'string', 'name must be a string');
Utils.assert(name.match(/^\w+$/), 'Invalid name:', name);
Utils.assert(!this.methods[name] && !this.helpers[name], 'Helper or method function name already exists');

var schemaError = Schema.helper(options);
Utils.assert(!schemaError, 'Invalid helper options for', name, ':', schemaError);

var settings = Utils.clone(options || {});
settings.generateKey = settings.generateKey || internals.generateKey;

// Create helper

var cache = null;
if (settings.cache) {
cache = this.pack._provisionCache(settings.cache, 'method', name, settings.cache.segment);
}

var helper = function (/* arguments, helperNext */) {

// Prepare arguments

var args = arguments;
var lastArgPos = args.length - 1;
var helperNext = args[lastArgPos];

// Wrap method for Cache.Stale interface 'function (next) { next(err, value); }'

var generateFunc = function (next) {

args[lastArgPos] = function (result) {

if (result instanceof Error) {
return next(result);
}

return next(null, result);
};

method.apply(null, args);
};

if (!cache) {
return generateFunc(function (err, result) {

helperNext(err || result);
});
}

var key = settings.generateKey.apply(null, args);
if (key === null) { // Value can be ''
self.pack.log(['hapi', 'helper', 'key', 'error'], { name: name, args: args });
}

cache.getOrGenerate(key, generateFunc, function (err, value, cached, report) {

return helperNext(err || value);
});
};

if (cache) {
helper.cache = {
drop: function (/* arguments, callback */) {

var dropCallback = arguments[arguments.length - 1];

var key = settings.generateKey.apply(null, arguments);
if (key === null) { // Value can be ''
return Utils.nextTick(dropCallback)(Boom.badImplementation('Invalid helper key'));
}

return cache.drop(key, dropCallback);
}
};
}

this.helpers[name] = helper;
};
Loading

0 comments on commit f926411

Please sign in to comment.