diff --git a/node_modules/frontdoor/lib/params.js b/node_modules/frontdoor/lib/params.js new file mode 100644 index 00000000..bf96b508 --- /dev/null +++ b/node_modules/frontdoor/lib/params.js @@ -0,0 +1,98 @@ +define(function(require, exports, module) { + "use strict"; + + /** + * Handles parameter definitions. A full parameter definition is a name => value + * object, where each value is an object with a type, source, optional and name parameter. + */ + + var Types = require("./types").Types; + var RegExpType = require("./types").RegExp; + var BuiltinTypes = new Types(); + + var Params = { + + /** + * Batch normalize params, and creates Type Objects for each param. + */ + normalize: function(params, types, source) { + if ( ! params ) return {}; + + types = types || BuiltinTypes; + + for (var name in params) { + var param = Params.param(params[name], name, source); + + param.type = types.get(param.type); + params[name] = param; + } + + return params; + }, + + /** + * Create/normalize a parameter definition from one of the following + * api's: + * + * 1. A URL parameter defined as part of a path definition: + * - This is a single string, defaulting to a required, *string* type url param + * + * 2. A url parameter following the { key: value } convention + * - The key is the name of the path/body/query part, value is a type + * - Values that are strings must be one of the builtin types + * - Values may be a RegExp, that will be converted to RegExpType + * + * 3. Fully defined param spec with valid values for type and source. + * + * @param String|Object def A param object or type string, or name. + * @param String name (Optional) param name. + * When omitted, the first argument will be the name + * @param String source (Optional) param source. Must be url, body or query + * + * @return Object param Full param object + */ + + param: function(def, name, source) { + var param = def; + source = source || 'url'; + + // Singe edge case for implicit param generation from the url pathparts, + // where the pathpart is not defined in params definition. + if (typeof def === 'string' && !name) { + return { + name: def, + source: 'url', + optional: false, + type: BuiltinTypes.get('string'), + }; + } + + if (typeof def === 'string' || def instanceof RegExp) { + param = { + name: name, + type: def + }; + } + + param.optional = !!param.optional; + param.source = param.source || source; + + // allow regular expressions as types + if (param.type instanceof RegExp) + param.type = new RegExpType(param.type); + + if (param.source == "body") + param.type = param.type || "json"; + + param.type = param.type || "string"; + + if (!/^body|url|query$/.test(param.source)) { + throw new Error("parameter source muste be 'url', 'query' or 'body'"); + } + + return param; + } + }; + + module.exports = Params; +}); \ No newline at end of file diff --git a/node_modules/frontdoor/lib/route.js b/node_modules/frontdoor/lib/route.js index 0c699ab7..4779de82 100644 --- a/node_modules/frontdoor/lib/route.js +++ b/node_modules/frontdoor/lib/route.js @@ -1,12 +1,25 @@ define(function(require, exports, module) { "use strict"; -var RegExpType = require("./types").RegExp; -var Types = require("./types").Types; +var Params = require("./params"); var flatten = require("./utils").flatten; var error = require("http-error"); -module.exports = function Route(route, options, handler, types) { +function prepareParams( options, types, parent ){ + var params = Params.normalize(options.params, types ); + + if ( parent ){ + for ( var key in parent.params ){ + if ( params[key] ) continue; + params[key] = parent.params[key]; + } + } + + return params; +} + + +module.exports = function Route(route, options, handler, types, parent ) { // options is optional if (typeof options == "function" || Array.isArray(options)) { @@ -14,9 +27,9 @@ module.exports = function Route(route, options, handler, types) { handler = options; options = {}; } + options.route = route; - types = types || new Types(); this.middlewares = flatten(handler); @@ -28,14 +41,13 @@ module.exports = function Route(route, options, handler, types) { }); this.middlewares.unshift(decodeParams); - this.method = (options.method || "GET").toLowerCase(); var self = this; var keys = []; - var params = options.params || {}; + + var params = prepareParams( options, types, parent ); var routeRe = normalizePath(options.route, keys, params); - params = normalizeParams(params); function wrapHandler(handler) { return function(req, res, next) { @@ -53,27 +65,18 @@ module.exports = function Route(route, options, handler, types) { * the default values for url parameters. */ function normalizePath(path, keys, params) { - for (var name in params) { - var param = params[name]; - if (typeof param == "string" || param instanceof RegExp) - params[name] = { type: param}; - } - path = path .concat("/?") .replace(/\/:([\w\.\-\_]+)(\*?)/g, function(match, key, wildcard) { keys.push(key); - if (!params[key]) { - params[key] = {}; - } - // url params default to type string and optional=false + + // Implicit params: part of path definition but not defined as + // { params }, created here on the fly. + if (!params[key]) + params[key] = Params.param( key ); + var param = params[key]; - param.type = param.type || "string"; - param.optional = false; - - if (!param.source) - param.source = "url"; - + if (param.source !== "url") throw new Error("Url parameters must have 'url' as source but found '" + param.source + "'"); @@ -84,44 +87,11 @@ module.exports = function Route(route, options, handler, types) { }) .replace(/([\/.])/g, '\\$1') .replace(/\*/g, '(.*)'); + return new RegExp('^' + path + '$'); } - - function normalizeParams(params) { - for (var name in params) { - var param = params[name]; - if (param.source == "query") { - // query params default to string - param.type = param.type || "string"; - } - else if (!param.source || param.source == "body") { - // body params default to json - param.type = param.type || "json"; - param.source = "body"; - } - else if (param.source === "url") { - param.type = param.type || "string"; - } - else { - throw new Error("parameter source muste be 'url', 'query' or 'body'"); - } - - // optional defaults to false - param.optional = !!param.optional; - - // allow regular expressions as types - if (param.type instanceof RegExp) - param.type = new RegExpType(param.type); - - // convert all types to type objects - param.type = types.get(param.type); - } - - return params; - } - /** * Check if the given path matched the route regular expression. If * the regular expression doesn't match or parsing fails `match` will @@ -129,8 +99,9 @@ module.exports = function Route(route, options, handler, types) { **/ this.match = function(req, path) { var m = path.match(routeRe); + if (!m) return false; - + var match = {}; for (var i = 0; i < keys.length; i++) { var value = m[i+1]; @@ -147,6 +118,7 @@ module.exports = function Route(route, options, handler, types) { } match[key] = value; } + req.match = match; return true; }; @@ -159,6 +131,7 @@ module.exports = function Route(route, options, handler, types) { */ function decodeParams(req, res, next) { var urlParams = req.match; + if (!urlParams) return; var body = req.body || {}; @@ -173,6 +146,10 @@ module.exports = function Route(route, options, handler, types) { // 1. check if all required params are there for (var key in params) { var param = params[key]; + + if ( keys.indexOf(key) === -1 && param.source == 'url' ) + continue; + if ( (!param.optional) && ( (param.source == "body" && !(key in body)) || @@ -191,7 +168,8 @@ module.exports = function Route(route, options, handler, types) { var type = param.type; var value = EMPTY; var isValid = true; - switch(param.source) { + + switch (param.source) { case "body": if (param.optional && !(key in body)) break; diff --git a/node_modules/frontdoor/lib/section.js b/node_modules/frontdoor/lib/section.js index 1e8edbfa..ddbe9394 100644 --- a/node_modules/frontdoor/lib/section.js +++ b/node_modules/frontdoor/lib/section.js @@ -4,6 +4,7 @@ define(function(require, exports, module) { var url = require("url"); var Types = require("./types").Types; +var Params = require("./params"); var Route = require("./route"); var flatten = require("./utils").flatten; @@ -32,10 +33,11 @@ module.exports = function Section(name, description, types) { return self._route(route, options, handler); }).bind(self, method); }); + this.del = this["delete"]; this.registerType = types.register.bind(types); - + this.all = function(method, route, options, handler) { var self = this; var args = arguments; @@ -54,7 +56,7 @@ module.exports = function Section(name, description, types) { }; this._route = function(route, options, handler) { - route = new Route(route, options, handler, types); + route = new Route(route, options, handler, types, this); route.parent = this; routes[route.method].push(route); return this; @@ -160,7 +162,9 @@ module.exports = function Section(name, description, types) { if (section && section.length) { var subPath = "/" + splitPath.slice(1).join("/"); for (var i = 0; i < section.length; i++) { + var handler = section[i].match(req, subPath, method); + if (handler) return handler; } @@ -204,6 +208,21 @@ module.exports = function Section(name, description, types) { return api; }; + + var params; + + Object.defineProperty( this, 'params', { + set: function( def, types ){ + params = Params.normalize( def ); + }, + + get: function(){ + return params; + } + }); + + + }; }); \ No newline at end of file diff --git a/node_modules/frontdoor/package.json b/node_modules/frontdoor/package.json index df2ae136..51c7c555 100644 --- a/node_modules/frontdoor/package.json +++ b/node_modules/frontdoor/package.json @@ -1,37 +1,41 @@ { - "author": "Ajax.org B.V. ", - "contributors": [{ - "name": "Fabian Jakobs", - "email": "fabian@c9.io" - }], - "name": "frontdoor", - "description": "Frontdoor is a libarary for creating RESTful API servers.", - "version": "0.0.1", - "scripts": { - "test": "find lib | grep '_test.js$' | xargs -n 1 node" - }, - "licenses": [{ - "type": "MIT", - "url": "http://github.com/frontdoor/smith/raw/master/LICENSE" - }], - "repository": { - "type": "git", - "url": "git://github.com/c9/frontdoor.git" - }, - "main": "frontdoor.js", - "engines": { - "node": ">=0.6.0" - }, - "dependencies": { - "http-error": "~0.0.1", - "request": "~2.12.0", - "amd-loader": "~0.0.5" - }, - "devDependencies": { - "asyncjs": "~0.0.9", - "sinon": "~1.3.0", - - "express": "3.0.3", - "ejs": "*" + "author": "Ajax.org B.V. ", + "contributors": [ + { + "name": "Fabian Jakobs", + "email": "fabian@c9.io" } -} \ No newline at end of file + ], + "name": "frontdoor", + "description": "Frontdoor is a libarary for creating RESTful API servers.", + "version": "0.0.1", + "scripts": { + "test": "find lib | grep '_test.js$' | xargs -n 1 node" + }, + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/frontdoor/smith/raw/master/LICENSE" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/c9/frontdoor.git" + }, + "main": "frontdoor.js", + "engines": { + "node": ">=0.6.0" + }, + "dependencies": { + "http-error": "~0.0.1", + "request": "~2.12.0", + "amd-loader": "~0.0.5" + }, + "devDependencies": { + "asyncjs": "~0.0.9", + "ejs": "*", + "express": "3.0.3", + "sinon": "~1.3.0", + "tape": "^3.5.0" + } +} diff --git a/node_modules/frontdoor/test/route.js b/node_modules/frontdoor/test/route.js new file mode 100644 index 00000000..024abade --- /dev/null +++ b/node_modules/frontdoor/test/route.js @@ -0,0 +1,149 @@ +"use strict"; + +var sinon = require("sinon"); +var frontdoor = require('../frontdoor'); +var Route = frontdoor.Route; +var test = require('tape'); + +test("test router: simple route with argument", function(assert) { + var route = new Route("/user/:name", sinon.stub()); + + var req = {}; + assert.equal(route.match(req, "/juhu"), false); + assert.equal(route.match(req, "/juhu/12"), false); + + assert.equal(route.match(req, "/user/fabian"), true); + assert.equal(req.match.name, "fabian"); + + assert.end(); +}); + +test("test router: simple route with number argument", function(assert) { + var route = new Route("/user/:id", { + params: { + id: { + type: "int" + } + } + }, sinon.stub()); + + var req = {}; + assert.equal(route.match(req, "/user/fabian"), false); + assert.equal(route.match(req, "/user/123"), true); + assert.equal(req.match.id, 123); + + assert.end(); +}); + +test("test router: for params if the value is a string it is treated as the type", function(assert) { + var route = new Route("/user/:id", { + params: { + id: "int" + } + }, sinon.stub()); + + var req = {}; + assert.equal(route.match(req, "/user/123"), true); + assert.equal(req.match.id, 123); + + assert.end(); +}); + +test("test router: complex route with wildcard arguments", function(assert) { + var route = new Route("/user/:name/:rest*", { + params: { + id: { + type: "int" + }, + rest: { + type: "string" + } + } + }, sinon.stub()); + + var req = {}; + + assert.equal(route.match(req, "/user/fabian"), false); + assert.equal(route.match(req, "/user/fabian/"), true); + assert.equal(req.match.name, "fabian"); + assert.equal(req.match.rest, "/"); + assert.equal(route.match(req, "/user/fabian/abc"), true); + assert.equal(req.match.name, "fabian"); + assert.equal(req.match.rest, "/abc"); + assert.equal(route.match(req, "/user/fabian/abc/123"), true); + assert.equal(req.match.name, "fabian"); + assert.equal(req.match.rest, "/abc/123"); + + assert.end(); + +}); + +test("test router: complex route with multiple arguments", function(assert) { + var route = new Route("/user/:name/:id", { + params: { + id: { + type: "int" + } + } + }, sinon.stub()); + + var req = {}; + + assert.equal(route.match(req, "/user/fabian"), false); + assert.equal(route.match(req, "/user/123"), false); + assert.equal(route.match(req, "/user/fabian/123"), true); + assert.equal(req.match.id, 123); + assert.equal(req.match.name, "fabian"); + + assert.end(); + +}); + +test("test regexp types", function(assert) { + var route = new Route("/users/:uid", { + params: { + uid: /u\d+/ + } + }, sinon.stub()); + + var req = {}; + + assert.ok(route.match(req, "/users/u123")); + assert.ok(!route.match(req, "/users/_u123")); + + assert.end(); + +}); + +test("test custom type without register", function(assert) { + var DateType = { + parse: function(string) { + if (!/\d{13}/.test(string)) + throw new Error("not a timestamp"); + + return new Date(parseInt(string, 10)); + }, + check: function(value) { + return value instanceof Date; + } + }; + + var route = new Route("/ts/:ts", { + params: { + ts: { + type: DateType + } + } + }, sinon.stub()); + + var req = {}; + + assert.ok(route.match(req, "/ts/1353676299181")); + assert.ok(req.match.ts instanceof Date); + + assert.ok(!route.match(req, "/ts/353676299181")); + assert.ok(!route.match(req, "/ts/abc")); + + + assert.end(); +}); \ No newline at end of file diff --git a/node_modules/frontdoor/test/section.js b/node_modules/frontdoor/test/section.js new file mode 100644 index 00000000..476b3153 --- /dev/null +++ b/node_modules/frontdoor/test/section.js @@ -0,0 +1,166 @@ +var test = require('tape'); +var Section = require('../frontdoor').Section; +var Url = require('url'); + +var mock = { + req: function( method, uri) { + var parsedUrl = Url.parse(uri||'', true); + + return { + method: method || 'get', + parsedUrl: parsedUrl, + pathname: parsedUrl.pathname, + } + + } +}; + +test('Defines params on section level', function(assert, things, done) { + var testParams = { + required: { + type: 'int', + optional: false, + }, + int: { + type: 'int', + source: 'url' + }, + string: 'string', + alphanum: { + type: /[a-z0-9]+/, + source: 'url' + }, + }; + + var cases = [ + { + label: 'Match a simple string param', + path: '/test/:string', + url: '/test/foo', + params: { + string: 'foo', + } + }, + { + label: 'Match a simple number param', + path: '/test/:int', + url: '/test/123', + params: { + int: 123, + } + }, + { + label: 'Match multiple params', + path: '/test/:int/:string', + url: '/test/123/hello', + params: { + string: 'hello', + int: 123, + } + }, + { + label: 'Match multiple params 3x', + path: '/test/:string/:int/:alphanum', + url: '/test/hello/123/baz123', + params: { + string: 'hello', + int: 123, + alphanum: 'baz123' + } + }, + { + label: 'Check ordered params', + path: '/test/:string/:int/:alphanum', + url: '/test/123/hello/baz123', + err: true, + }, + { + label: 'Must match type int param', + path: '/test/:int', + url: '/test/test', + err: true, + }, + { + label: 'Must match optinal type int', + path: '/test/:int', + url: '/test', + err: true, + }, + { + label: 'Must match required type int', + path: '/test/:required', + url: '/test', + err: true, + }, + { + label: 'Match an optional param', + path: '/test/:optional', + url: '/test', + err: true, + }, + { + label: 'Match an implied url param', + path: '/test/:implied', + url: '/test/ok', + params: { + implied: 'ok', + }, + }, + { + label: 'Query params can be passed along', + path: '/test/:string/:int/:alphanum', + url: '/test/hello/123/baz123?q=123', + options: { + params: { + q: { + type: 'int', + optional: false, + source: 'query', + } + } + }, + params: { + string: 'hello', + int: 123, + alphanum: 'baz123', + q: 123 + } + }, + { + label: 'Required query params must be passed', + path: '/test/:string/:int/:alphanum', + url: '/test/hello/123/baz123', + err: true, + options: { + params: { + q: { + type: 'int', + optional: false, + source: 'query', + } + } + }, + }, + ]; + + cases.forEach(function(testCase) { + var req = mock.req('get', testCase.url), + api = new Section('test'); + + api.params = testParams; + + api.get( testCase.path, testCase.options || {}, function(req, res, next){ + assert.deepEqual( req.params, testCase.params, testCase.label ); + }); + + api.handle( req.pathname, req, {}, function(err) { + if ( testCase.err ) { + assert.pass( 'route not matched: ' + testCase.label ); + return; + } + assert.fail( 'route not matched: ' + testCase.label ); + }); + }); + + assert.end(); +}); diff --git a/package.json b/package.json index 65bd0136..48df1325 100644 --- a/package.json +++ b/package.json @@ -50,18 +50,18 @@ "licenses": [], "c9plugins": { "c9.ide.language": "#afda452919", - "c9.ide.language.css": "#eab00e0694", - "c9.ide.language.generic": "#8a3be4533a", - "c9.ide.language.html": "#13321c4eb3", - "c9.ide.language.html.diff": "#0d49022da4", + "c9.ide.language.css": "#afda1f867c", + "c9.ide.language.generic": "#87a4a44671", + "c9.ide.language.html": "#fa4833e117", + "c9.ide.language.html.diff": "#a7311cfc9f", "c9.ide.language.javascript": "#8479d0a9c1", - "c9.ide.language.javascript.immediate": "#99ed8f4560", + "c9.ide.language.javascript.immediate": "#9a2cce9121", "c9.ide.language.javascript.eslint": "#8832423ad1", "c9.ide.language.javascript.tern": "#7aab8b0b6a", - "c9.ide.language.javascript.infer": "#ded7df5136", + "c9.ide.language.javascript.infer": "#ebb2daf81a", "c9.ide.language.jsonalyzer": "#efa4426f1f", "c9.ide.collab": "#104ec85dd1", - "c9.ide.local": "#cf624506cc", + "c9.ide.local": "#2bfd7ff051", "c9.ide.find": "#ef82bc4f0d", "c9.ide.find.infiles": "#1b83cf12f1", "c9.ide.find.replace": "#e4daf722b8", @@ -70,7 +70,7 @@ "c9.ide.ace.emmet": "#e5f1a92ac3", "c9.ide.ace.gotoline": "#4d1a93172c", "c9.ide.ace.keymaps": "#6c4bb65b1f", - "c9.ide.ace.repl": "#ada99852fa", + "c9.ide.ace.repl": "#8d6534a11c", "c9.ide.ace.split": "#0ae0151c78", "c9.ide.ace.statusbar": "#d7b45bb7c3", "c9.ide.ace.stripws": "#34426a03d1", @@ -84,14 +84,14 @@ "c9.ide.imgeditor": "#08bbc53578", "c9.ide.immediate": "#6845a93705", "c9.ide.installer": "#38f5840924", - "c9.ide.mount": "#cb45b621f1", + "c9.ide.mount": "#32e79866ee", "c9.ide.navigate": "#64156c7f4a", "c9.ide.newresource": "#9a7464cc47", "c9.ide.openfiles": "#28a4f5af16", "c9.ide.preview": "#dba2f4214d", "c9.ide.preview.browser": "#ac18aaf31d", "c9.ide.preview.markdown": "#ab8d30ad9f", - "c9.ide.pubsub": "#b83cf15ade", + "c9.ide.pubsub": "#92ec19ed3a", "c9.ide.readonly": "#f6f07bbe42", "c9.ide.recentfiles": "#7c099abf40", "c9.ide.remote": "#cd45e81d2f",