Skip to content

Commit

Permalink
Support server.ext(events). Closes #2827
Browse files Browse the repository at this point in the history
  • Loading branch information
hueniverse committed Oct 5, 2015
1 parent b37dc3e commit 6f8374f
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 54 deletions.
91 changes: 61 additions & 30 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
- [`server.dependency(dependencies, [after])`](#serverdependencydependencies-after)
- [`server.expose(key, value)`](#serverexposekey-value)
- [`server.expose(obj)`](#serverexposeobj)
- [`server.ext(events)`](#serverextevents)
- [`server.ext(event, method, [options])`](#serverextevent-method-options)
- [`server.handler(name, method)`](#serverhandlername-method)
- [`server.initialize(callback)`](#serverinitializecallback)
Expand Down Expand Up @@ -973,38 +974,68 @@ exports.register = function (server, options, next) {
};
```

### `server.ext(event, method, [options])`
### `server.ext(events)`

Registers an extension function in one of the available extension points where:
- `event` - the event name.
- `method` - a function or an array of functions to be executed at a specified point during request
processing. The required extension function signature is:
- server extension points: `function(server, next)` where:
- `server` - the server object.
- `next` - the continuation method with signature `function(err)`.
- `this` - the object provided via `options.bind` or the current active context set with
[`server.bind()`](#serverbindcontext).
- request extension points: `function(request, reply)` where:
- `request` - the [request object](#request-object).
- `reply` - the [reply interface](#reply-interface) which is used to return control back to the
framework. To continue normal execution of the [request lifecycle](#request-lifecycle),
`reply.continue()` must be called. To abort processing and return a response to the client,
call `reply(value)` where value is an error or any other valid response.
- `this` - the object provided via `options.bind` or the current active context set with
[`server.bind()`](#serverbindcontext).
- `options` - an optional object with the following:
- `before` - a string or array of strings of plugin names this method must execute before (on
the same event). Otherwise, extension methods are executed in the order added.
- `after` - a string or array of strings of plugin names this method must execute after (on the
same event). Otherwise, extension methods are executed in the order added.
- `bind` - a context object passed back to the provided method (via `this`) when called.

The available extension points include the [request extension points](#request-lifecycle) as well
as the following server extension points:
- `'onPreStart'` - called before the connection listeners are started.
- `'onPostStart'` - called after the connection listeners are started.
- `'onPreStop'` - called before the connection listeners are stopped.
- `'onPostStop'` - called after the connection listeners are stopped.
- `events` - an object or array of objects with the following:
- `type` - the extension point event name. The available extension points include the
[request extension points](#request-lifecycle) as well as the following server extension points:
- `'onPreStart'` - called before the connection listeners are started.
- `'onPostStart'` - called after the connection listeners are started.
- `'onPreStop'` - called before the connection listeners are stopped.
- `'onPostStop'` - called after the connection listeners are stopped.
- `method` - a function or an array of functions to be executed at a specified point during request
processing. The required extension function signature is:
- server extension points: `function(server, next)` where:
- `server` - the server object.
- `next` - the continuation method with signature `function(err)`.
- `this` - the object provided via `options.bind` or the current active context set with
[`server.bind()`](#serverbindcontext).
- request extension points: `function(request, reply)` where:
- `request` - the [request object](#request-object).
- `reply` - the [reply interface](#reply-interface) which is used to return control back to the
framework. To continue normal execution of the [request lifecycle](#request-lifecycle),
`reply.continue()` must be called. To abort processing and return a response to the client,
call `reply(value)` where value is an error or any other valid response.
- `this` - the object provided via `options.bind` or the current active context set with
[`server.bind()`](#serverbindcontext).
- `options` - an optional object with the following:
- `before` - a string or array of strings of plugin names this method must execute before (on
the same event). Otherwise, extension methods are executed in the order added.
- `after` - a string or array of strings of plugin names this method must execute after (on the
same event). Otherwise, extension methods are executed in the order added.
- `bind` - a context object passed back to the provided method (via `this`) when called.

```js
var Hapi = require('hapi');
var server = new Hapi.Server();
server.connection({ port: 80 });

server.ext({
type: 'onRequest',
method: function (request, reply) {

// Change all requests to '/test'
request.setUrl('/test');
return reply.continue();
}
});

var handler = function (request, reply) {

return reply({ status: 'ok' });
};

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

// All requests will get routed to '/test'
```

### `server.ext(event, method, [options])`

Registers a single extension event using the same properties as used in
[`server.ext(events)`](#serverextevents), but passed as arguments.

```js
var Hapi = require('hapi');
Expand Down
47 changes: 33 additions & 14 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ exports = module.exports = internals.Plugin = function (server, connections, env
this.version = Package.version;

this.realm = typeof env !== 'string' ? env : {
_extensions: {
onPreAuth: null,
onPostAuth: null,
onPreHandler: null,
onPostHandler: null,
onPreResponse: null
},
modifiers: {
route: {}
},
Expand Down Expand Up @@ -418,11 +425,24 @@ internals.Plugin.prototype.expose = function (key, value) {
};


internals.Plugin.prototype.ext = function (event, func, options) {
internals.Plugin.prototype.ext = function (events) { // (event, method, options) -OR- (events)

var self = this;
if (typeof events === 'string') {
events = { type: arguments[0], method: arguments[1], options: arguments[2] };
}

events = [].concat(events);
for (var i = 0, il = events.length; i < il; ++i) {
this._ext(events[i]);
}
};

options = options || {};

internals.Plugin.prototype._ext = function (event) {

var type = event.type;
var methods = [].concat(event.method);
var options = event.options || {};

var settings = {
before: options.before,
Expand All @@ -431,30 +451,29 @@ internals.Plugin.prototype.ext = function (event, func, options) {
};

var nodes = [];
([].concat(func)).forEach(function (fn, i) {

for (var i = 0, il = methods.length; i < il; ++i) {
var node = {
func: fn, // Connection: function (request, next), Server: function (server, next)
realm: self.realm,
func: methods[i], // Connection: function (request, next), Server: function (server, next)
realm: this.realm,
bind: options.bind,
plugin: self
plugin: this
};

nodes.push(node);
});
}

// Connection extensions

if (this.root._extensions[event] === undefined) {
return this._apply('ext', Connection.prototype._ext, [event, nodes, settings]);
if (this.root._extensions[type] === undefined) {
return this._apply('ext', Connection.prototype._ext, [type, nodes, settings]);
}

// Server extensions

Hoek.assert(event !== 'onPreStart' || this.root._state === 'stopped', 'Cannot add onPreStart (after) extension after the server was initialized');
Hoek.assert(type !== 'onPreStart' || this.root._state === 'stopped', 'Cannot add onPreStart (after) extension after the server was initialized');

this.root._extensions[event] = this.root._extensions[event] || new Topo();
this.root._extensions[event].add(nodes, settings);
this.root._extensions[type] = this.root._extensions[type] || new Topo();
this.root._extensions[type].add(nodes, settings);
};


Expand Down
8 changes: 4 additions & 4 deletions lib/protect.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,6 @@ internals.Protect.prototype.run = function (next, enter) { // enter

var self = this;

if (!this.domain) {
return enter(finish);
}

var finish = function (arg0, arg1, arg2) {

self._error = null;
Expand All @@ -60,6 +56,10 @@ internals.Protect.prototype.run = function (next, enter) { // enter

finish = Hoek.once(finish);

if (!this.domain) {
return enter(finish);
}

this._error = function (err) {

return finish(Boom.badImplementation('Uncaught error', err));
Expand Down
26 changes: 20 additions & 6 deletions lib/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ exports = module.exports = internals.Route = function (options, connection, real
this.server = connection.server;
this.path = options.path;
this.method = method;
this.realm = realm;

this.public = {
method: this.method,
path: this.path,
vhost: this.vhost,
realm: realm,
realm: this.realm,
settings: this.settings
};

Expand Down Expand Up @@ -241,18 +242,31 @@ exports = module.exports = internals.Route = function (options, connection, real
// Route lifecycle

this._extensions = {
onPreAuth: Hoek.clone(this.connection._extensions.onPreAuth),
onPostAuth: Hoek.clone(this.connection._extensions.onPostAuth),
onPreHandler: Hoek.clone(this.connection._extensions.onPreHandler),
onPostHandler: Hoek.clone(this.connection._extensions.onPostHandler),
onPreResponse: Hoek.clone(this.connection._extensions.onPreResponse)
onPreAuth: this._combineExtensions('onPreAuth'),
onPostAuth: this._combineExtensions('onPostAuth'),
onPreHandler: this._combineExtensions('onPreHandler'),
onPostHandler: this._combineExtensions('onPostHandler'),
onPreResponse: this._combineExtensions('onPreResponse')
};

this._cycle = null;
this.rebuild();
};


internals.Route.prototype._combineExtensions = function (event) {

var sources = [
this.connection._extensions[event],
this.realm._extensions[event]
];

var ext = new Topo();
ext.merge(sources);
return (ext.nodes.length ? ext : null);
};


internals.compileRule = function (rule) {

// null, undefined, true - anything allowed
Expand Down
55 changes: 55 additions & 0 deletions test/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -3062,6 +3062,61 @@ describe('Plugin', function () {
});
});
});

it('extends server actions (single call)', function (done) {

var server = new Hapi.Server();
server.connection();

var result = '';
server.ext([
{
type: 'onPreStart',
method: function (srv, next) {

result += '1';
return next();
}
},
{
type: 'onPostStart',
method: function (srv, next) {

result += '2';
return next();
}
},
{
type: 'onPreStop',
method: function (srv, next) {

result += '3';
return next();
}
},
{
type: 'onPreStop',
method: function (srv, next) {

result += '4';
return next();
}
}
]);

server.start(function (err) {

expect(err).to.not.exist();
expect(result).to.equal('12');

server.stop(function (err) {

expect(err).to.not.exist();
expect(result).to.equal('1234');
done();
});
});
});
});

describe('handler()', function () {
Expand Down

0 comments on commit 6f8374f

Please sign in to comment.