Merge pull request #64 from strongloop/feature/property-constraints

Preserve property type constraints
This commit is contained in:
Miroslav Bajtoš 2014-10-16 08:58:37 +02:00
commit 0f5ab45c0b
8 changed files with 139 additions and 84 deletions

View File

@ -4,7 +4,7 @@
* Module dependencies. * Module dependencies.
*/ */
var modelHelper = require('./model-helper'); var modelHelper = require('./model-helper');
var routeHelper = require('./route-helper'); var typeConverter = require('./type-converter');
var urlJoin = require('./url-join'); var urlJoin = require('./url-join');
/** /**
@ -47,7 +47,7 @@ var classHelper = module.exports = {
return { return {
path: aClass.http.path, path: aClass.http.path,
description: routeHelper.convertText(description) description: typeConverter.convertText(description)
}; };
} }
}; };

View File

@ -4,7 +4,9 @@
* Module dependencies. * Module dependencies.
*/ */
var _cloneDeep = require('lodash.clonedeep'); var _cloneDeep = require('lodash.clonedeep');
var _pick = require('lodash.pick');
var translateDataTypeKeys = require('./translate-data-type-keys'); var translateDataTypeKeys = require('./translate-data-type-keys');
var typeConverter = require('./type-converter');
/** /**
* Export the modelHelper singleton. * Export the modelHelper singleton.
@ -57,20 +59,20 @@ var modelHelper = module.exports = {
} }
// Eke a type out of the constructors we were passed. // Eke a type out of the constructors we were passed.
prop = modelHelper.LDLPropToSwaggerDataType(prop); var swaggerType = modelHelper.LDLPropToSwaggerDataType(prop);
var desc = typeConverter.convertText(prop.description || prop.doc);
if (desc) swaggerType.description = desc;
// Required props sit in a per-model array. // Required props sit in a per-model array.
if (prop.required || (prop.id && !prop.generated)) { if (prop.required || (prop.id && !prop.generated)) {
required.push(key); required.push(key);
} }
// Change mismatched keys.
prop = translateDataTypeKeys(prop);
// Assign this back to the properties object. // Assign this back to the properties object.
properties[key] = prop; properties[key] = swaggerType;
var propType = def.properties[key].type; var propType = prop.type;
if (typeof propType === 'function' && propType.modelName) { if (typeof propType === 'function' && propType.modelName) {
if (referencedModels.indexOf(propType) === -1) { if (referencedModels.indexOf(propType) === -1) {
referencedModels.push(propType); referencedModels.push(propType);
@ -88,6 +90,7 @@ var modelHelper = module.exports = {
out[name] = { out[name] = {
id: name, id: name,
description: typeConverter.convertText(def.description),
properties: properties, properties: properties,
required: required required: required
}; };
@ -134,34 +137,52 @@ var modelHelper = module.exports = {
// Converts a prop defined with the LDL spec to one conforming to the // Converts a prop defined with the LDL spec to one conforming to the
// Swagger spec. // Swagger spec.
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives // https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives
LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(prop) { LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(ldlType) {
var out = _cloneDeep(prop); var SWAGGER_DATA_TYPE_FIELDS = [
out.type = modelHelper.getPropType(out.type); 'format',
'defaultValue',
'enum',
'minimum',
'maximum',
'uniqueItems',
// loopback-explorer extensions
'length',
// https://www.npmjs.org/package/swagger-validation
'pattern'
];
if (out.type === 'array') { // Rename LoopBack keys to Swagger keys
var hasItemType = Array.isArray(prop.type) && prop.type.length; ldlType = translateDataTypeKeys(ldlType);
var arrayItem = hasItemType && prop.type[0];
// Pick only keys supported by Swagger
var swaggerType = _pick(ldlType, SWAGGER_DATA_TYPE_FIELDS);
swaggerType.type = modelHelper.getPropType(ldlType.type);
if (swaggerType.type === 'array') {
var hasItemType = Array.isArray(ldlType.type) && ldlType.type.length;
var arrayItem = hasItemType && ldlType.type[0];
if (arrayItem) { if (arrayItem) {
if(typeof arrayItem === 'object') { if(typeof arrayItem === 'object') {
out.items = modelHelper.LDLPropToSwaggerDataType(arrayItem); swaggerType.items = modelHelper.LDLPropToSwaggerDataType(arrayItem);
} else { } else {
out.items = { type: modelHelper.getPropType(arrayItem) }; swaggerType.items = { type: modelHelper.getPropType(arrayItem) };
} }
} else { } else {
// NOTE: `any` is not a supported type in swagger 1.2 // NOTE: `any` is not a supported type in swagger 1.2
out.items = { type: 'any' }; swaggerType.items = { type: 'any' };
} }
} else if (out.type === 'date') { } else if (swaggerType.type === 'date') {
out.type = 'string'; swaggerType.type = 'string';
out.format = 'date'; swaggerType.format = 'date';
} else if (out.type === 'buffer') { } else if (swaggerType.type === 'buffer') {
out.type = 'string'; swaggerType.type = 'string';
out.format = 'byte'; swaggerType.format = 'byte';
} else if (out.type === 'number') { } else if (swaggerType.type === 'number') {
out.format = 'double'; // Since all JS numbers are doubles swaggerType.format = 'double'; // Since all JS numbers are doubles
} }
return out; return swaggerType;
} }
}; };

View File

@ -6,8 +6,9 @@
var debug = require('debug')('loopback:explorer:routeHelpers'); var debug = require('debug')('loopback:explorer:routeHelpers');
var _cloneDeep = require('lodash.clonedeep'); var _cloneDeep = require('lodash.clonedeep');
var translateDataTypeKeys = require('./translate-data-type-keys'); var _assign = require('lodash.assign');
var modelHelper = require('./model-helper'); var modelHelper = require('./model-helper');
var typeConverter = require('./type-converter');
/** /**
* Export the routeHelper singleton. * Export the routeHelper singleton.
@ -70,9 +71,6 @@ var routeHelper = module.exports = {
return true; return true;
}); });
// Translate LDL keys to Swagger keys.
accepts = accepts.map(translateDataTypeKeys);
// Turn accept definitions in to parameter docs. // Turn accept definitions in to parameter docs.
accepts = accepts.map(routeHelper.acceptToParameter(route)); accepts = accepts.map(routeHelper.acceptToParameter(route));
@ -97,19 +95,19 @@ var routeHelper = module.exports = {
} }
} }
// Translate LDL keys to Swagger keys.
var returns = routeReturns.map(translateDataTypeKeys);
// Convert `returns` into a single object for later conversion into an // Convert `returns` into a single object for later conversion into an
// operation object. // operation object.
if (returns && returns.length > 1) { if (routeReturns && routeReturns.length > 1) {
// TODO ad-hoc model definition in the case of multiple return values. // TODO ad-hoc model definition in the case of multiple return values.
returns = {model: 'object'}; routeReturns = { type: 'object' };
} else { } else {
returns = returns[0] || {}; // Per the spec:
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object
// This is the only object that may have a type of 'void'.
routeReturns = routeReturns[0] || { type: 'void' };
} }
return returns; return routeReturns;
}, },
/** /**
@ -118,8 +116,6 @@ var routeHelper = module.exports = {
* See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object * See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object
*/ */
routeToAPIDoc: function routeToAPIDoc(route, classDef) { routeToAPIDoc: function routeToAPIDoc(route, classDef) {
var returnDesc;
// Some parameters need to be altered; eventually most of this should // Some parameters need to be altered; eventually most of this should
// be removed. // be removed.
var accepts = routeHelper.convertAcceptsToSwagger(route, classDef); var accepts = routeHelper.convertAcceptsToSwagger(route, classDef);
@ -135,17 +131,13 @@ var routeHelper = module.exports = {
method: routeHelper.convertVerb(route.verb), method: routeHelper.convertVerb(route.verb),
// [rfeng] Swagger UI doesn't escape '.' for jQuery selector // [rfeng] Swagger UI doesn't escape '.' for jQuery selector
nickname: route.method.replace(/\./g, '_'), nickname: route.method.replace(/\./g, '_'),
// Per the spec:
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#523-operation-object
// This is the only object that may have a type of 'void'.
type: returns.model || returns.type || 'void',
parameters: accepts, parameters: accepts,
// TODO(schoon) - We don't have descriptions for this yet. // TODO(schoon) - We don't have descriptions for this yet.
responseMessages: [], responseMessages: [],
summary: routeHelper.convertText(route.description), summary: typeConverter.convertText(route.description),
notes: routeHelper.convertText(route.notes), notes: typeConverter.convertText(route.notes),
deprecated: route.deprecated deprecated: route.deprecated
})] }, returns)]
}; };
return apiDoc; return apiDoc;
@ -200,16 +192,12 @@ var routeHelper = module.exports = {
var out = { var out = {
paramType: paramType || type, paramType: paramType || type,
name: name, name: name,
description: routeHelper.convertText(accepts.description), description: typeConverter.convertText(accepts.description),
type: accepts.type,
required: !!accepts.required, required: !!accepts.required,
defaultValue: accepts.defaultValue,
minimum: accepts.minimum,
maximum: accepts.maximum,
allowMultiple: false allowMultiple: false
}; };
out = routeHelper.extendWithType(out); out = routeHelper.extendWithType(out, accepts);
// HACK: Derive the type from model // HACK: Derive the type from model
if(out.name === 'data' && out.type === 'object') { if(out.name === 'data' && out.type === 'object') {
@ -225,32 +213,20 @@ var routeHelper = module.exports = {
* a proper Swagger type and optional `format` and `items` fields. * a proper Swagger type and optional `format` and `items` fields.
* Does not modify original object. * Does not modify original object.
* @param {Object} obj Object to extend. * @param {Object} obj Object to extend.
* @param {Object} ldlType LDL type definition
* @return {Object} Extended object. * @return {Object} Extended object.
*/ */
extendWithType: function extendWithType(obj) { extendWithType: function extendWithType(obj, ldlType) {
obj = _cloneDeep(obj); obj = _cloneDeep(obj);
// Format the `type` property using our LDL converter. // Format the `type` property using our LDL converter.
var typeDesc = modelHelper var typeDesc = modelHelper.LDLPropToSwaggerDataType(ldlType);
.LDLPropToSwaggerDataType({type: obj.model || obj.type});
// The `typeDesc` may have additional attributes, such as // The `typeDesc` may have additional attributes, such as
// `format` for non-primitive types. // `format` for non-primitive types.
Object.keys(typeDesc).forEach(function(key){ _assign(obj, typeDesc);
obj[key] = typeDesc[key];
});
return obj;
},
/** return obj;
* Convert a text value that can be expressed either as a string or
* as an array of strings.
* @param {string|Array} value
* @returns {string}
*/
convertText: function(value) {
if (Array.isArray(value))
return value.join('\n');
return value;
} }
}; };

View File

@ -9,7 +9,6 @@ var _cloneDeep = require('lodash.clonedeep');
// Keys that are different between LDL and Swagger // Keys that are different between LDL and Swagger
var KEY_TRANSLATIONS = { var KEY_TRANSLATIONS = {
// LDL : Swagger // LDL : Swagger
'doc': 'description',
'default': 'defaultValue', 'default': 'defaultValue',
'min': 'minimum', 'min': 'minimum',
'max': 'maximum' 'max': 'maximum'

14
lib/type-converter.js Normal file
View File

@ -0,0 +1,14 @@
var typeConverter = module.exports = {
/**
* Convert a text value that can be expressed either as a string or
* as an array of strings.
* @param {string|Array} value
* @returns {string}
*/
convertText: function(value) {
if (Array.isArray(value))
return value.join('\n');
return value;
}
};

View File

@ -34,8 +34,10 @@
"cors": "^2.4.2", "cors": "^2.4.2",
"debug": "~1.0.3", "debug": "~1.0.3",
"express": "3.x", "express": "3.x",
"lodash.assign": "^2.4.1",
"lodash.clonedeep": "^2.4.1", "lodash.clonedeep": "^2.4.1",
"lodash.defaults": "^2.4.1", "lodash.defaults": "^2.4.1",
"lodash.pick": "^2.4.1",
"swagger-ui": "~2.0.18" "swagger-ui": "~2.0.18"
} }
} }

