From 77eb93a6c1c0aec1f517c8d02076dd1c7db9480e Mon Sep 17 00:00:00 2001 From: kenjones Date: Fri, 8 Jan 2016 17:28:21 -0500 Subject: [PATCH] feat(template): Support any named path parameter Handle path parameters other than `{id}` and `{parentId}` such that individual API developers can choose to name parameters as they see fit. Currently the paths only support 2 levels, after the 2nd path parameter the generator will fall back to the default handlers. closes #8 --- handlers/templates/_handler.js | 29 +-- handlers/templates/deleteResource.ejs | 2 +- handlers/templates/deleteSubResource.ejs | 4 +- handlers/templates/getResource.ejs | 2 +- handlers/templates/getSubResource.ejs | 4 +- handlers/templates/getSubResources.ejs | 2 +- .../{postResource.ejs => postResources.ejs} | 0 ...stSubResource.ejs => postSubResources.ejs} | 2 +- handlers/templates/putResource.ejs | 2 +- handlers/templates/putSubResource.ejs | 4 +- lib/specutil.js | 91 ++++---- test/test_libspecutil.js | 196 +++++++++--------- 12 files changed, 161 insertions(+), 177 deletions(-) rename handlers/templates/{postResource.ejs => postResources.ejs} (100%) rename handlers/templates/{postSubResource.ejs => postSubResources.ejs} (86%) diff --git a/handlers/templates/_handler.js b/handlers/templates/_handler.js index 0034aa1..3a20511 100644 --- a/handlers/templates/_handler.js +++ b/handlers/templates/_handler.js @@ -7,7 +7,8 @@ var <%=dbmodel.name%> = require('<%=dbmodel.path%>');<%})%> * Operations on <%=path%> */ module.exports = { -<% _.forEach(methods, function (method, i) {%> +<% var metadata = helpers.getPathMetadata(path); + _.forEach(methods, function (method, i) {%> /** * <%=method.description%> * @@ -23,13 +24,13 @@ module.exports = { var includeOpts = { Model: dbmodels[0].name, method: method, - formatSuccessResponse: helpers.formatSuccessResponse + formatSuccessResponse: helpers.formatSuccessResponse, + subModelAttribute: metadata.subResource, + id: metadata.id, + parentId: metadata.parentId }; - var includeSubOpts = _.defaults({ - subModelAttribute: helpers.getSubResourceAttribute(path) - }, includeOpts); - switch (helpers.getPathType(path, method.method)) { + switch (method.method + metadata.type) { case 'getResources':%> <%- include('getResources', includeOpts); %> <%break; @@ -37,28 +38,28 @@ module.exports = { <%- include('getResource', includeOpts); %> <%break; case 'getSubResources':%> - <%- include('getSubResources', includeSubOpts); %> + <%- include('getSubResources', includeOpts); %> <%break; case 'getSubResource':%> - <%- include('getSubResource', includeSubOpts); %> + <%- include('getSubResource', includeOpts); %> <%break; case 'putResource':%> <%- include('putResource', includeOpts); %> <%break; case 'putSubResource':%> - <%- include('putSubResource', includeSubOpts); %> + <%- include('putSubResource', includeOpts); %> <%break; case 'deleteResource':%> <%- include('deleteResource', includeOpts); %> <%break; case 'deleteSubResource':%> - <%- include('deleteSubResource', includeSubOpts); %> + <%- include('deleteSubResource', includeOpts); %> <%break; - case 'postResource':%> - <%- include('postResource', includeOpts); %> + case 'postResources':%> + <%- include('postResources', includeOpts); %> <%break; - case 'postSubResource':%> - <%- include('postSubResource', includeSubOpts); %> + case 'postSubResources':%> + <%- include('postSubResources', includeOpts); %> <%break; default:%> <%- include('default'); %> diff --git a/handlers/templates/deleteResource.ejs b/handlers/templates/deleteResource.ejs index 2b89986..91661ac 100644 --- a/handlers/templates/deleteResource.ejs +++ b/handlers/templates/deleteResource.ejs @@ -1,4 +1,4 @@ - <%=Model%>.findByIdAndRemove(req.params.id).exec().then(function (item) { + <%=Model%>.findByIdAndRemove(req.params.<%=id%>).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); diff --git a/handlers/templates/deleteSubResource.ejs b/handlers/templates/deleteSubResource.ejs index c537a28..d237c07 100644 --- a/handlers/templates/deleteSubResource.ejs +++ b/handlers/templates/deleteSubResource.ejs @@ -1,9 +1,9 @@ - <%=Model%>.findById(req.params.parentId).exec().then(function (item) { + <%=Model%>.findById(req.params.<%=parentId%>).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); } - item.<%=subModelAttribute%>.id(req.params.id).remove(); + item.<%=subModelAttribute%>.id(req.params.<%=id%>).remove(); item.save(function (saveErr) { return next(saveErr); }); diff --git a/handlers/templates/getResource.ejs b/handlers/templates/getResource.ejs index 67932e4..355f17d 100644 --- a/handlers/templates/getResource.ejs +++ b/handlers/templates/getResource.ejs @@ -1,5 +1,5 @@ var fields = !req.query.fields ? null : req.query.fields.replace(',', ' '); - <%=Model%>.findById(req.params.id, fields).exec().then(function (item) { + <%=Model%>.findById(req.params.<%=id%>, fields).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); diff --git a/handlers/templates/getSubResource.ejs b/handlers/templates/getSubResource.ejs index ef74ce6..cd46ef1 100644 --- a/handlers/templates/getSubResource.ejs +++ b/handlers/templates/getSubResource.ejs @@ -1,10 +1,10 @@ - <%=Model%>.findById(req.params.parentId).exec().then(function (item) { + <%=Model%>.findById(req.params.<%=parentId%>).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); } - var subDoc = item.<%=subModelAttribute%>.id(req.params.id); + var subDoc = item.<%=subModelAttribute%>.id(req.params.<%=id%>); if (!subDoc) { res.sendStatus(404); return next(); diff --git a/handlers/templates/getSubResources.ejs b/handlers/templates/getSubResources.ejs index 9f44986..ce837f2 100644 --- a/handlers/templates/getSubResources.ejs +++ b/handlers/templates/getSubResources.ejs @@ -1,4 +1,4 @@ - <%=Model%>.findById(req.params.id).exec().then(function (item) { + <%=Model%>.findById(req.params.<%=id%>).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); diff --git a/handlers/templates/postResource.ejs b/handlers/templates/postResources.ejs similarity index 100% rename from handlers/templates/postResource.ejs rename to handlers/templates/postResources.ejs diff --git a/handlers/templates/postSubResource.ejs b/handlers/templates/postSubResources.ejs similarity index 86% rename from handlers/templates/postSubResource.ejs rename to handlers/templates/postSubResources.ejs index 5a6de10..569a3d9 100644 --- a/handlers/templates/postSubResource.ejs +++ b/handlers/templates/postSubResources.ejs @@ -1,4 +1,4 @@ - <%=Model%>.findById(req.params.parentId).exec().then(function (item) { + <%=Model%>.findById(req.params.<%=parentId%>).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); diff --git a/handlers/templates/putResource.ejs b/handlers/templates/putResource.ejs index bbe7356..d8a0ae5 100644 --- a/handlers/templates/putResource.ejs +++ b/handlers/templates/putResource.ejs @@ -1,4 +1,4 @@ - <%=Model%>.findByIdAndUpdate(req.params.id, req.body, {'new': true}).exec().then(function (item) { + <%=Model%>.findByIdAndUpdate(req.params.<%=id%>, req.body, {'new': true}).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); diff --git a/handlers/templates/putSubResource.ejs b/handlers/templates/putSubResource.ejs index 546f970..4cd0206 100644 --- a/handlers/templates/putSubResource.ejs +++ b/handlers/templates/putSubResource.ejs @@ -1,10 +1,10 @@ - <%=Model%>.findById(req.params.parentId).exec().then(function (item) { + <%=Model%>.findById(req.params.<%=parentId%>).exec().then(function (item) { if (!item) { res.sendStatus(404); return next(); } - var subDoc = item.<%=subModelAttribute%>.id(req.params.id); + var subDoc = item.<%=subModelAttribute%>.id(req.params.<%=id%>); if (!subDoc) { res.sendStatus(404); return next(); diff --git a/lib/specutil.js b/lib/specutil.js index d8669e1..a4ad1d3 100644 --- a/lib/specutil.js +++ b/lib/specutil.js @@ -10,6 +10,8 @@ var helpers = module.exports; var refRegExp = /^#\/definitions\/(\w*)$/; +var pathParamRegExp = /^{.*}$/; + var simpleTypes = { integer: 'Number', @@ -150,66 +152,47 @@ helpers.getRespSchema = function getRespSchema(object) { return _.uniq(schemas); }; -helpers.getPathType = function getPathType(routePath, verb) { - var parent = false; - var byId = false; - var subDoc = false; - routePath.split('/').forEach(function (element) { - if (element) { - if (element === '{parentId}') { - parent = true; - } else if (element === '{id}') { - byId = true; - } else { - if (byId) { - subDoc = true; - } +helpers.getPathMetadata = function getPathMetadata(routePath) { + var totParam = 0; + var totPaths = 0; + var metadata = { + type: null, + subResource: null, + parentId: null, + id: null + }; + + _.compact(routePath.split('/')).forEach(function (element) { + if (pathParamRegExp.test(element)) { + element = element.replace(/{/g, '').replace(/}/g, ''); + totParam += 1; + if (!metadata.id) { + metadata.id = element; + } else if (!metadata.parentId) { + metadata.parentId = metadata.id; + metadata.id = element; } + } else { + totPaths += 1; + metadata.subResource = element; } }); - switch (verb) { - case 'get': - if (!byId && !parent) { - return 'getResources'; - } - if (byId) { - if (parent) { - return 'getSubResource'; - } - return subDoc ? 'getSubResources' : 'getResource'; - } - break; - case 'put': - if (byId && !subDoc) { - return parent ? 'putSubResource' : 'putResource'; - } - break; - case 'delete': - if (byId && !subDoc) { - return parent ? 'deleteSubResource' : 'deleteResource'; - } - break; - case 'post': - return byId ? 'postSubResource' : 'postResource'; - default: - break; - } - return null; -}; - -helpers.getSubResourceAttribute = function getSubResourceAttribute(routePath) { - var parts = routePath.split('/'); - var subattr = null; - - while (parts) { - var part = parts.pop(); - if (part && part !== '{parentId}' && part !== '{id}') { - subattr = part; - break; + if (totParam === 0 && totPaths > 0) { + metadata.type = 'Resources'; + } else if (totParam === 1) { + if (totPaths === 1) { + metadata.type = 'Resource'; + } + if (totPaths === 2) { + metadata.type = 'SubResources'; } + } else if (totParam === 2) { + metadata.type = 'SubResource'; } - return subattr; + + debug('path metadata', metadata); + return metadata; }; helpers.formatSuccessResponse = function formatSuccessResponse(responses, returnKey) { diff --git a/test/test_libspecutil.js b/test/test_libspecutil.js index 6ed431b..4dc00a0 100644 --- a/test/test_libspecutil.js +++ b/test/test_libspecutil.js @@ -38,10 +38,12 @@ describe('lib/specutil', function () { type: 'object' } } - }}; + } + }; expect(specutil.makeTestData(parameters, schemas)).toEqual({ testArray: null, - testObj: null}); + testObj: null + }); }); it('param type Number', function () { @@ -59,9 +61,11 @@ describe('lib/specutil', function () { type: 'number' } } - }}; + } + }; expect(specutil.makeTestData(parameters, schemas)).toEqual({ - testNum: 1}); + testNum: 1 + }); }); it('param type Boolean', function () { @@ -79,9 +83,11 @@ describe('lib/specutil', function () { type: 'boolean' } } - }}; + } + }; expect(specutil.makeTestData(parameters, schemas)).toEqual({ - testBool: true}); + testBool: true + }); }); it('param type Date', function () { @@ -99,7 +105,8 @@ describe('lib/specutil', function () { type: 'date' } } - }}; + } + }; // date matching exactly is too difficult and prone for random // failures during testing; so as long as it is defined it is // considered a success. @@ -121,9 +128,11 @@ describe('lib/specutil', function () { type: 'string' } } - }}; + } + }; expect(specutil.makeTestData(parameters, schemas)).toEqual({ - testString: 'helloworld'}); + testString: 'helloworld' + }); }); }); @@ -331,95 +340,86 @@ describe('lib/specutil', function () { }); - describe('#getPathType', function () { - - it('GET no param ids', function () { - expect(specutil.getPathType('/pets', 'get')) - .toBe('getResources'); - }); - - it('GET param id - no parent', function () { - expect(specutil.getPathType('/pets/{id}', 'get')) - .toBe('getResource'); - }); - - it('GET param parentId - no id', function () { - expect(specutil.getPathType('/pets/{parentId}', 'get')) - .toBe(null); // invalid - }); - - it('GET param id - child resources', function () { - expect(specutil.getPathType('/pets/{id}/toys', 'get')) - .toBe('getSubResources'); - }); - - it('GET param parentid and id', function () { - expect(specutil.getPathType('/pets/{parentId}/toys/{id}', 'get')) - .toBe('getSubResource'); - }); - - it('PUT no param ids', function () { - expect(specutil.getPathType('/pets', 'put')) - .toBe(null); - }); - - it('PUT param id - no parent', function () { - expect(specutil.getPathType('/pets/{id}', 'put')) - .toBe('putResource'); - }); - - it('PUT param id - child resources', function () { - expect(specutil.getPathType('/pets/{id}/toys', 'put')) - .toBe(null); // invalid REST API - }); - - it('PUT param parentid and id', function () { - expect(specutil.getPathType('/pets/{parentId}/toys/{id}', 'put')) - .toBe('putSubResource'); - }); - - it('DELETE no param ids', function () { - expect(specutil.getPathType('/pets', 'delete')) - .toBe(null); - }); - - it('DELETE param id - no parent', function () { - expect(specutil.getPathType('/pets/{id}', 'delete')) - .toBe('deleteResource'); - }); - - it('DELETE param id - child resources', function () { - expect(specutil.getPathType('/pets/{id}/toys', 'delete')) - .toBe(null); // invalid REST API (delete should be specific items) - }); - - it('DELETE param parentid and id', function () { - expect(specutil.getPathType('/pets/{parentId}/toys/{id}', 'delete')) - .toBe('deleteSubResource'); - }); - - it('POST no param ids', function () { - expect(specutil.getPathType('/pets', 'post')) - .toBe('postResource'); - }); - - it('POST param id - no parent', function () { - expect(specutil.getPathType('/pets/{id}', 'post')) - .toBe('postSubResource'); // FIXME - }); - - it('POST param id - child resources', function () { - expect(specutil.getPathType('/pets/{id}/toys', 'post')) - .toBe('postSubResource'); - }); - - it('POST param parentid and id', function () { - expect(specutil.getPathType('/pets/{parentId}/toys/{id}', 'post')) - .toBe('postSubResource'); - }); - - it('PATCH no param ids', function () { - expect(specutil.getPathType('/pets/', 'patch')).toBe(null); + describe('#getPathMetadata', function () { + + it('root path only', function () { + expect(specutil.getPathMetadata('/')) + .toEqual({ + type: null, + subResource: null, + parentId: null, + id: null + }); + }); + + it('no param ids', function () { + expect(specutil.getPathMetadata('/pets')) + .toEqual({ + type: 'Resources', + subResource: 'pets', + parentId: null, + id: null + }); + }); + + it('param id - no parent', function () { + expect(specutil.getPathMetadata('/pets/{id}')) + .toEqual({ + type: 'Resource', + subResource: 'pets', + parentId: null, + id: 'id' + }); + }); + + it('param parentId - no id', function () { + expect(specutil.getPathMetadata('/pets/{parentId}')) + .toEqual({ + type: 'Resource', + subResource: 'pets', + parentId: null, + id: 'parentId' + }); + }); + + it('param id - child resources', function () { + expect(specutil.getPathMetadata('/pets/{id}/toys')) + .toEqual({ + type: 'SubResources', + subResource: 'toys', + parentId: null, + id: 'id' + }); + }); + + it('param parentid and id', function () { + expect(specutil.getPathMetadata('/pets/{parentId}/toys/{id}')) + .toEqual({ + type: 'SubResource', + subResource: 'toys', + parentId: 'parentId', + id: 'id' + }); + }); + + it('path with extra slash', function () { + expect(specutil.getPathMetadata('/pets/')) + .toEqual({ + type: 'Resources', + subResource: 'pets', + parentId: null, + id: null + }); + }); + + it('more than two path params', function () { + expect(specutil.getPathMetadata('/pets/{petId}/toys/{toyId}/balls/{ballId}')) + .toEqual({ + type: null, + subResource: 'balls', + parentId: 'petId', + id: 'toyId' + }); }); });