From 7d22173c5ef902558d90e46768c20c197e9ca580 Mon Sep 17 00:00:00 2001 From: Rajat Kumar Date: Thu, 21 Feb 2019 16:11:44 -0800 Subject: [PATCH 1/2] 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..76429ec14 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() +let 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(); +}); From 60afec9dfa8cced1394ccd9592dc4fb15a35bb61 Mon Sep 17 00:00:00 2001 From: Rajat Kumar Date: Thu, 21 Feb 2019 16:17:48 -0800 Subject: [PATCH 2/2] fix: linting blocked usage of `let` --- test/router.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/router.test.js b/test/router.test.js index 76429ec14..c1a8132e8 100644 --- a/test/router.test.js +++ b/test/router.test.js @@ -373,7 +373,7 @@ test('toString() with ignoreTrailingSlash', function(t) { }); // Tests router.render() -let mockResponse = function respond(req, res, next) { +var mockResponse = function respond(req, res, next) { res.send(200); };