Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow plugins to register handler types #1521

Merged
merged 15 commits into from
Apr 5, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/Reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [`server.method(name, fn, [options])`](#servermethodname-fn-options)
- [`server.method(method)`](#servermethodmethod)
- [`server.inject(options, callback)`](#serverinjectoptions-callback)
- [`server.handler(name, method)`](#serverhandlername-method)
- [`Server` events](#server-events)
- [Request object](#request-object)
- [`request` properties](#request-properties)
Expand Down Expand Up @@ -93,6 +94,7 @@
- [`plugin.require(names, callback)`](#pluginrequirenames-callback)
- [`plugin.loader(require)`](#pluginloader-require)
- [`plugin.bind(bind)`](#pluginbind-bind)
- [`plugin.handler(name, method)`](#pluginhandlername-method)
- [Selectable methods and properties](#selectable-methods-and-properties)
- [`plugin.select(labels)`](#pluginselectlabels)
- [`plugin.length`](#pluginlength)
Expand Down Expand Up @@ -1241,6 +1243,35 @@ server.inject('/', function (res) {
});
```

#### `server.handler(name, method)`

Registers a new handler type. This allows you to define a new handler type which can then be used in routes. Overriding the built in handler types (`directory`, `file`, `proxy`, and `view`), or any previously registered type is not allowed.

- `name` - String name for the handler that you want to register.
- `method` - The method that will be used to handle the requests routed to it.

```javascript
var Hapi = require('hapi');
var server = Hapi.createServer('localhost', 8000);

// Defines new handler for routes on this server
server.handler('test', function (route, options) {

return function (request, reply) {

reply ('new handler: ' + options.msg);
}
});

server.route({
method: 'GET',
path: '/',
handler: { test: { msg: 'test' } }
});

server.start();
```

### `Server` events

The server object inherits from `Events.EventEmitter` and emits the following events:
Expand Down Expand Up @@ -2711,6 +2742,29 @@ exports.register = function (plugin, options, next) {
};
```

#### `plugin.handler(name, method)`

Registers a new handler type. This allows you to define a new handler type which can then be used in routes. Overriding the built in handler types (`directory`, `file`, `proxy`, and `view`), or any previously registered type is not allowed.

- `name` - String name for the handler that you want to register.
- `method` - The method that will be used to handle the requests routed to it.

```javascript
exports.register = function (plugin, options, next) {

var handlerFunc = function (route, options) {

return function(request, reply) {

reply('Message from plugin handler: ' + options.msg);
}
};

plugin.handler('testHandler', handlerFunc);
next();
}
```

### Selectable methods and properties

The plugin interface selectable methods and properties are those available both on the `plugin` object received via the
Expand Down
4 changes: 4 additions & 0 deletions lib/directory.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
var Fs = require('fs');
var Path = require('path');
var Boom = require('boom');
var Joi = require('joi');
var Async = require('async');
var Response = require('./response');
var File = require('./file');
var Utils = require('./utils');
var Schema = require('./schema');


// Declare internals
Expand All @@ -16,6 +18,8 @@ var internals = {};

exports.handler = function (route, options) {

var schemaError = Joi.validate(options, Schema.directoryHandlerSchema);
Utils.assert(!schemaError, 'Invalid handler options for directory:', schemaError);
Utils.assert(route.path[route.path.length - 1] === '}', 'The route path must end with a parameter:', route.path);
Utils.assert(route.params.length >= 1, 'The route path must include at least one parameter:', route.path);

Expand Down
4 changes: 4 additions & 0 deletions lib/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ var Path = require('path');
var Crypto = require('crypto');
var Mime = require('mime');
var Boom = require('boom');
var Joi = require('joi');
var Utils = require('./utils');
var Response = require('./response');
var Schema = require('./schema');


// Declare internals
Expand All @@ -16,6 +18,8 @@ var internals = {};

exports.handler = function (route, options) {

var schemaError = Joi.validate(options, Schema.fileHandlerSchema);
Utils.assert(!schemaError, 'Invalid handler options for file:', schemaError);
var settings = (typeof options !== 'object' ? { path: options } : Utils.clone(options)); // options can be reused
Utils.assert(typeof settings.path !== 'string' || settings.path[settings.path.length - 1] !== '/', 'File path cannot end with a \'/\':', route.path);

Expand Down
29 changes: 16 additions & 13 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -197,21 +197,12 @@ internals.wrap = function (result, request, finalize) {
exports.configure = function (handler, route) {

if (typeof handler === 'object') {
if (handler.proxy) {
return Proxy.handler(route, handler.proxy);
}
var type = Object.keys(handler)[0];
var serverHandler = route.server.pack._handlers[type];

if (handler.file) {
return File.handler(route, handler.file);
}
Utils.assert(serverHandler, 'Unknown handler:', type);

if (handler.directory) {
return Directory.handler(route, handler.directory);
}

if (handler.view) {
return Views.handler(route, handler.view);
}
return serverHandler(route, handler[type]);
}

if (typeof handler === 'string') {
Expand Down Expand Up @@ -400,3 +391,15 @@ exports.invoke = function (request, event, callback) {
});
};


exports.register = function (server) {
// If handlers are already registered, just return
if (Object.keys(server.pack._handlers).length > 0) {
return;
}

server.handler('proxy', Proxy.handler);
server.handler('file', File.handler);
server.handler('directory', Directory.handler);
server.handler('view', Views.handler);
};
18 changes: 18 additions & 0 deletions lib/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ exports = module.exports = internals.Pack = function (options) {
this._caches = {}; // Cache clients
this._methods = new Methods(this); // Server methods
this._protect = new Protect(this);
this._handlers = {}; // Registered handlers

this.list = {}; // Loaded plugins by name
this.plugins = {}; // Exposed plugin properties by name
Expand Down Expand Up @@ -264,6 +265,7 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen
root.plugins = self.plugins;
root.events = self.events;
root.methods = self._methods.methods;
root.handlers = self._handlers;

root.expose = function (/* key, value */) {

Expand Down Expand Up @@ -329,6 +331,13 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen
return self._methods.add.apply(self._methods, args);
};

root.handler = function (/* name, method */) {

var args = Array.prototype.slice.call(arguments);

return self._handler.apply(self, args);
};

root.cache = function (options) {

return self._provisionCache(options, 'plugin', plugin.name, options.segment);
Expand Down Expand Up @@ -675,6 +684,15 @@ internals.Pack.prototype._method = function () {
};


internals.Pack.prototype._handler = function (name, fn) {

Utils.assert(typeof name === 'string', 'Invalid handler name');
Utils.assert(!this._handlers[name], 'Handler name already exists:', name);
Utils.assert(typeof fn === 'function', 'Handler must be a function:', name);
this._handlers[name] = fn;
};


internals.Pack.prototype._invoke = function (event, callback) {

var self = this;
Expand Down
4 changes: 4 additions & 0 deletions lib/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

var Boom = require('boom');
var Nipple = require('nipple');
var Joi = require('joi');
var Defaults = require('./defaults');
var Utils = require('./utils');
var Response = require('./response');
var Schema = require('./schema');


// Declare internals
Expand All @@ -14,6 +16,8 @@ var internals = {};

exports.handler = function (route, options) {

var schemaError = Joi.validate(options, Schema.proxyHandlerSchema);
Utils.assert(!schemaError, 'Invalid handler options for proxy:', schemaError);
Utils.assert(!route.settings.payload || ((route.settings.payload.output === 'data' || route.settings.payload.output === 'stream') && !route.settings.payload.parse), 'Cannot proxy if payload is parsed or if output is not stream or data');
var settings = Utils.applyToDefaults(Defaults.proxy, options);
settings.mapUri = options.mapUri || internals.mapUri(options.protocol, options.host, options.port, options.uri);
Expand Down
95 changes: 52 additions & 43 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,49 +159,7 @@ internals.routeConfigSchema = {
handler: [
Joi.func(),
Joi.string(),
Joi.object({
directory: Joi.object({
path: Joi.alternatives(Joi.string(), Joi.array().includes(Joi.string()), Joi.func()).required(),
index: Joi.boolean(),
listing: Joi.boolean(),
showHidden: Joi.boolean(),
redirectToSlash: Joi.boolean(),
lookupCompressed: Joi.boolean(),
defaultExtension: Joi.string().alphanum()
}),
file: [
Joi.string(),
Joi.func(),
Joi.object({
path: Joi.string().required(),
filename: Joi.string().with('mode'),
mode: Joi.string().valid('attachment', 'inline').allow(false),
lookupCompressed: Joi.boolean()
})
],
proxy: Joi.object({
host: Joi.string().xor('mapUri', 'uri'),
port: Joi.number().integer().without('mapUri', 'uri'),
protocol: Joi.string().valid('http', 'https', 'http:', 'https:').without('mapUri', 'uri'),
uri: Joi.string().without('host', 'port', 'protocol', 'mapUri'),
passThrough: Joi.boolean(),
rejectUnauthorized: Joi.boolean(),
xforward: Joi.boolean(),
redirects: Joi.number().min(0).integer().allow(false),
timeout: Joi.number().integer(),
mapUri: Joi.func().without('host', 'port', 'protocol', 'uri'),
onResponse: Joi.func(),
postResponse: Joi.func(), // Backward compatibility
ttl: Joi.string().valid('upstream').allow(null)
}),
view: [
Joi.string(),
Joi.object({
template: Joi.string(),
context: Joi.object()
})
]
}).length(1)
Joi.object().length(1)
],
bind: Joi.object().allow(null),
payload: Joi.object({
Expand Down Expand Up @@ -253,6 +211,57 @@ internals.routeConfigSchema = {
};


// Built in route handler schemas

exports.directoryHandlerSchema = Joi.object({
path: Joi.alternatives(Joi.string(), Joi.array().includes(Joi.string()), Joi.func()).required(),
index: Joi.boolean(),
listing: Joi.boolean(),
showHidden: Joi.boolean(),
redirectToSlash: Joi.boolean(),
lookupCompressed: Joi.boolean(),
defaultExtension: Joi.string().alphanum()
});


exports.fileHandlerSchema = [
Joi.string(),
Joi.func(),
Joi.object({
path: Joi.string().required(),
filename: Joi.string().with('mode'),
mode: Joi.string().valid('attachment', 'inline').allow(false),
lookupCompressed: Joi.boolean()
})
];


exports.proxyHandlerSchema = Joi.object({
host: Joi.string().xor('mapUri', 'uri'),
port: Joi.number().integer().without('mapUri', 'uri'),
protocol: Joi.string().valid('http', 'https', 'http:', 'https:').without('mapUri', 'uri'),
uri: Joi.string().without('host', 'port', 'protocol', 'mapUri'),
passThrough: Joi.boolean(),
rejectUnauthorized: Joi.boolean(),
xforward: Joi.boolean(),
redirects: Joi.number().min(0).integer().allow(false),
timeout: Joi.number().integer(),
mapUri: Joi.func().without('host', 'port', 'protocol', 'uri'),
onResponse: Joi.func(),
postResponse: Joi.func(), // Backward compatibility
ttl: Joi.string().valid('upstream').allow(null)
});


exports.viewHandlerSchema = [
Joi.string(),
Joi.object({
template: Joi.string(),
context: Joi.object()
})
];


exports.view = function (options) {

var schema = internals.viewSchema({
Expand Down
8 changes: 8 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,8 @@ exports = module.exports = internals.Server = function (/* host, port, options *
this.info.uri = this.info.protocol + '://' + (this._host || Os.hostname() || 'localhost') + ':' + this.info.port;
}
}

Handler.register(this);
};

Utils.inherits(internals.Server, Events.EventEmitter);
Expand Down Expand Up @@ -468,3 +470,9 @@ internals.Server.prototype.method = function () {

return this.pack._method.apply(this.pack, arguments);
};


internals.Server.prototype.handler = function () {

return this.pack._handler.apply(this.pack, arguments);
};
5 changes: 5 additions & 0 deletions lib/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
var Fs = require('fs');
var Path = require('path');
var Boom = require('boom');
var Joi = require('joi');
var Defaults = require('./defaults');
var Schema = require('./schema');
var Utils = require('./utils');
var Response = require('./response');
var Schema = require('./schema');
// Additional engine modules required in constructor


Expand Down Expand Up @@ -358,6 +360,9 @@ internals.Manager.prototype._compile = function (template, engine, settings, cal

exports.handler = function (route, options) {

var schemaError = Joi.validate(options, Schema.viewHandlerSchema);
Utils.assert(!schemaError, 'Invalid handler options for view:', schemaError);

if (typeof options === 'string') {
options = { template: options };
}
Expand Down
Loading