diff --git a/README.md b/README.md index 6caf96e3e..e5d39b782 100755 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/README.md b/docs/README.md index c65f3e214..6d057192a 100755 --- a/docs/README.md +++ b/docs/README.md @@ -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) diff --git a/docs/Reference.md b/docs/Reference.md index 5559d39a3..f07850490 100755 --- a/docs/Reference.md +++ b/docs/Reference.md @@ -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) @@ -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: - `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. @@ -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`. @@ -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'` diff --git a/lib/handler.js b/lib/handler.js index 1afe11195..157ebaa08 100755 --- a/lib/handler.js +++ b/lib/handler.js @@ -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 @@ -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; }; @@ -227,7 +233,7 @@ exports.prerequisites = function (config, server) { /* [ [ - function (request, next) { }, + function (request, reply) { }, { method: function (request, reply) { } assign: key1 @@ -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)); @@ -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; }; diff --git a/lib/methods.js b/lib/methods.js new file mode 100755 index 000000000..b8a21e8ac --- /dev/null +++ b/lib/methods.js @@ -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; +}; diff --git a/lib/pack.js b/lib/pack.js index 92b1e37b2..bd664de05 100755 --- a/lib/pack.js +++ b/lib/pack.js @@ -3,14 +3,13 @@ var Path = require('path'); var Events = require('events'); var Async = require('async'); -var Boom = require('boom'); var Catbox = require('catbox'); var Server = require('./server'); var Views = require('./views'); var Utils = require('./utils'); var Defaults = require('./defaults'); var Ext = require('./ext'); -var Schema = require('./schema'); +var Methods = require('./methods'); // Declare internals @@ -32,9 +31,8 @@ exports = module.exports = internals.Pack = function (options) { this._byLabel = {}; // Server [ids] organized by labels this._byId = {}; // Servers indexed by id this._env = {}; // Plugin-specific environment (e.g. views manager) - this._methods = {}; // Method functions - this._helpers = {}; // Helper functions this._caches = {}; // Cache clients + this._methods = new Methods(this); // Server methods and helpers this.list = {}; // Loaded plugins by name this.plugins = {}; // Exposed plugin properties by name @@ -235,8 +233,8 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen root.path = plugin.path; root.plugins = self.plugins; root.events = self.events; - root.methods = self._methods; - root.helpers = self._helpers; + root.methods = self._methods.methods; + root.helpers = self._methods.helpers; root.expose = function (/* key, value */) { @@ -293,12 +291,12 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen root.method = function (/* name, method, options */) { - return self._method.apply(self, arguments); + return self._methods.add.apply(self._methods, Array.prototype.slice.call(arguments).concat(env)); }; root.helper = function (/* name, method, options */) { - return self._helper.apply(self, arguments); + return self._methods.addHelper.apply(self._methods, arguments); }; root.cache = function (options) { @@ -644,194 +642,15 @@ internals.Pack.prototype._provisionCache = function (options, type, name, segmen }; -internals.Pack.prototype._method = function (/* name, fn, options | {} | [{}, {}] */) { +internals.Pack.prototype._method = function () { - var items = [].concat(typeof arguments[0] === 'string' ? { name: arguments[0], fn: arguments[1], options: arguments[2] } : arguments[0]); - for (var i = 0, il = items.length; i < il; ++i) { - var item = items[i]; - this._addMethod(item.name, item.fn, item.options); - } -}; - - -internals.Pack.prototype._addMethod = function (name, fn, options) { - - 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(/^\w+$/), 'Invalid name:', name); - Utils.assert(!this._methods[name] && !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; - - // Create method - - var cache = null; - if (settings.cache) { - cache = this._provisionCache(settings.cache, 'method', name, settings.cache.segment); - } - - var method = function (/* arguments, methodNext */) { - - if (!cache) { - return fn.apply(null, arguments); - } - - var args = arguments; - var lastArgPos = args.length - 1; - var methodNext = args[lastArgPos]; - - var generateFunc = function (next) { - - args[lastArgPos] = next; - fn.apply(null, args); - }; - - var key = settings.generateKey.apply(null, args); - if (key === null) { // Value can be '' - self.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); - } - }; - } - - this._methods[name] = method; -}; - - -internals.Pack.prototype._helper = 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.Pack.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._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.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; + return this._methods.add.apply(this._methods, arguments); }; -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()); - } +internals.Pack.prototype._helper = function () { - return key; + return this._methods.addHelper.apply(this._methods, arguments); }; diff --git a/lib/schema.js b/lib/schema.js index 044587d83..d5b092b34 100755 --- a/lib/schema.js +++ b/lib/schema.js @@ -172,6 +172,7 @@ internals.routeConfigSchema = { pre: Joi.array().includes(internals.pre.concat(Joi.array().includes(internals.pre).min(1))), handler: [ Joi.func(), + Joi.string(), Joi.object({ directory: Joi.object({ path: [ @@ -311,6 +312,7 @@ exports.method = function (options) { internals.methodSchema = { + bind: Joi.object().allow(null), generateKey: Joi.func(), cache: internals.cacheSchema }; diff --git a/lib/server.js b/lib/server.js index 2890965f0..9e9aacfb0 100755 --- a/lib/server.js +++ b/lib/server.js @@ -134,8 +134,8 @@ module.exports = internals.Server = function (/* host, port, options */) { this.plugins = {}; // Registered plugin APIs by plugin name this.app = {}; // Place for application-specific state without conflicts with hapi, should not be used by plugins - this.methods = this.pack._methods; // Method functions - this.helpers = this.pack._helpers; // Helper functions + this.methods = this.pack._methods.methods; // Method functions + this.helpers = this.pack._methods.helpers; // Helper functions // Generate CORS headers diff --git a/package.json b/package.json index 1d25960b7..c2ea160fd 100755 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "hapi", "description": "HTTP Server framework", "homepage": "http://hapijs.com", - "version": "2.5.0", + "version": "2.6.0", "repository": { "type": "git", "url": "git://github.com/spumko/hapi" @@ -18,7 +18,7 @@ "node": "0.10.x" }, "dependencies": { - "hoek": "^1.4.x", + "hoek": "^1.5.x", "boom": "^2.2.x", "joi": "^2.7.x", "catbox": "1.x.x", diff --git a/test/handler.js b/test/handler.js index a3b86d3ea..91033b846 100755 --- a/test/handler.js +++ b/test/handler.js @@ -345,13 +345,13 @@ describe('Handler', function () { }); }); - it('returns a user record using helper', function (done) { + it('returns a user record using server method with trailing space', function (done) { var server = new Hapi.Server(); - server.helper('user', function (id, next) { + server.method('user', function (id, next) { - return next({ id: id, name: 'Bob' }); + return next(null, { id: id, name: 'Bob' }); }); server.route({ @@ -359,7 +359,7 @@ describe('Handler', function () { path: '/user/{id}', config: { pre: [ - 'user(params.id)' + 'user(params.id )' ], handler: function (request, reply) { @@ -375,43 +375,127 @@ describe('Handler', function () { }); }); - it('returns a user name using multiple helpers', function (done) { + it('returns a user record using server method with leading space', function (done) { var server = new Hapi.Server(); - server.helper('user', function (id, next) { + server.method('user', function (id, next) { + + return next(null, { id: id, name: 'Bob' }); + }); - return next({ id: id, name: 'Bob' }); + server.route({ + method: 'GET', + path: '/user/{id}', + config: { + pre: [ + 'user( params.id)' + ], + handler: function (request, reply) { + + return reply(request.pre.user); + } + } + }); + + server.inject('/user/5', function (res) { + + expect(res.result).to.deep.equal({ id: '5', name: 'Bob' }); + done(); }); + }); + + it('returns a user record using server method with zero args', function (done) { - server.helper('name', function (user, next) { + var server = new Hapi.Server(); + + server.method('user', function (next) { - return next(user.name); + return next(null, { name: 'Bob' }); }); server.route({ method: 'GET', - path: '/user/{id}/name', + path: '/user', config: { pre: [ - 'user(params.id)', - 'name(pre.user)' + 'user()' ], handler: function (request, reply) { - return reply(request.pre.name); + return reply(request.pre.user); } } }); - server.inject('/user/5/name', function (res) { + server.inject('/user', function (res) { - expect(res.result).to.equal('Bob'); + expect(res.result).to.deep.equal({ name: 'Bob' }); + done(); + }); + }); + + it('returns a user record using server method with no args', function (done) { + + var server = new Hapi.Server(); + + server.method('user', function (request, next) { + + return next(null, { id: request.params.id, name: 'Bob' }); + }); + + server.route({ + method: 'GET', + path: '/user/{id}', + config: { + pre: [ + 'user' + ], + handler: function (request, reply) { + + return reply(request.pre.user); + } + } + }); + + server.inject('/user/5', function (res) { + + expect(res.result).to.deep.equal({ id: '5', name: 'Bob' }); + done(); + }); + }); + + it('returns a user record using server method with nested name', function (done) { + + var server = new Hapi.Server(); + + server.method('user.get', function (next) { + + return next(null, { name: 'Bob' }); + }); + + server.route({ + method: 'GET', + path: '/user', + config: { + pre: [ + 'user.get()' + ], + handler: function (request, reply) { + + return reply(request.pre['user.get']); + } + } + }); + + server.inject('/user', function (res) { + + expect(res.result).to.deep.equal({ name: 'Bob' }); done(); }); }); - it('fails on bad helper name', function (done) { + it('fails on bad method name', function (done) { var server = new Hapi.Server(); var test = function () { @@ -431,7 +515,7 @@ describe('Handler', function () { }); }; - expect(test).to.throw('Unknown server helper or method in prerequisite string'); + expect(test).to.throw('Unknown server helper or method in string notation: xuser(params.id)'); done(); }); @@ -455,8 +539,24 @@ describe('Handler', function () { }); }; - expect(test).to.throw('Invalid prerequisite string syntax'); + expect(test).to.throw('Invalid server method string notation: userparams.id)'); done(); }); + + it('uses string handler', function (done) { + + var server = new Hapi.Server(); + server.method('handler.get', function (request, next) { + + next(null, request.params.x + request.params.y); + }); + + server.route({ method: 'GET', path: '/{x}/{y}', handler: 'handler.get' }); + server.inject('/a/b', function (res) { + + expect(res.result).to.equal('ab'); + done(); + }); + }) }); diff --git a/test/helper.js b/test/helper.js index aaff4d13e..9265a1d49 100755 --- a/test/helper.js +++ b/test/helper.js @@ -207,110 +207,173 @@ describe('Helper', function () { }); }); - describe('with cache', function () { + it('returns a valid result when calling a helper using the cache', function (done) { - it('returns a valid result when calling a helper using the cache', function (done) { + var server = new Hapi.Server(0, { cache: 'memory' }); - var server = new Hapi.Server(0, { cache: 'memory' }); - - var gen = 0; - server.helper('user', function (id, next) { return next({ id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); + var gen = 0; + server.helper('user', function (id, next) { return next({ id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); - server.start(function () { + server.start(function () { - var id = Math.random(); - server.helpers.user(id, function (result1) { + var id = Math.random(); + server.helpers.user(id, function (result1) { - expect(result1.id).to.equal(id); - expect(result1.gen).to.equal(1); - server.helpers.user(id, function (result2) { + expect(result1.id).to.equal(id); + expect(result1.gen).to.equal(1); + server.helpers.user(id, function (result2) { - expect(result2.id).to.equal(id); - expect(result2.gen).to.equal(1); - done(); - }); + expect(result2.id).to.equal(id); + expect(result2.gen).to.equal(1); + done(); }); }); }); + }); - it('supports empty key helper', function (done) { + it('supports empty key helper', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); + var server = new Hapi.Server(0, { cache: 'memory' }); - var gen = 0; - var terms = 'I agree to give my house'; - server.helper('tos', function (next) { return next({ gen: gen++, terms: terms }); }, { cache: { expiresIn: 2000 } }); + var gen = 0; + var terms = 'I agree to give my house'; + server.helper('tos', function (next) { return next({ gen: gen++, terms: terms }); }, { cache: { expiresIn: 2000 } }); - server.start(function () { + server.start(function () { - server.helpers.tos(function (result1) { + server.helpers.tos(function (result1) { - expect(result1.terms).to.equal(terms); - expect(result1.gen).to.equal(0); - server.helpers.tos(function (result2) { + expect(result1.terms).to.equal(terms); + expect(result1.gen).to.equal(0); + server.helpers.tos(function (result2) { - expect(result2.terms).to.equal(terms); - expect(result2.gen).to.equal(0); - done(); - }); + expect(result2.terms).to.equal(terms); + expect(result2.gen).to.equal(0); + done(); }); }); }); + }); - it('returns valid results when calling a helper (with different keys) using the cache', function (done) { + it('returns valid results when calling a helper (with different keys) using the cache', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); - var gen = 0; - server.helper('user', function (id, next) { return next({ id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); - server.start(function () { - - var id1 = Math.random(); - server.helpers.user(id1, function (result1) { + var server = new Hapi.Server(0, { cache: 'memory' }); + var gen = 0; + server.helper('user', function (id, next) { return next({ id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); + server.start(function () { + + var id1 = Math.random(); + server.helpers.user(id1, function (result1) { - expect(result1.id).to.equal(id1); - expect(result1.gen).to.equal(1); - var id2 = Math.random(); - server.helpers.user(id2, function (result2) { + expect(result1.id).to.equal(id1); + expect(result1.gen).to.equal(1); + var id2 = Math.random(); + server.helpers.user(id2, function (result2) { - expect(result2.id).to.equal(id2); - expect(result2.gen).to.equal(2); - done(); - }); + expect(result2.id).to.equal(id2); + expect(result2.gen).to.equal(2); + done(); }); }); }); + }); - it('returns new object (not cached) when second key generation fails when using the cache', function (done) { + it('returns new object (not cached) when second key generation fails when using the cache', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); - var id1 = Math.random(); - var gen = 0; - var helper = function (id, next) { + var server = new Hapi.Server(0, { cache: 'memory' }); + var id1 = Math.random(); + var gen = 0; + var helper = function (id, next) { - if (typeof id === 'function') { - id = id1; - } + if (typeof id === 'function') { + id = id1; + } - return next({ id: id, gen: ++gen }); - }; + return next({ id: id, gen: ++gen }); + }; - server.helper([{ name: 'user', method: helper, options: { cache: { expiresIn: 2000 } } }]); + server.helper([{ name: 'user', method: helper, options: { cache: { expiresIn: 2000 } } }]); - server.start(function () { + server.start(function () { - server.helpers.user(id1, function (result1) { + server.helpers.user(id1, function (result1) { - expect(result1.id).to.equal(id1); - expect(result1.gen).to.equal(1); + expect(result1.id).to.equal(id1); + expect(result1.gen).to.equal(1); - server.helpers.user(function () { }, function (result2) { + server.helpers.user(function () { }, function (result2) { - expect(result2.id).to.equal(id1); - expect(result2.gen).to.equal(2); - done(); - }); + expect(result2.id).to.equal(id1); + expect(result2.gen).to.equal(2); + done(); }); }); }); }); + + it('returns a user record using helper', function (done) { + + var server = new Hapi.Server(); + + server.helper('user', function (id, next) { + + return next({ id: id, name: 'Bob' }); + }); + + server.route({ + method: 'GET', + path: '/user/{id}', + config: { + pre: [ + 'user(params.id)' + ], + handler: function (request, reply) { + + return reply(request.pre.user); + } + } + }); + + server.inject('/user/5', function (res) { + + expect(res.result).to.deep.equal({ id: '5', name: 'Bob' }); + done(); + }); + }); + + it('returns a user name using multiple helpers', function (done) { + + var server = new Hapi.Server(); + + server.helper('user', function (id, next) { + + return next({ id: id, name: 'Bob' }); + }); + + server.helper('name', function (user, next) { + + return next(user.name); + }); + + server.route({ + method: 'GET', + path: '/user/{id}/name', + config: { + pre: [ + 'user(params.id)', + 'name(pre.user)' + ], + handler: function (request, reply) { + + return reply(request.pre.name); + } + } + }); + + server.inject('/user/5/name', function (res) { + + expect(res.result).to.equal('Bob'); + done(); + }); + }); }); \ No newline at end of file diff --git a/test/methods.js b/test/methods.js index d6e9cd0a2..76de19515 100755 --- a/test/methods.js +++ b/test/methods.js @@ -20,6 +20,80 @@ var it = Lab.test; describe('Method', function () { + it('registers a method', function (done) { + + var add = function (a, b, next) { + + return next(null, a + b); + }; + + var server = new Hapi.Server(0); + server.method('add', add); + + server.start(function () { + + server.methods.add(1, 5, function (err, result) { + + expect(result).to.equal(6); + done(); + }); + }); + }); + + it('registers a method with nested name', function (done) { + + var add = function (a, b, next) { + + return next(null, a + b); + }; + + var server = new Hapi.Server(0); + server.method('tools.add', add); + + server.start(function () { + + server.methods.tools.add(1, 5, function (err, result) { + + expect(result).to.equal(6); + done(); + }); + }); + }); + + it('throws when registering a method with nested name twice', function (done) { + + var add = function (a, b, next) { + + return next(null, a + b); + }; + + var server = new Hapi.Server(0); + server.method('tools.add', add); + expect(function () { + + server.method('tools.add', add); + }).to.throw('Helper or method function name already exists'); + + done(); + }); + + it('throws when registering a method with name nested through a function', function (done) { + + var add = function (a, b, next) { + + return next(null, a + b); + }; + + var server = new Hapi.Server(0); + server.method('add', add); + expect(function () { + + server.method('add.another', add); + }).to.throw('Invalid segment another in reach path add.another'); + + done(); + }); + it('reuses cached method value', function (done) { var gen = 0; @@ -46,6 +120,32 @@ describe('Method', function () { }); }); + it('does not cache value when isUncacheable is true', function (done) { + + var gen = 0; + var method = function (id, next) { + + return next(null, { id: id, gen: gen++ }, true); + }; + + var server = new Hapi.Server(0); + server.method('test', method, { cache: { expiresIn: 1000 } }); + + server.start(function () { + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(0); + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(1); + done(); + }); + }); + }); + }); + it('generates new value after cache drop', function (done) { var gen = 0; @@ -108,6 +208,35 @@ describe('Method', function () { done(); }); + it('throws an error when name is invalid', function (done) { + + expect(function () { + + var server = new Hapi.Server(); + server.method('0', function () { }); + }).to.throw(Error); + + expect(function () { + + var server = new Hapi.Server(); + server.method('a..', function () { }); + }).to.throw(Error); + + expect(function () { + + var server = new Hapi.Server(); + server.method('a.0', function () { }); + }).to.throw(Error); + + expect(function () { + + var server = new Hapi.Server(); + server.method('.a', function () { }); + }).to.throw(Error); + + done(); + }); + it('throws an error when fn is not a function', function (done) { var fn = function () { @@ -207,108 +336,155 @@ describe('Method', function () { }); }); - describe('with cache', function () { - - it('returns a valid result when calling a method using the cache', function (done) { + it('returns a valid result when calling a method using the cache', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); + var server = new Hapi.Server(0, { cache: 'memory' }); - var gen = 0; - server.method('user', function (id, next) { return next(null, { id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); + var gen = 0; + server.method('user', function (id, next) { return next(null, { id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); - server.start(function () { + server.start(function () { - var id = Math.random(); - server.methods.user(id, function (err, result1) { + var id = Math.random(); + server.methods.user(id, function (err, result1) { - expect(result1.id).to.equal(id); - expect(result1.gen).to.equal(1); - server.methods.user(id, function (err, result2) { + expect(result1.id).to.equal(id); + expect(result1.gen).to.equal(1); + server.methods.user(id, function (err, result2) { - expect(result2.id).to.equal(id); - expect(result2.gen).to.equal(1); - done(); - }); + expect(result2.id).to.equal(id); + expect(result2.gen).to.equal(1); + done(); }); }); }); + }); - it('supports empty key method', function (done) { + it('supports empty key method', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); + var server = new Hapi.Server(0, { cache: 'memory' }); - var gen = 0; - var terms = 'I agree to give my house'; - server.method('tos', function (next) { return next(null, { gen: gen++, terms: terms }); }, { cache: { expiresIn: 2000 } }); + var gen = 0; + var terms = 'I agree to give my house'; + server.method('tos', function (next) { return next(null, { gen: gen++, terms: terms }); }, { cache: { expiresIn: 2000 } }); - server.start(function () { + server.start(function () { - server.methods.tos(function (err, result1) { + server.methods.tos(function (err, result1) { - expect(result1.terms).to.equal(terms); - expect(result1.gen).to.equal(0); - server.methods.tos(function (err, result2) { + expect(result1.terms).to.equal(terms); + expect(result1.gen).to.equal(0); + server.methods.tos(function (err, result2) { - expect(result2.terms).to.equal(terms); - expect(result2.gen).to.equal(0); - done(); - }); + expect(result2.terms).to.equal(terms); + expect(result2.gen).to.equal(0); + done(); }); }); }); + }); - it('returns valid results when calling a method (with different keys) using the cache', function (done) { + it('returns valid results when calling a method (with different keys) using the cache', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); - var gen = 0; - server.method('user', function (id, next) { return next(null, { id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); - server.start(function () { - - var id1 = Math.random(); - server.methods.user(id1, function (err, result1) { + var server = new Hapi.Server(0, { cache: 'memory' }); + var gen = 0; + server.method('user', function (id, next) { return next(null, { id: id, gen: ++gen }); }, { cache: { expiresIn: 2000 } }); + server.start(function () { + + var id1 = Math.random(); + server.methods.user(id1, function (err, result1) { - expect(result1.id).to.equal(id1); - expect(result1.gen).to.equal(1); - var id2 = Math.random(); - server.methods.user(id2, function (err, result2) { + expect(result1.id).to.equal(id1); + expect(result1.gen).to.equal(1); + var id2 = Math.random(); + server.methods.user(id2, function (err, result2) { - expect(result2.id).to.equal(id2); - expect(result2.gen).to.equal(2); - done(); - }); + expect(result2.id).to.equal(id2); + expect(result2.gen).to.equal(2); + done(); }); }); }); + }); - it('returns new object (not cached) when second key generation fails when using the cache', function (done) { + it('returns new object (not cached) when second key generation fails when using the cache', function (done) { - var server = new Hapi.Server(0, { cache: 'memory' }); - var id1 = Math.random(); - var gen = 0; - var method = function (id, next) { + var server = new Hapi.Server(0, { cache: 'memory' }); + var id1 = Math.random(); + var gen = 0; + var method = function (id, next) { - if (typeof id === 'function') { - id = id1; - } + if (typeof id === 'function') { + id = id1; + } - return next(null, { id: id, gen: ++gen }); - }; + return next(null, { id: id, gen: ++gen }); + }; - server.method([{ name: 'user', fn: method, options: { cache: { expiresIn: 2000 } } }]); + server.method([{ name: 'user', fn: method, options: { cache: { expiresIn: 2000 } } }]); - server.start(function () { + server.start(function () { - server.methods.user(id1, function (err, result1) { + server.methods.user(id1, function (err, result1) { - expect(result1.id).to.equal(id1); - expect(result1.gen).to.equal(1); + expect(result1.id).to.equal(id1); + expect(result1.gen).to.equal(1); - server.methods.user(function () { }, function (err, result2) { + server.methods.user(function () { }, function (err, result2) { - expect(result2.id).to.equal(id1); - expect(result2.gen).to.equal(2); - done(); - }); + expect(result2.id).to.equal(id1); + expect(result2.gen).to.equal(2); + done(); + }); + }); + }); + }); + + it('sets method bind without cache', function (done) { + + var method = function (id, next) { + + return next(null, { id: id, gen: this.gen++ }); + }; + + var server = new Hapi.Server(0); + server.method('test', method, { bind: { gen: 7 } }); + + server.start(function () { + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(8); + done(); + }); + }); + }); + }); + + it('sets method bind with cache', function (done) { + + var method = function (id, next) { + + return next(null, { id: id, gen: this.gen++ }); + }; + + var server = new Hapi.Server(0); + server.method('test', method, { bind: { gen: 7 }, cache: { expiresIn: 1000 } }); + + server.start(function () { + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + + server.methods.test(1, function (err, result) { + + expect(result.gen).to.equal(7); + done(); }); }); });