View File

@ -1,6 +1,7 @@
'use strict'; 'use strict';
var modelHelper = require('../lib/model-helper'); var modelHelper = require('../lib/model-helper');
var _defaults = require('lodash.defaults');
var loopback = require('loopback'); var loopback = require('loopback');
var expect = require('chai').expect; var expect = require('chai').expect;
@ -122,6 +123,27 @@ describe('model-helper', function() {
}); });
}); });
it('converts model property field `doc`', function() {
var def = buildSwaggerModels({
name: { type: String, doc: 'a-description' }
});
var nameProp = def.properties.name;
expect(nameProp).to.have.property('description', 'a-description');
});
it('converts model property field `description`', function() {
var def = buildSwaggerModels({
name: { type: String, description: 'a-description' }
});
var nameProp = def.properties.name;
expect(nameProp).to.have.property('description', 'a-description');
});
it('converts model field `description`', function() {
var def = buildSwaggerModels({}, { description: 'a-description' });
expect(def).to.have.property('description', 'a-description');
});
}); });
describe('related models', function() { describe('related models', function() {
@ -195,21 +217,27 @@ describe('model-helper', function() {
}); });
// Simulates the format of a remoting class. // Simulates the format of a remoting class.
function buildSwaggerModels(model) { function buildSwaggerModels(modelProperties, modelOptions) {
var aClass = createModelCtor(model); var aClass = createModelCtor(modelProperties, modelOptions);
return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel; return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel;
} }
function createModelCtor(model) { function createModelCtor(properties, modelOptions) {
Object.keys(model).forEach(function(name) { Object.keys(properties).forEach(function(name) {
model[name] = {type: model[name]}; var type = properties[name];
if (typeof type !== 'object' || Array.isArray(type))
properties[name] = { type: type };
}); });
var definition = {
name: 'testModel',
properties: properties
};
_defaults(definition, modelOptions);
var aClass = { var aClass = {
ctor: { ctor: {
definition: { definition: definition
name: 'testModel',
properties: model
}
} }
}; };
return aClass; return aClass;

View File

@ -143,6 +143,21 @@ describe('route-helper', function() {
expect(params.length).to.equal(0); expect(params.length).to.equal(0);
}); });
it('preserves `enum` accepts arg metadata', function() {
var doc = createAPIDoc({
accepts: [{ name: 'arg', type: 'number', enum: [1,2,3] }]
});
expect(doc.operations[0].parameters[0])
.to.have.property('enum').eql([1,2,3]);
});
it('preserves `enum` returns arg metadata', function() {
var doc = createAPIDoc({
returns: [{ name: 'arg', root: true, type: 'number', enum: [1,2,3] }]
});
expect(doc.operations[0])
.to.have.property('enum').eql([1,2,3]);
});
}); });
// Easy wrapper around createRoute // Easy wrapper around createRoute