UNPKG

6.53 kBJavaScriptView Raw
1/*
2Routing Primitives for Kettle Servers
3
4Copyright 2015 Raising the Floor (International)
5
6Licensed under the New BSD license. You may not use this file except in
7compliance with this License.
8
9You may obtain a copy of the License at
10https://github.com/fluid-project/kettle/blob/master/LICENSE.txt
11*/
12
13/* Contains code adapted from express 4.x "layer.js":
14 * Copyright(c) 2009-2013 TJ Holowaychuk
15 * Copyright(c) 2013 Roman Shtylman
16 * Copyright(c) 2014-2015 Douglas Christopher Wilson
17 * MIT Licensed
18 */
19
20"use strict";
21
22var fluid = require("infusion"),
23 urlModule = require("url"),
24 kettle = fluid.registerNamespace("kettle");
25
26// Upstream dependency stolen from express 4.x
27// Note that path-to-regexp 2.0.0 breaks compatibility with our use of /* to encode middleware matches - seems unlikely we will upgrade
28kettle.pathToRegexp = require("path-to-regexp");
29
30fluid.defaults("kettle.router.http", {
31 gradeNames: "fluid.component",
32 members: {
33 handlers: []
34 },
35 invokers: {
36 register: {
37 funcName: "kettle.router.http.register",
38 args: ["{that}", "{arguments}.0"]
39 },
40 match: {
41 funcName: "kettle.router.http.match",
42 args: ["{that}.handlers", "{arguments}.0"]
43 }
44 }
45});
46
47/** A structure specifying a route and the request grades which will handle it.
48 * See docs in "%kettle/docs/RequestHandlersAndApps.md" for more information.
49 * @typedef {Object} handlerRecord
50 * @member {String} type - The name of a request handling grade, which must be descended from `kettle.request`. If the
51 * `method` field is filled in, the grade must be descended from `kettle.request.http`.
52 * @member {String} [route] - A routing specification in the traditional format for express routes, e.g. of the form
53 * "/preferences/:gpiiKey". A special form "/*" is supported indicating that the router handles all routes
54 * @member {String} [method] - An HTTP method specification, possibly including multiple comma-separated values
55 * @member {String} prefix - A routing prefix to be prepended to this handler's `route`. The prefix plus the route
56 * expression must match the incoming request in order for this handler to be activated
57 * @member {String[]} gradeNames - One or more grade names which will be mixed in to the constructed handler when it is constructed.
58 */
59
60/** A "partially cooked" version of a `handlerRecord` as stored in various routing structures
61 * @typedef {handlerRecord} internalHandlerRecord
62 * @member {String} [method] - A single HTTP method specification
63 * @member {kettle.app} app - The Kettle app for which this handler record is registered
64 */
65
66/** A structure holding details of a matched route
67 * @typedef routeMatch
68 * @member {internalHandlerRecord} handler - The (elaborated version of the) original handler structure which led to the match
69 * @member {Object} output - A free-form structure which will be merged into the resulting request. This will
70 * contain at least:
71 * @member {Object} output.params - A decoded hash of keys to values extracted from the incoming request by route variables
72 * such as ":gpiiKey"
73 */
74
75/** Registers a new route handler with this router. Note that the router is not dynamic and routes can currently not be removed.
76 * Note that this is an internal method which corrupts its 2nd argument which must have been copied beforehand.
77 * @param {kettle.router.http} that - The router in which the handler should be registered
78 * @param {interalHandlerRecord} handler - A route handler structure
79 */
80kettle.router.http.register = function (that, handler) {
81 var prefix = handler.prefix || "";
82 handler.regexp = kettle.pathToRegexp(prefix + handler.route, handler.keys = []);
83 that.handlers.push(handler);
84};
85
86kettle.router.registerOneHandlerImpl = function (that, handler, extend) {
87 var handlerCopy = fluid.extend({
88 method: "get"
89 }, handler, extend);
90 kettle.router.http.register(that, handlerCopy);
91};
92
93/** Decodes a routing parameter which has been found to be a URL component matching the routing specification. If it is
94 * a string, it will be URI decoded - if this decoding fails, an exception will be thrown.
95 * @param {Any} val - The URL component to be decoded
96 * @return {Any} Either the original argument if it was not a String or was an empty string, or the argument after
97 * successful URI decoding.
98 */
99kettle.router.http.decodeParam = function (val) {
100 if (typeof val !== "string" || val.length === 0) {
101 return val;
102 }
103 try {
104 return decodeURIComponent(val);
105 } catch (err) {
106 err.message = "Failed to decode request routing parameter \"" + val + "\"";
107 err.status = err.statusCode = 400;
108 throw err;
109 }
110};
111
112/** Extract the matched routing variables into a hash of names to values
113 * @param {routeHandler} handler - The routeHandler which has been determined to match this request
114 * @param {String[]} match - The output of regexp.exec as applied to the incoming request URL
115 * @return {Object} A map of strings to strings of matched and decoded parameters
116 */
117kettle.router.http.matchToParams = function (handler, match) {
118 var params = {};
119 for (var i = 1; i < match.length; i++) {
120 var key = handler.keys[i - 1];
121 var prop = key.name;
122 var val = kettle.router.http.decodeParam(match[i]);
123
124 if (val !== undefined) {
125 params[prop] = val;
126 }
127 }
128 return params;
129};
130
131// cf. Router.prototype.matchRequest
132/** Evaluates the URL and method of an incoming HTTP for a match in the table of route handlers.
133 * @param {handlerRecord[]} handlers - An array of handlers in which a matching route is to be looked up
134 * @param {http.IncomingMessage} req - Node's native HTTP request object
135 * @return {routeMatch|Undefined} A matched route structure, or undefined if no route matched the incoming request
136 */
137kettle.router.http.match = function (handlers, req) {
138 var method = req.method.toLowerCase(),
139 parsedUrl = urlModule.parse(req.url),
140 path = parsedUrl.pathname;
141 for (var i = 0; i < handlers.length; ++i) {
142 var handler = handlers[i];
143 if (method === handler.method) {
144 var match = handler.regexp.exec(path);
145 if (match) {
146 return {
147 handler: handler,
148 output: {
149 params: kettle.router.http.matchToParams(handler, match)
150 }
151 };
152 }
153 }
154 }
155};