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 6 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
17 changes: 4 additions & 13 deletions lib/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ var Utils = require('./utils');
var Response = require('./response');
var Ext = require('./ext');
var File = require('./file');
var Directory = require('./directory');
var Proxy = require('./proxy');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leave this here along with the server handler registration. Instead of the server constructor having the logic of adding the built-ins, create a function here and have the server call it. Keeps the inter dependencies cleaner.

var Views = require('./views');
var Methods = require('./methods');
Expand Down Expand Up @@ -197,21 +196,13 @@ internals.wrap = function (result, request, finalize) {
exports.configure = function (handler, route) {

if (typeof handler === 'object') {
if (handler.proxy) {
return Proxy.handler(route, handler.proxy);
}

if (handler.file) {
return File.handler(route, handler.file);
}
var type = Object.keys(handler)[0];
var func = type && route.server.handlers[type];

if (handler.directory) {
return Directory.handler(route, handler.directory);
}
Utils.assert(func, 'Unknown handler:', type);

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

if (typeof handler === 'string') {
Expand Down
4 changes: 4 additions & 0 deletions lib/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ internals.Pack.prototype._register = function (plugin, options, callback, _depen
ext: function () {

self._applySync(selection.servers, Server.prototype._ext, [arguments[0], arguments[1], arguments[2], env]);
},
handler: function (name, method, schema) {

self._applySync(selection.servers, Server.prototype.handler, [name, method, schema]);
}
};

Expand Down
3 changes: 3 additions & 0 deletions lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ exports = module.exports = internals.Route = function (options, server, env) {
schemaError = Schema.routeConfig(this.settings);
Utils.assert(!schemaError, 'Invalid route config for', options.path, ':', schemaError);

schemaError = Schema.routeHandler(this.settings.handler, server.handlerSchema);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Schema validation should move into each handler generation function. This also removes the need to pass the schema when creating custom handlers. Instead, the handler generation method can just do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would that work? The handler generation method has access to the handler function and the schema, but not what the user has actually specified. For example:

server.handler('name', fn, schema);

The validation can't actually happen until:

server.route({handler: {name: {}}});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

server.handler(name, method) only registers method under name. Handler.configure() is where the method is called with the actual route config and can be validated.

Utils.assert(!schemaError, 'Invalid route handler for', options.path, ':', schemaError);

this.server = server;
this.env = env || {}; // Plugin-specific environment
this.method = options.method.toLowerCase();
Expand Down
116 changes: 68 additions & 48 deletions lib/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ internals.routeOptionsSchema = {
method: Joi.alternatives(Joi.string(), Joi.array().includes(Joi.string()).min(1)).required(),
path: Joi.string().required(),
vhost: [Joi.string(), Joi.array()],
handler: Joi.any(), // Validated in route.config
handler: Joi.any(), // Validated in routeHandler()
config: Joi.object().allow(null)
};

Expand Down Expand Up @@ -156,53 +156,7 @@ internals.pre = [

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: 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)
],
handler: Joi.any(), // Validated in routeHandler()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not right. It still can be one of the provided types here, including object with one key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation still exists, it's just getting put off until later. I got the idea from https://github.com/spumko/hapi/blob/master/lib/schema.js#L131

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It shouldn't. Validate the basic requirements first, then the handler-specifics later.

bind: Joi.object().allow(null),
payload: Joi.object({
output: Joi.string().valid('data', 'stream', 'file'),
Expand Down Expand Up @@ -253,6 +207,72 @@ internals.routeConfigSchema = {
};


// Validate route handler

exports.routeHandler = function (config, handlerSchema) {

var schema = Utils.clone(internals.routeHandlerSchema);
var error;

schema.push(Joi.object(handlerSchema).length(1));
error = Joi.validate(config, schema);

return (error ? error.annotated() : null);
};


// Not a joi object. Route handler schema is compiled based on registered handler types
exports.routeHandlerObjectSchema = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Break this into a schema per handler type and call those from each handler.

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()
})
]
};


internals.routeHandlerSchema = [
Joi.func(),
Joi.string()
];


exports.view = function (options) {

var schema = internals.viewSchema({
Expand Down
27 changes: 27 additions & 0 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@ var Defaults = require('./defaults');
var Request = require('./request');
var Router = require('./router');
var Schema = require('./schema');
var File = require('./file');
var Proxy = require('./proxy');
var Directory = require('./directory');
var Views = require('./views');
var Ext = require('./ext');
var Handler = require('./handler');
var Utils = require('./utils');
var Joi = require('joi');
// Pack delayed required inline


Expand Down Expand Up @@ -218,6 +222,19 @@ exports = module.exports = internals.Server = function (/* host, port, options *
this.info.uri = this.info.protocol + '://' + (this._host || Os.hostname() || 'localhost') + ':' + this.info.port;
}
}

this.handlers = {
proxy: Proxy.handler,
file: File.handler,
directory: Directory.handler,
view: Views.handler
};
this.handlerSchema = {
proxy: Schema.routeHandlerObjectSchema.proxy,
file: Schema.routeHandlerObjectSchema.file,
directory: Schema.routeHandlerObjectSchema.directory,
view: Schema.routeHandlerObjectSchema.view
};
};

Utils.inherits(internals.Server, Events.EventEmitter);
Expand Down Expand Up @@ -469,3 +486,13 @@ internals.Server.prototype.method = function () {

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


internals.Server.prototype.handler = function (name, method, schema) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to follow the same pattern as server methods which are actually managed by the pack. This way, when you have multiple servers, they should all share the same handlers.


Utils.assert(typeof name === 'string', 'Invalid handler name');
Utils.assert(typeof method === 'function', 'Handler must be a function:', name);
schema = schema || Joi.any();
this.handlers[name] = method;
this.handlerSchema[name] = schema;
};
39 changes: 39 additions & 0 deletions test/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -821,4 +821,43 @@ describe('Pack', function () {
done();
});
});

describe('#handler', function () {

it('add new handler', function (done) {

var server = new Hapi.Server();
var plugin = {
name: 'foo',
version: '0.0.1',
register: function (plugin, options, next) {

plugin.handler('bar', function (route, options) {

return function (request, reply) {

reply('success');
};
});
next();
}
};

server.pack.register(plugin, function (err) {

expect(err).to.not.exist;
server.route({
method: 'GET',
path: '/',
handler: {
bar: {}
}
});
server.inject('/', function (res) {
expect(res.payload).to.equal('success');
done();
});
});
});
});
});
11 changes: 11 additions & 0 deletions test/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,15 @@ describe('Schema', function () {
done();
});
});

describe('#routeHandler', function () {

it('fails when route has duplicate handler', function (done) {

var handler = { file: 'something', view: 'something' };

expect(Schema.routeHandler(handler)).to.exist;
done();
});
});
});
Loading