From d96b758c540ab0726921380fae73ce777723dd3d Mon Sep 17 00:00:00 2001 From: Richard Taylor Date: Mon, 7 Dec 2015 15:07:38 +0000 Subject: [PATCH] Handle nested defaults in body parameters Body parameters (`in` is `"body"`) are always grouped in a single Object, and as such there is only the option to set a `default` for the whole body (or none). This change looks deeper into the object for `default`s to support more flexible configuration of expected parameters in `body`. This brings the body support closer to e.g. `path` which already supports multiple parameters. This change will look for defaults parameter values for all `object` type parameters. This is also recursive, so if an `object` has a parameter that is an `object` then this code will look for defaults in that nested object. Note: this has only been implemented for Swagger 2.0 as I am not familiar with Swagger 1.2, so felt it best to leave 1.2 as-is. Test Plan: - Test that `gulp` runs and passes, ensuring: -- new code passes lint rules -- existing unit tests all run and pass - Add new tests for testing the new features, and confirm all pass. - Check the code coverage, and verify that (almost) all new lines have been covered. -- The only new lines not being covered is the exclusion of Swagger 1.2 schemas, which has only been checked visually. --- middleware/helpers.js | 55 +++++++ test/2.0/test-middleware-swagger-metadata.js | 117 ++++++++++++++ .../test-nested-defaults.json | 153 ++++++++++++++++++ 3 files changed, 325 insertions(+) create mode 100644 test/test-swagger-definitions/test-nested-defaults.json diff --git a/middleware/helpers.js b/middleware/helpers.js index 99e8953583..55309d3fc9 100644 --- a/middleware/helpers.js +++ b/middleware/helpers.js @@ -65,6 +65,55 @@ var isModelParameter = module.exports.isModelParameter = function (version, para return isModel; }; +/* + * Gets the parameter values for the members of an object (recursively). This + * includes getting any applicable defaults based on the Swagger definition. + */ +function getObjectParameterValue(version, parameter, val, debug) { + if (version === '1.2') { + return undefined; //Doesn't support old formats + } + if (_.isUndefined(val) || !_.isObject(val)) { + val = {}; + } + var foundDefault = false; + var nestedParams = parameter.properties; + if (_.isUndefined(nestedParams) && !_.isUndefined(parameter.schema)) { + nestedParams = parameter.schema.properties; + } + + if (!_.isObject(nestedParams)) { + return undefined; + } + + _.each(nestedParams, function (schemaParam, key) { + // + // Do we have a default for a missing value? + // + if (_.isUndefined(val[key]) && !_.isUndefined(schemaParam.default)) { + foundDefault = true; + val[key] = schemaParam.default; + debug(' Nested model default: %s =', key, schemaParam.default); + } else if (getParameterType(schemaParam) === 'object') { + // + // Go deeper + // + var newVal = getObjectParameterValue(version, schemaParam, val[key], debug); + if (!_.isUndefined(newVal)) { + val[key] = newVal; + foundDefault = true; + } + } + }); + + if (foundDefault) { + return val; + } else { + // Didn't change anything (except perhaps making val an object) so... + return undefined; + } +} + module.exports.getParameterValue = function (version, parameter, pathKeys, match, req, debug) { var defaultVal = version === '1.2' ? parameter.defaultValue : parameter.default; var paramLocation = version === '1.2' ? parameter.paramType : parameter.in; @@ -111,6 +160,12 @@ module.exports.getParameterValue = function (version, parameter, pathKeys, match // Use the default value when necessary if (_.isUndefined(val) && !_.isUndefined(defaultVal)) { val = defaultVal; + } else if (getParameterType(parameter) === 'object') { + // We need to look deeper for defaults + var newVal = getObjectParameterValue(version, parameter, val, debug); + if (!_.isUndefined(newVal)) { + val = newVal; + } } return val; diff --git a/test/2.0/test-middleware-swagger-metadata.js b/test/2.0/test-middleware-swagger-metadata.js index 16aa91a8c7..32abaeba4d 100644 --- a/test/2.0/test-middleware-swagger-metadata.js +++ b/test/2.0/test-middleware-swagger-metadata.js @@ -35,6 +35,7 @@ var async = require('async'); var helpers = require('../helpers'); var path = require('path'); var petStoreJson = _.cloneDeep(require('../../samples/2.0/petstore.json')); +var nestingTestJson = _.cloneDeep(require('../test-swagger-definitions/test-nested-defaults.json')); var pkg = require('../../package.json'); var request = require('supertest'); var spec = require('../../lib/helpers').getSpec('2.0'); @@ -248,6 +249,122 @@ describe('Swagger Metadata Middleware v2.0', function () { .end(helpers.expectContent({id: 1, name: 'Top Dog'}, done)); }); }); + + /* + * Tests for setting defaults of parameters nested within an object (i.e. in + * the request body) + */ + describe('nested default parameters in body object', function () { + /* + * Before each test load the test Swagger definied for the tests + */ + var cTestJson; + beforeEach(function () { + cTestJson = _.cloneDeep(nestingTestJson); + }); + + it('should add 1 deep defaults', function (done) { + helpers.createServer([cTestJson], { + swaggerRouterOptions: { + controllers: { + testNest1: function (req, res, next) { + var body = req.swagger.params.testParam.value; + var expected = { + hasDefault: 'nest1' + }; + assert.deepEqual(body, expected); + + res.end('OK'); + + next(); + } + } + } + }, function (app) { + request(app) + .post('/api/nest1') + .expect(200) + .end(helpers.expectContent('OK', done)); + }); + }); + + it('should not add 1 deep if no defaults', function (done) { + helpers.createServer([cTestJson], { + swaggerRouterOptions: { + controllers: { + testNest1NoDefault: function (req, res, next) { + var body = req.swagger.params.testParam.value; + var expected = {}; + assert.deepEqual(body, expected); + + res.end('OK'); + + next(); + } + } + } + }, function (app) { + request(app) + .post('/api/nest1_NoDefault') + .expect(200) + .end(helpers.expectContent('OK', done)); + }); + }); + + it('should add 2 deep with defaults', function (done) { + helpers.createServer([cTestJson], { + swaggerRouterOptions: { + controllers: { + testNest2: function (req, res, next) { + var body = req.swagger.params.testParam.value; + var expected = { + nest1: { + hasDefault: 'nest1' + } + }; + assert.deepEqual(body, expected); + + res.end('OK'); + + next(); + } + } + } + }, function (app) { + request(app) + .post('/api/nest2') + .expect(200) + .end(helpers.expectContent('OK', done)); + }); + }); + + it('should add 2 deep with partial defaults', function (done) { + helpers.createServer([cTestJson], { + swaggerRouterOptions: { + controllers: { + testNest2Default1: function (req, res, next) { + var body = req.swagger.params.testParam.value; + var expected = { + withDefault: { + hasDefault: 'nest1' + } + }; + assert.deepEqual(body, expected); + + res.end('OK'); + + next(); + } + } + } + }, function (app) { + request(app) + .post('/api/nest2_default1') + .expect(200) + .end(helpers.expectContent('OK', done)); + }); + }); + }); describe('non-multipart form parameters', function () { it('should handle primitives', function (done) { diff --git a/test/test-swagger-definitions/test-nested-defaults.json b/test/test-swagger-definitions/test-nested-defaults.json new file mode 100644 index 0000000000..44653e89d4 --- /dev/null +++ b/test/test-swagger-definitions/test-nested-defaults.json @@ -0,0 +1,153 @@ +{ + "swagger": "2.0", + "info": { + "version": "1.0.0", + "title": "Test nested default values", + "license": { + "name": "MIT" + } + }, + "host": "example.com", + "basePath": "/api", + "schemes": [ + "http" + ], + "paths": { + "/nest1": { + "post": { + "operationId": "testNest1", + "summary": "Tests a 1 level of nesting with a default", + "parameters": [ + { + "name": "testParam", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Nest1" + } + } + ], + "responses": { + "200": { + "description": "Created test response" + } + } + } + }, + "/nest1_NoDefault": { + "post": { + "operationId": "testNest1NoDefault", + "summary": "Tests a 1 level of nesting without a default", + "parameters": [ + { + "name": "testParam", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Nest1_NoDefault" + } + } + ], + "responses": { + "200": { + "description": "Created test response" + } + } + } + }, + "/nest2": { + "post": { + "operationId": "testNest2", + "summary": "Tests 2 levels of nesting with a default at level 2", + "parameters": [ + { + "name": "testParam", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Nest2" + } + } + ], + "responses": { + "200": { + "description": "Created test response" + } + } + } + }, + "/nest2_default1": { + "post": { + "operationId": "testNest2Default1", + "summary": "Tests 2 levels of nesting with one default param, and one non-default", + "parameters": [ + { + "name": "testParam", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Nest2_Default1" + } + } + ], + "responses": { + "200": { + "description": "Created test response" + } + } + } + } + + }, + "definitions": { + "Nest1": { + "type": "object", + "properties": { + "hasDefault": { + "type": "string", + "default": "nest1" + } + } + }, + "Nest1_NoDefault": { + "type": "object", + "properties": { + "noDefault": { + "type": "string" + } + } + }, + "Nest2": { + "properties": { + "nest1": { + "$ref": "#/definitions/Nest1" + } + } + }, + "Nest2_Default1": { + "properties": { + "withDefault": { + "$ref": "#/definitions/Nest1" + }, + "withoutDefault": { + "$ref": "#/definitions/Nest1_NoDefault" + } + } + } + }, + "produces": [ + "application/json" + ], + "securityDefinitions": { + "oauth2": { + "type": "oauth2", + "scopes": { + "read": "Read access.", + "write": "Write access" + }, + "flow": "accessCode", + "authorizationUrl": "http://petstore.swagger.wordnik.com/oauth/authorize", + "tokenUrl": "http://petstore.swagger.wordnik.com/oauth/token" + } + } +}