From 0700cfd445e45401c36c4229e37e12b8220339d9 Mon Sep 17 00:00:00 2001 From: Rajat Kumar Date: Mon, 4 Mar 2019 10:13:07 -0800 Subject: [PATCH] feat: add router.render() back to support hypermedia usecase (#1752) * feat: add router.render() to support hypermedia link generation usecase, fixes #1684 --- lib/router.js | 49 +++++++++++++++ test/router.test.js | 141 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/lib/router.js b/lib/router.js index 10c4a2dd9..68550a64b 100644 --- a/lib/router.js +++ b/lib/router.js @@ -114,6 +114,55 @@ Router.prototype.lookupByName = function lookupByName(name, req, res) { return route.chain.run.bind(route.chain); }; +/** + * Takes an object of route params and query params, and 'renders' a URL. + * + * @public + * @function render + * @param {String} routeName - the route name + * @param {Object} params - an object of route params + * @param {Object} query - an object of query params + * @returns {String} URL + * @example + * server.get({ + * name: 'cities', + * path: '/countries/:name/states/:state/cities' + * }, (req, res, next) => ...)); + * let cities = server.router.render('cities', { + * name: 'Australia', + * state: 'New South Wales' + * }); + * // cities: '/countries/Australia/states/New%20South%20Wales/cities' + */ +Router.prototype.render = function render(routeName, params, query) { + var self = this; + + function pathItem(match, key) { + if (params.hasOwnProperty(key) === false) { + throw new Error( + 'Route <' + routeName + '> is missing parameter <' + key + '>' + ); + } + return '/' + encodeURIComponent(params[key]); + } + + function queryItem(key) { + return encodeURIComponent(key) + '=' + encodeURIComponent(query[key]); + } + + var route = self._registry.get()[routeName]; + + if (!route) { + return null; + } + + var _path = route.spec.path; + var _url = _path.replace(/\/:([A-Za-z0-9_]+)(\([^\\]+?\))?/g, pathItem); + var items = Object.keys(query || {}).map(queryItem); + var queryString = items.length > 0 ? '?' + items.join('&') : ''; + return _url + queryString; +}; + /** * Adds a route. * diff --git a/test/router.test.js b/test/router.test.js index 002f60017..c1a8132e8 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -371,3 +371,144 @@ test('toString() with ignoreTrailingSlash', function(t) { ); t.end(); }); + +// Tests router.render() +var mockResponse = function respond(req, res, next) { + res.send(200); +}; + +test('render route', function(t) { + var server = restify.createServer(); + server.get({ name: 'countries', path: '/countries' }, mockResponse); + server.get({ name: 'country', path: '/countries/:name' }, mockResponse); + server.get( + { name: 'cities', path: '/countries/:name/states/:state/cities' }, + mockResponse + ); + + var countries = server.router.render('countries', {}); + t.equal(countries, '/countries'); + + var country = server.router.render('country', { name: 'Australia' }); + t.equal(country, '/countries/Australia'); + + var cities = server.router.render('cities', { + name: 'Australia', + state: 'New South Wales' + }); + t.equal(cities, '/countries/Australia/states/New%20South%20Wales/cities'); + + t.end(); +}); + +test('render route (missing params)', function(t) { + var server = restify.createServer(); + server.get( + { name: 'cities', path: '/countries/:name/states/:state/cities' }, + mockResponse + ); + + try { + server.router.render('cities', { name: 'Australia' }); + } catch (ex) { + // server is expected to throw an error + // hence catching it here + t.equal(ex, 'Error: Route is missing parameter '); + } + + t.end(); +}); + +test('GH #704: render route (special charaters)', function(t) { + var server = restify.createServer(); + server.get({ name: 'my-route', path: '/countries/:name' }, mockResponse); + + var link = server.router.render('my-route', { name: 'AustraliaIsC@@!' }); + // special charaacters are URI encoded + t.equal(link, '/countries/AustraliaIsC%40%40!'); + + t.end(); +}); + +test('GH #704: render route (with sub-regex param)', function(t) { + var server = restify.createServer(); + server.get( + { + name: 'my-route', + path: '/countries/:code([A-Z]{2,3})' + }, + mockResponse + ); + + var link = server.router.render('my-route', { code: 'FR' }); + t.equal(link, '/countries/FR'); + + link = server.router.render('my-route', { code: '111' }); + t.equal(link, '/countries/111'); + + t.end(); +}); + +test('GH-796: render route (with multiple sub-regex param)', function(t) { + var server = restify.createServer(); + server.get( + { + name: 'my-route', + path: '/countries/:code([A-Z]{2,3})/:area([0-9]+)' + }, + mockResponse + ); + + var link = server.router.render('my-route', { code: '111', area: 42 }); + t.equal(link, '/countries/111/42'); + t.end(); +}); + +test('render route (with encode)', function(t) { + var server = restify.createServer(); + server.get({ name: 'my-route', path: '/countries/:name' }, mockResponse); + + var link = server.router.render('my-route', { name: 'Trinidad & Tobago' }); + t.equal(link, '/countries/Trinidad%20%26%20Tobago'); + + t.end(); +}); + +test('render route (query string)', function(t) { + var server = restify.createServer(); + server.get({ name: 'country', path: '/countries/:name' }, mockResponse); + + var country1 = server.router.render( + 'country', + { + name: 'Australia' + }, + { + state: 'New South Wales', + 'cities/towns': 5 + } + ); + + t.equal( + country1, + '/countries/Australia?state=New%20South%20Wales&cities%2Ftowns=5' + ); + + var country2 = server.router.render( + 'country', + { + name: 'Australia' + }, + { + state: 'NSW & VIC', + 'cities&towns': 5 + } + ); + + t.equal( + country2, + '/countries/Australia?state=NSW%20%26%20VIC&cities%26towns=5' + ); + + t.end(); +});