Merge pull request #64 from strongloop/feature/property-constraints
Preserve property type constraints
This commit is contained in:
commit
0f5ab45c0b
|
@ -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)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue