Merge pull request #118 from strongloop/feature/use-loopback-swagger
Use loopback-swagger to generate swagger.json
This commit is contained in:
commit
016a9b5457
36
index.js
36
index.js
|
@ -6,7 +6,8 @@ var url = require('url');
|
|||
var path = require('path');
|
||||
var urlJoin = require('./lib/url-join');
|
||||
var _defaults = require('lodash').defaults;
|
||||
var swagger = require('./lib/swagger');
|
||||
var cors = require('cors');
|
||||
var createSwaggerObject = require('loopback-swagger').generateSwaggerSpec;
|
||||
var SWAGGER_UI_ROOT = require('strong-swagger-ui/index').dist;
|
||||
var STATIC_ROOT = path.join(__dirname, 'public');
|
||||
|
||||
|
@ -42,7 +43,7 @@ function routes(loopbackApplication, options) {
|
|||
|
||||
var router = new loopback.Router();
|
||||
|
||||
swagger.mountSwagger(loopbackApplication, router, options);
|
||||
mountSwagger(loopbackApplication, router, options);
|
||||
|
||||
// config.json is loaded by swagger-ui. The server should respond
|
||||
// with the relative URI of the resource doc.
|
||||
|
@ -81,3 +82,34 @@ function routes(loopbackApplication, options) {
|
|||
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup Swagger documentation on the given express app.
|
||||
*
|
||||
* @param {Application} loopbackApplication The loopback application to
|
||||
* document.
|
||||
* @param {Application} swaggerApp Swagger application used for hosting
|
||||
* swagger documentation.
|
||||
* @param {Object} opts Options.
|
||||
*/
|
||||
function mountSwagger(loopbackApplication, swaggerApp, opts) {
|
||||
var swaggerObject = createSwaggerObject(loopbackApplication, opts);
|
||||
|
||||
var resourcePath = opts && opts.resourcePath || 'swagger.json';
|
||||
if (resourcePath[0] !== '/') resourcePath = '/' + resourcePath;
|
||||
|
||||
var remotes = loopbackApplication.remotes();
|
||||
setupCors(swaggerApp, remotes);
|
||||
|
||||
swaggerApp.get(resourcePath, function sendSwaggerObject(req, res) {
|
||||
res.status(200).send(swaggerObject);
|
||||
});
|
||||
}
|
||||
|
||||
function setupCors(swaggerApp, remotes) {
|
||||
var corsOptions = remotes.options && remotes.options.cors ||
|
||||
{ origin: true, credentials: true };
|
||||
|
||||
// TODO(bajtos) Skip CORS when remotes.options.cors === false
|
||||
swaggerApp.use(cors(corsOptions));
|
||||
}
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
var schemaBuilder = require('./schema-builder');
|
||||
var typeConverter = require('./type-converter');
|
||||
var TypeRegistry = require('./type-registry');
|
||||
|
||||
/**
|
||||
* Export the modelHelper singleton.
|
||||
*/
|
||||
var modelHelper = module.exports = {
|
||||
/**
|
||||
* Given a class (from remotes.classes()), generate a model definition.
|
||||
* This is used to generate the schema at the top of many endpoints.
|
||||
* @param {Class} modelClass Model class.
|
||||
* @param {TypeRegistry} typeRegistry Registry of types and models.
|
||||
* @return {Object} Associated model definition.
|
||||
*/
|
||||
registerModelDefinition: function(modelCtor, typeRegistry) {
|
||||
var lbdef = modelCtor.definition;
|
||||
|
||||
if (!lbdef) {
|
||||
// The model does not have any definition, it was most likely
|
||||
// created as a placeholder for an unknown property type
|
||||
return;
|
||||
}
|
||||
|
||||
var name = lbdef.name;
|
||||
if (typeRegistry.isDefined(name)) {
|
||||
// The model is already included
|
||||
return;
|
||||
}
|
||||
|
||||
var swaggerDef = {
|
||||
description: typeConverter.convertText(
|
||||
lbdef.description || (lbdef.settings && lbdef.settings.description)),
|
||||
properties: {},
|
||||
required: []
|
||||
};
|
||||
|
||||
var properties = lbdef.rawProperties || lbdef.properties;
|
||||
|
||||
// Iterate through each property in the model definition.
|
||||
// Types may be defined as constructors (e.g. String, Date, etc.),
|
||||
// or as strings; swaggerSchema.builFromLoopBackType() will take
|
||||
// care of the conversion.
|
||||
Object.keys(properties).forEach(function(key) {
|
||||
var prop = properties[key];
|
||||
|
||||
// Hide hidden properties.
|
||||
if (modelHelper.isHiddenProperty(lbdef, key))
|
||||
return;
|
||||
|
||||
// Eke a type out of the constructors we were passed.
|
||||
var schema = schemaBuilder.buildFromLoopBackType(prop, typeRegistry);
|
||||
|
||||
var desc = typeConverter.convertText(prop.description || prop.doc);
|
||||
if (desc) schema.description = desc;
|
||||
|
||||
// Required props sit in a per-model array.
|
||||
if (prop.required || (prop.id && !prop.generated)) {
|
||||
swaggerDef.required.push(key);
|
||||
}
|
||||
|
||||
// Assign the schema to the properties object.
|
||||
swaggerDef.properties[key] = schema;
|
||||
});
|
||||
|
||||
if (lbdef.settings) {
|
||||
var strict = lbdef.settings.strict;
|
||||
var additionalProperties = lbdef.settings.additionalProperties;
|
||||
var notAllowAdditionalProperties = strict || (additionalProperties !== true);
|
||||
if (notAllowAdditionalProperties){
|
||||
swaggerDef.additionalProperties = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!swaggerDef.required.length) {
|
||||
// "required" must have at least one item when present
|
||||
delete swaggerDef.required;
|
||||
}
|
||||
|
||||
typeRegistry.register(name, swaggerDef);
|
||||
|
||||
// Add models from settings
|
||||
if (lbdef.settings && lbdef.settings.models) {
|
||||
for (var m in lbdef.settings.models) {
|
||||
var model = modelCtor[m];
|
||||
if (typeof model !== 'function' || !model.modelName) continue;
|
||||
modelHelper.registerModelDefinition(model, typeRegistry);
|
||||
// TODO it shouldn't be necessary to reference the model here,
|
||||
// let accepts/returns/property reference it instead
|
||||
typeRegistry.reference(model.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate model definitions for related models
|
||||
for (var r in modelCtor.relations) {
|
||||
var rel = modelCtor.relations[r];
|
||||
if (rel.modelTo) {
|
||||
modelHelper.registerModelDefinition(rel.modelTo, typeRegistry);
|
||||
// TODO it shouldn't be necessary to reference the model here,
|
||||
// let accepts/returns/property reference it instead
|
||||
typeRegistry.reference(rel.modelTo.modelName);
|
||||
}
|
||||
if (rel.modelThrough) {
|
||||
modelHelper.registerModelDefinition(rel.modelThrough, typeRegistry);
|
||||
// TODO it shouldn't be necessary to reference the model here,
|
||||
// let accepts/returns/property reference it instead
|
||||
typeRegistry.reference(rel.modelThrough.modelName);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
isHiddenProperty: function(definition, propName) {
|
||||
return definition.settings &&
|
||||
Array.isArray(definition.settings.hidden) &&
|
||||
definition.settings.hidden.indexOf(propName) !== -1;
|
||||
},
|
||||
};
|
|
@ -1,252 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
|
||||
var debug = require('debug')('loopback:explorer:routeHelpers');
|
||||
var _assign = require('lodash').assign;
|
||||
var typeConverter = require('./type-converter');
|
||||
var schemaBuilder = require('./schema-builder');
|
||||
|
||||
/**
|
||||
* Export the routeHelper singleton.
|
||||
*/
|
||||
var routeHelper = module.exports = {
|
||||
/**
|
||||
* Given a route, generate an API description and add it to the doc.
|
||||
* If a route shares a path with another route (same path, different verb),
|
||||
* add it as a new operation under that path entry.
|
||||
*
|
||||
* Routes can be translated to API declaration 'operations',
|
||||
* but they need a little massaging first. The `accepts` and
|
||||
* `returns` declarations need some basic conversions to be compatible.
|
||||
*
|
||||
* This method will convert the route and add it to the doc.
|
||||
* @param {Route} route Strong Remoting Route object.
|
||||
* @param {Class} classDef Strong Remoting class.
|
||||
* @param {TypeRegistry} typeRegistry Registry of types and models.
|
||||
* @param {Object} paths Swagger Path Object,
|
||||
* see https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#pathsObject
|
||||
*/
|
||||
addRouteToSwaggerPaths: function(route, classDef, typeRegistry, paths) {
|
||||
var entryToAdd = routeHelper.routeToPathEntry(route, classDef,
|
||||
typeRegistry);
|
||||
if (!(entryToAdd.path in paths)) {
|
||||
paths[entryToAdd.path] = {};
|
||||
}
|
||||
paths[entryToAdd.path][entryToAdd.method] = entryToAdd.operation;
|
||||
},
|
||||
|
||||
/**
|
||||
* Massage route.accepts.
|
||||
* @param {Object} route Strong Remoting Route object.
|
||||
* @param {Class} classDef Strong Remoting class.
|
||||
* @param {TypeRegistry} typeRegistry Registry of types and models.
|
||||
* @return {Array} Array of param docs.
|
||||
*/
|
||||
convertAcceptsToSwagger: function(route, classDef, typeRegistry) {
|
||||
var accepts = route.accepts || [];
|
||||
var split = route.method.split('.');
|
||||
if (classDef && classDef.sharedCtor &&
|
||||
classDef.sharedCtor.accepts && split.length > 2 /* HACK */) {
|
||||
accepts = accepts.concat(classDef.sharedCtor.accepts);
|
||||
}
|
||||
|
||||
// Filter out parameters that are generated from the incoming request,
|
||||
// or generated by functions that use those resources.
|
||||
accepts = accepts.filter(function(arg) {
|
||||
if (!arg.http) return true;
|
||||
// Don't show derived arguments.
|
||||
if (typeof arg.http === 'function') return false;
|
||||
// Don't show arguments set to the incoming http request.
|
||||
// Please note that body needs to be shown, such as User.create().
|
||||
if (arg.http.source === 'req' ||
|
||||
arg.http.source === 'res' ||
|
||||
arg.http.source === 'context') {
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Turn accept definitions in to parameter docs.
|
||||
accepts = accepts.map(
|
||||
routeHelper.acceptToParameter(route, classDef, typeRegistry));
|
||||
|
||||
return accepts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Massage route.returns.
|
||||
* @param {Object} route Strong Remoting Route object.
|
||||
* @return {Object} A single returns param doc.
|
||||
*/
|
||||
convertReturnsToSwagger: function(route, typeRegistry) {
|
||||
var routeReturns = route.returns;
|
||||
if (!routeReturns || !routeReturns.length) {
|
||||
// An operation that returns nothing will have
|
||||
// no schema declaration for its response.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (routeReturns.length === 1 && routeReturns[0].root) {
|
||||
if (routeReturns[0].model)
|
||||
return { $ref: typeRegistry.reference(routeReturns[0].model) };
|
||||
return schemaBuilder.buildFromLoopBackType(routeReturns[0], typeRegistry);
|
||||
}
|
||||
|
||||
// Convert `returns` into a single object for later conversion into an
|
||||
// operation object.
|
||||
// TODO ad-hoc model definition in the case of multiple return values.
|
||||
// It is enough to replace 'object' with an anonymous type definition
|
||||
// based on all routeReturn items. The schema converter should take
|
||||
// care of the remaning conversions.
|
||||
var def = { type: 'object' };
|
||||
return schemaBuilder.buildFromLoopBackType(def, typeRegistry);
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts from an sl-remoting-formatted "Route" description to a
|
||||
* Swagger-formatted "Path Item Object"
|
||||
* See swagger-spec/2.0.md#pathItemObject
|
||||
*/
|
||||
routeToPathEntry: function(route, classDef, typeRegistry) {
|
||||
// Some parameters need to be altered; eventually most of this should
|
||||
// be removed.
|
||||
var accepts = routeHelper.convertAcceptsToSwagger(route, classDef,
|
||||
typeRegistry);
|
||||
var returns = routeHelper.convertReturnsToSwagger(route, typeRegistry);
|
||||
var defaultCode = route.returns && route.returns.length ? 200 : 204;
|
||||
// TODO - support strong-remoting's option for a custom response code
|
||||
|
||||
var responseMessages = {};
|
||||
responseMessages[defaultCode] = {
|
||||
description: 'Request was successful',
|
||||
schema: returns,
|
||||
// TODO - headers, examples
|
||||
};
|
||||
|
||||
if (route.errors) {
|
||||
// TODO define new LDL syntax that is status-code-indexed
|
||||
// and which allow users to specify headers & examples
|
||||
route.errors.forEach(function(msg) {
|
||||
responseMessages[msg.code] = {
|
||||
description: msg.message,
|
||||
schema: schemaBuilder.buildFromLoopBackType(msg.responseModel,
|
||||
typeRegistry),
|
||||
// TODO - headers, examples
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
debug('route %j', route);
|
||||
|
||||
var tags = [];
|
||||
if (classDef && classDef.name) {
|
||||
tags.push(classDef.name);
|
||||
}
|
||||
|
||||
var entry = {
|
||||
path: routeHelper.convertPathFragments(route.path),
|
||||
method: routeHelper.convertVerb(route.verb),
|
||||
operation: {
|
||||
tags: tags,
|
||||
summary: typeConverter.convertText(route.description),
|
||||
description: typeConverter.convertText(route.notes),
|
||||
// [bajtos] We used to remove leading model name from the operation
|
||||
// name for Swagger Spec 1.2. Swagger Spec 2.0 requires
|
||||
// operation ids to be unique, thus we have to include the model name.
|
||||
operationId: route.method,
|
||||
// [bajtos] we are omitting consumes and produces, as they are same
|
||||
// for all methods and they are already specified in top-level fields
|
||||
parameters: accepts,
|
||||
responses: responseMessages,
|
||||
deprecated: !!route.deprecated,
|
||||
// TODO: security
|
||||
}
|
||||
};
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
convertPathFragments: function convertPathFragments(path) {
|
||||
return path.split('/').map(function (fragment) {
|
||||
if (fragment.charAt(0) === ':') {
|
||||
return '{' + fragment.slice(1) + '}';
|
||||
}
|
||||
return fragment;
|
||||
}).join('/');
|
||||
},
|
||||
|
||||
convertVerb: function convertVerb(verb) {
|
||||
if (verb.toLowerCase() === 'all') {
|
||||
return 'post';
|
||||
}
|
||||
|
||||
if (verb.toLowerCase() === 'del') {
|
||||
return 'delete';
|
||||
}
|
||||
|
||||
return verb.toLowerCase();
|
||||
},
|
||||
|
||||
/**
|
||||
* A generator to convert from an sl-remoting-formatted "Accepts" description
|
||||
* to a Swagger-formatted "Parameter" description.
|
||||
*/
|
||||
acceptToParameter: function acceptToParameter(route, classDef, typeRegistry) {
|
||||
var DEFAULT_TYPE =
|
||||
route.verb.toLowerCase() === 'get' ? 'query' : 'formData';
|
||||
|
||||
return function (accepts) {
|
||||
var name = accepts.name || accepts.arg;
|
||||
var paramType = DEFAULT_TYPE;
|
||||
|
||||
// TODO: Regex. This is leaky.
|
||||
if (route.path.indexOf(':' + name) !== -1) {
|
||||
paramType = 'path';
|
||||
}
|
||||
|
||||
// Check the http settings for the argument
|
||||
if (accepts.http && accepts.http.source) {
|
||||
paramType = accepts.http.source;
|
||||
}
|
||||
|
||||
// TODO: ensure that paramType has a valid value
|
||||
// path, query, header, body, formData
|
||||
// See swagger-spec/2.0.md#parameterObject
|
||||
|
||||
var paramObject = {
|
||||
name: name,
|
||||
in: paramType,
|
||||
description: typeConverter.convertText(accepts.description),
|
||||
required: !!accepts.required
|
||||
};
|
||||
|
||||
var schema = schemaBuilder.buildFromLoopBackType(accepts, typeRegistry);
|
||||
if (paramType === 'body') {
|
||||
// HACK: Derive the type from model
|
||||
if (paramObject.name === 'data' && schema.type === 'object') {
|
||||
paramObject.schema = { $ref: typeRegistry.reference(classDef.name) };
|
||||
} else {
|
||||
paramObject.schema = schema;
|
||||
}
|
||||
} else {
|
||||
var isComplexType = schema.type === 'object' ||
|
||||
schema.type === 'array' ||
|
||||
schema.$ref;
|
||||
if (isComplexType) {
|
||||
paramObject.type = 'string';
|
||||
paramObject.format = 'JSON';
|
||||
// TODO support array of primitive types
|
||||
// and map them to Swagger array of primitive types
|
||||
} else {
|
||||
_assign(paramObject, schema);
|
||||
}
|
||||
}
|
||||
|
||||
return paramObject;
|
||||
};
|
||||
},
|
||||
};
|
|
@ -1,170 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var assert = require('assert');
|
||||
var typeConverter = require('./type-converter');
|
||||
|
||||
var TYPES_PRIMITIVE = [
|
||||
'boolean',
|
||||
'integer',
|
||||
'number',
|
||||
'null',
|
||||
'string',
|
||||
'object',
|
||||
'array'
|
||||
];
|
||||
|
||||
var KEY_TRANSLATIONS = {
|
||||
// LDL : Swagger
|
||||
min: 'minimum',
|
||||
max: 'maximum',
|
||||
length: 'maxLength',
|
||||
};
|
||||
|
||||
var SWAGGER_DATA_TYPE_FIELDS = [
|
||||
'format',
|
||||
'default',
|
||||
'enum',
|
||||
'minimum',
|
||||
'minItems',
|
||||
'minLength',
|
||||
'maximum',
|
||||
'maxItems',
|
||||
'maxLength',
|
||||
'uniqueItems',
|
||||
'pattern'
|
||||
];
|
||||
|
||||
/**
|
||||
* Build a Swagger Schema Object and/or Parameter Object from LoopBack
|
||||
* type descriptor.
|
||||
*
|
||||
* @param {String|Function|Array|Object} ldlDef The loopback type to convert,
|
||||
* the value should be one of the following:
|
||||
* - a string value (type name), e.g. `'string'` or `'MyModel'`
|
||||
* - a constructor function, e.g. `String` or `MyModel`
|
||||
* - an array of a single item in `lbType` format
|
||||
* - an object containing a `type` property with string/function/array value
|
||||
* and validation fields like `length` or `max`
|
||||
* @param {TypeRegistry} typeRegistry The registry of known types and models.
|
||||
* @returns {Object} Swagger Schema Object that can be used as `schema` field
|
||||
* or as a base for Parameter Object.
|
||||
*/
|
||||
exports.buildFromLoopBackType = function(ldlDef, typeRegistry) {
|
||||
assert(!!typeRegistry, 'typeRegistry is a required parameter');
|
||||
|
||||
// Normalize non-object values to object format `{ type: XYZ }`
|
||||
if (typeof ldlDef === 'string' || typeof ldlDef === 'function') {
|
||||
ldlDef = { type: ldlDef };
|
||||
} else if (Array.isArray(ldlDef)) {
|
||||
ldlDef = { type: ldlDef };
|
||||
}
|
||||
|
||||
var schema = exports.buildMetadata(ldlDef);
|
||||
var ldlType = exports.getLdlTypeName(ldlDef.type);
|
||||
|
||||
if (Array.isArray(ldlType)) {
|
||||
var itemLdl = ldlType[0] || 'any';
|
||||
var itemSchema = exports.buildFromLoopBackType(itemLdl, typeRegistry);
|
||||
schema.type = 'array';
|
||||
schema.items = itemSchema;
|
||||
return schema;
|
||||
}
|
||||
|
||||
var ldlTypeLowerCase = ldlType.toLowerCase();
|
||||
switch (ldlTypeLowerCase) {
|
||||
case 'date':
|
||||
schema.type = 'string';
|
||||
schema.format = 'date';
|
||||
break;
|
||||
case 'buffer':
|
||||
schema.type = 'string';
|
||||
schema.format = 'byte';
|
||||
break;
|
||||
case 'number':
|
||||
schema.type = 'number';
|
||||
schema.format = schema.format || 'double'; // All JS numbers are doubles
|
||||
break;
|
||||
case 'any':
|
||||
schema.$ref = typeRegistry.reference('x-any');
|
||||
break;
|
||||
default:
|
||||
if (exports.isPrimitiveType(ldlTypeLowerCase)) {
|
||||
schema.type = ldlTypeLowerCase;
|
||||
} else {
|
||||
// TODO - register anonymous types
|
||||
schema.$ref = typeRegistry.reference(ldlType);
|
||||
}
|
||||
}
|
||||
return schema;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {String|Function|Array|Object} ldlType LDL type
|
||||
* @returns {String|Array} Type name
|
||||
*/
|
||||
exports.getLdlTypeName = function(ldlType) {
|
||||
// Value "array" is a shortcut for `['any']`
|
||||
if (ldlType === 'array') {
|
||||
return ['any'];
|
||||
}
|
||||
|
||||
if (typeof ldlType === 'string') {
|
||||
var arrayMatch = ldlType.match(/^\[(.*)\]$/);
|
||||
return arrayMatch ? [arrayMatch[1]] : ldlType;
|
||||
}
|
||||
|
||||
if (typeof ldlType === 'function') {
|
||||
return ldlType.modelName || ldlType.name;
|
||||
}
|
||||
|
||||
if (Array.isArray(ldlType)) {
|
||||
return ldlType;
|
||||
}
|
||||
|
||||
if (typeof ldlType === 'object') {
|
||||
// Anonymous objects, they are allowed e.g. in accepts/returns definitions
|
||||
// TODO(bajtos) Build a named schema for this anonymous object
|
||||
return 'object';
|
||||
}
|
||||
|
||||
if (ldlType === undefined) {
|
||||
return 'any';
|
||||
}
|
||||
|
||||
console.error('Warning: unknown LDL type %j, using "any" instead', ldlType);
|
||||
return 'any';
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert validations and other metadata from LDL format to Swagger format.
|
||||
* @param {Object} ldlDef LDL property/argument definition,
|
||||
* for example `{ type: 'string', maxLength: 64 }`.
|
||||
* @return {Object} Metadata in Swagger format.
|
||||
*/
|
||||
exports.buildMetadata = function(ldlDef) {
|
||||
var result = {};
|
||||
var key;
|
||||
|
||||
for (key in KEY_TRANSLATIONS) {
|
||||
if (key in ldlDef)
|
||||
result[KEY_TRANSLATIONS[key]] = ldlDef[key];
|
||||
}
|
||||
|
||||
for (var ix in SWAGGER_DATA_TYPE_FIELDS) {
|
||||
key = SWAGGER_DATA_TYPE_FIELDS[ix];
|
||||
if (key in ldlDef)
|
||||
result[key] = ldlDef[key];
|
||||
}
|
||||
|
||||
if (ldlDef.description) {
|
||||
result.description = typeConverter.convertText(ldlDef.description);
|
||||
} else if (ldlDef.doc) {
|
||||
result.description = typeConverter.convertText(ldlDef.doc);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
exports.isPrimitiveType = function(typeName) {
|
||||
return TYPES_PRIMITIVE.indexOf(typeName.toLowerCase()) !== -1;
|
||||
};
|
173
lib/swagger.js
173
lib/swagger.js
|
@ -1,173 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
var path = require('path');
|
||||
var _ = require('lodash');
|
||||
var routeHelper = require('./route-helper');
|
||||
var modelHelper = require('./model-helper');
|
||||
var cors = require('cors');
|
||||
var typeConverter = require('./type-converter');
|
||||
var tagBuilder = require('./tag-builder');
|
||||
var TypeRegistry = require('./type-registry');
|
||||
|
||||
/**
|
||||
* Create Swagger Object describing the API provided by loopbacApplication.
|
||||
*
|
||||
* @param {Application} loopbackApplication The application to document.
|
||||
* @param {Object} opts Options.
|
||||
* @returns {Object}
|
||||
*/
|
||||
exports.createSwaggerObject = function(loopbackApplication, opts) {
|
||||
opts = _.defaults(opts || {}, {
|
||||
basePath: loopbackApplication.get('restApiRoot') || '/api',
|
||||
// Default consumes/produces
|
||||
consumes: [
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'application/xml', 'text/xml'
|
||||
],
|
||||
produces: [
|
||||
'application/json',
|
||||
'application/xml', 'text/xml',
|
||||
// JSONP content types
|
||||
'application/javascript', 'text/javascript'
|
||||
],
|
||||
version: getPackagePropertyOrDefault('version', '1.0.0'),
|
||||
});
|
||||
|
||||
// We need a temporary REST adapter to discover our available routes.
|
||||
var remotes = loopbackApplication.remotes();
|
||||
var adapter = remotes.handler('rest').adapter;
|
||||
var routes = adapter.allRoutes();
|
||||
var classes = remotes.classes();
|
||||
|
||||
// Generate fixed fields like info and basePath
|
||||
var swaggerObject = generateSwaggerObjectBase(opts);
|
||||
|
||||
var typeRegistry = new TypeRegistry();
|
||||
var loopbackRegistry = loopbackApplication.registry ||
|
||||
loopbackApplication.loopback.registry ||
|
||||
loopbackApplication.loopback;
|
||||
var models = loopbackRegistry.modelBuilder.models;
|
||||
for (var modelName in models) {
|
||||
modelHelper.registerModelDefinition(models[modelName], typeRegistry);
|
||||
}
|
||||
|
||||
// A class is an endpoint root; e.g. /users, /products, and so on.
|
||||
// In Swagger 2.0, there is no endpoint roots, but one can group endpoints
|
||||
// using tags.
|
||||
classes.forEach(function(aClass) {
|
||||
if (!aClass.name) return;
|
||||
|
||||
var hasDocumentedMethods = aClass.methods().some(function(m) {
|
||||
return m.documented;
|
||||
});
|
||||
if (!hasDocumentedMethods) return;
|
||||
|
||||
swaggerObject.tags.push(tagBuilder.buildTagFromClass(aClass));
|
||||
});
|
||||
|
||||
// A route is an endpoint, such as /users/findOne.
|
||||
routes.forEach(function(route) {
|
||||
if (!route.documented) return;
|
||||
|
||||
// Get the class definition matching this route.
|
||||
var className = route.method.split('.')[0];
|
||||
var classDef = classes.filter(function(item) {
|
||||
return item.name === className;
|
||||
})[0];
|
||||
|
||||
if (!classDef) {
|
||||
console.error('Route exists with no class: %j', route);
|
||||
return;
|
||||
}
|
||||
|
||||
routeHelper.addRouteToSwaggerPaths(route, classDef, typeRegistry,
|
||||
swaggerObject.paths);
|
||||
});
|
||||
|
||||
_.assign(swaggerObject.definitions, typeRegistry.getDefinitions());
|
||||
|
||||
loopbackApplication.emit('swaggerResources', swaggerObject);
|
||||
return swaggerObject;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setup Swagger documentation on the given express app.
|
||||
*
|
||||
* @param {Application} loopbackApplication The loopback application to
|
||||
* document.
|
||||
* @param {Application} swaggerApp Swagger application used for hosting
|
||||
* swagger documentation.
|
||||
* @param {Object} opts Options.
|
||||
*/
|
||||
exports.mountSwagger = function(loopbackApplication, swaggerApp, opts) {
|
||||
var swaggerObject = exports.createSwaggerObject(loopbackApplication, opts);
|
||||
|
||||
var resourcePath = opts && opts.resourcePath || 'swagger.json';
|
||||
if (resourcePath[0] !== '/') resourcePath = '/' + resourcePath;
|
||||
|
||||
var remotes = loopbackApplication.remotes();
|
||||
setupCors(swaggerApp, remotes);
|
||||
|
||||
swaggerApp.get(resourcePath, function sendSwaggerObject(req, res) {
|
||||
res.status(200).send(swaggerObject);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a top-level resource doc. This is the entry point for swagger UI
|
||||
* and lists all of the available APIs.
|
||||
* @param {Object} opts Swagger options.
|
||||
* @return {Object} Resource doc.
|
||||
*/
|
||||
function generateSwaggerObjectBase(opts) {
|
||||
var apiInfo = _.cloneDeep(opts.apiInfo) || {};
|
||||
for (var propertyName in apiInfo) {
|
||||
var property = apiInfo[propertyName];
|
||||
apiInfo[propertyName] = typeConverter.convertText(property);
|
||||
}
|
||||
apiInfo.version = String(apiInfo.version || opts.version);
|
||||
if (!apiInfo.title) {
|
||||
apiInfo.title = getPackagePropertyOrDefault('name', 'LoopBack Application');
|
||||
}
|
||||
|
||||
var basePath = opts.basePath;
|
||||
if (basePath && /\/$/.test(basePath))
|
||||
basePath = basePath.slice(0, -1);
|
||||
|
||||
return {
|
||||
swagger: '2.0',
|
||||
// See swagger-spec/2.0.md#infoObject
|
||||
info: apiInfo,
|
||||
host: opts.host,
|
||||
basePath: basePath,
|
||||
schemes: opts.protocol ? [opts.protocol] : undefined,
|
||||
consumes: opts.consumes,
|
||||
produces: opts.produces,
|
||||
paths: {},
|
||||
definitions: opts.models || {},
|
||||
// TODO Authorizations (security, securityDefinitions)
|
||||
// TODO: responses, externalDocs
|
||||
tags: []
|
||||
};
|
||||
}
|
||||
|
||||
function setupCors(swaggerApp, remotes) {
|
||||
var corsOptions = remotes.options && remotes.options.cors ||
|
||||
{ origin: true, credentials: true };
|
||||
|
||||
// TODO(bajtos) Skip CORS when remotes.options.cors === false
|
||||
swaggerApp.use(cors(corsOptions));
|
||||
}
|
||||
|
||||
function getPackagePropertyOrDefault(name, defautValue) {
|
||||
try {
|
||||
var pkg = require(path.join(process.cwd(), 'package.json'));
|
||||
return pkg[name] || defautValue;
|
||||
} catch(e) {
|
||||
return defautValue;
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var typeConverter = require('./type-converter');
|
||||
|
||||
exports.buildTagFromClass = function(sharedClass) {
|
||||
var name = sharedClass.name;
|
||||
var modelSettings = sharedClass.ctor && sharedClass.ctor.settings;
|
||||
var sharedCtor = sharedClass.ctor && sharedClass.ctor.sharedCtor;
|
||||
|
||||
var description = modelSettings && modelSettings.description ||
|
||||
sharedCtor && sharedCtor.description;
|
||||
|
||||
return {
|
||||
name: name,
|
||||
description: typeConverter.convertText(description),
|
||||
// TODO: externalDocs: { description, url }
|
||||
};
|
||||
};
|
|
@ -1,16 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
|
@ -1,43 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
|
||||
module.exports = TypeRegistry;
|
||||
|
||||
function TypeRegistry() {
|
||||
this._definitions = Object.create(null);
|
||||
this._referenced = Object.create(null);
|
||||
|
||||
this.register('x-any', { properties: {} });
|
||||
// TODO - register GeoPoint and other built-in LoopBack types
|
||||
}
|
||||
|
||||
TypeRegistry.prototype.register = function(typeName, definition) {
|
||||
this._definitions[typeName] = definition;
|
||||
};
|
||||
|
||||
TypeRegistry.prototype.reference = function(typeName) {
|
||||
this._referenced[typeName] = true;
|
||||
return '#/definitions/' + typeName;
|
||||
};
|
||||
|
||||
TypeRegistry.prototype.getDefinitions = function() {
|
||||
var defs = Object.create(null);
|
||||
for (var name in this._referenced) {
|
||||
if (this._definitions[name]) {
|
||||
defs[name] = _.cloneDeep(this._definitions[name]);
|
||||
} else {
|
||||
// https://github.com/strongloop/loopback-explorer/issues/71
|
||||
console.warn('Swagger: skipping unknown type %j.', name);
|
||||
}
|
||||
}
|
||||
return defs;
|
||||
};
|
||||
|
||||
TypeRegistry.prototype.getAllDefinitions = function() {
|
||||
return _.cloneDeep(this._definitions);
|
||||
};
|
||||
|
||||
TypeRegistry.prototype.isDefined = function(typeName) {
|
||||
return typeName in this._definitions;
|
||||
};
|
|
@ -34,6 +34,7 @@
|
|||
"cors": "^2.7.1",
|
||||
"debug": "^2.2.0",
|
||||
"lodash": "^3.10.0",
|
||||
"loopback-swagger": "^2.1.0",
|
||||
"strong-swagger-ui": "^21.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -190,6 +190,36 @@ describe('explorer', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Cross-origin resource sharing', function() {
|
||||
it('allows cross-origin requests by default', function(done) {
|
||||
var app = loopback();
|
||||
configureRestApiAndExplorer(app, '/explorer');
|
||||
|
||||
request(app)
|
||||
.options('/explorer/swagger.json')
|
||||
.set('Origin', 'http://example.com/')
|
||||
.expect('Access-Control-Allow-Origin', /^http:\/\/example.com\/|\*/)
|
||||
.expect('Access-Control-Allow-Methods', /\bGET\b/)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('can be disabled by configuration', function(done) {
|
||||
var app = loopback();
|
||||
app.set('remoting', { cors: { origin: false } });
|
||||
configureRestApiAndExplorer(app, '/explorer');
|
||||
|
||||
request(app)
|
||||
.options('/explorer/swagger.json')
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
var allowOrigin = res.get('Access-Control-Allow-Origin');
|
||||
expect(allowOrigin, 'Access-Control-Allow-Origin')
|
||||
.to.equal(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function givenLoopBackAppWithExplorer(explorerBase) {
|
||||
return function(done) {
|
||||
var app = this.app = loopback();
|
||||
|
|
|
@ -1,193 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var modelHelper = require('../lib/model-helper');
|
||||
var TypeRegistry = require('../lib/type-registry');
|
||||
var _defaults = require('lodash').defaults;
|
||||
var loopback = require('loopback');
|
||||
var expect = require('chai').expect;
|
||||
|
||||
describe('model-helper', function() {
|
||||
describe('related models', function() {
|
||||
it('should include related models', function() {
|
||||
var defs = buildSwaggerModelsWithRelations({
|
||||
str: String // 'string'
|
||||
});
|
||||
expect(defs).has.property('testModel');
|
||||
expect(defs).has.property('relatedModel');
|
||||
});
|
||||
|
||||
it('should include nesting models', function() {
|
||||
var Model2 = loopback.createModel('Model2', {street: String});
|
||||
var Model1 = loopback.createModel('Model1', {
|
||||
str: String, // 'string'
|
||||
address: Model2
|
||||
}, { models: { Model2: Model2 } });
|
||||
var defs = getDefinitionsForModel(Model1);
|
||||
expect(defs).has.property('Model1');
|
||||
expect(defs).has.property('Model2');
|
||||
});
|
||||
|
||||
it('should include used models', function() {
|
||||
var Model4 = loopback.createModel('Model4', {street: String});
|
||||
var Model3 = loopback.createModel('Model3', {
|
||||
str: String // 'string'
|
||||
}, {models: {model4: 'Model4'}});
|
||||
var defs = getDefinitionsForModel(Model3);
|
||||
expect(defs).has.property('Model3');
|
||||
expect(defs).has.property('Model4');
|
||||
});
|
||||
|
||||
it('should include nesting models in array', function() {
|
||||
var Model6 = loopback.createModel('Model6', {street: String});
|
||||
var Model5 = loopback.createModel('Model5', {
|
||||
str: String, // 'string'
|
||||
addresses: [Model6]
|
||||
}, { models: { Model6: Model6 } });
|
||||
var defs = getDefinitionsForModel(Model5);
|
||||
expect(defs).has.property('Model5');
|
||||
expect(defs).has.property('Model6');
|
||||
});
|
||||
|
||||
// https://github.com/strongloop/loopback-explorer/issues/49
|
||||
it('should work if Array class is extended and no related models are found',
|
||||
function() {
|
||||
var Model7 = loopback.createModel('Model7', {street: String});
|
||||
Array.prototype.customFunc = function() {
|
||||
};
|
||||
var defs = getDefinitionsForModel(Model7);
|
||||
expect(defs).has.property('Model7');
|
||||
expect(Object.keys(defs)).has.property('length', 1);
|
||||
});
|
||||
|
||||
// https://github.com/strongloop/loopback-explorer/issues/71
|
||||
it('should skip unknown types', function() {
|
||||
var Model8 = loopback.createModel('Model8', {
|
||||
patient: {
|
||||
model: 'physician',
|
||||
type: 'hasMany',
|
||||
through: 'appointment'
|
||||
}
|
||||
});
|
||||
var defs = getDefinitionsForModel(Model8);
|
||||
// Hack: prevent warnings in other tests caused by global model registry
|
||||
Model8.definition.rawProperties.patient.type = 'string';
|
||||
Model8.definition.properties.patient.type = 'string';
|
||||
|
||||
expect(Object.keys(defs)).to.not.contain('hasMany');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hidden properties', function() {
|
||||
it('should hide properties marked as "hidden"', function() {
|
||||
var aClass = createModelCtor({
|
||||
visibleProperty: 'string',
|
||||
hiddenProperty: 'string'
|
||||
});
|
||||
aClass.ctor.definition.settings = {
|
||||
hidden: ['hiddenProperty']
|
||||
};
|
||||
var def = getDefinitionsForModel(aClass.ctor).testModel;
|
||||
expect(def.properties).to.not.have.property('hiddenProperty');
|
||||
expect(def.properties).to.have.property('visibleProperty');
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert top level array description to string', function() {
|
||||
var model = {};
|
||||
model.definition = {
|
||||
name: 'test',
|
||||
description: ['1', '2', '3'],
|
||||
properties: {}
|
||||
};
|
||||
var defs = getDefinitionsForModel(model);
|
||||
expect(defs.test.description).to.equal('1\n2\n3');
|
||||
});
|
||||
|
||||
it('should convert property level array description to string', function() {
|
||||
var model = {};
|
||||
model.definition = {
|
||||
name: 'test',
|
||||
properties: {
|
||||
prop1: {
|
||||
type: 'string',
|
||||
description: ['1', '2', '3']
|
||||
}
|
||||
}
|
||||
};
|
||||
var defs = getDefinitionsForModel(model);
|
||||
expect(defs.test.properties.prop1.description).to.equal('1\n2\n3');
|
||||
});
|
||||
|
||||
it('omits empty "required" array', function() {
|
||||
var aClass = createModelCtor({});
|
||||
var def = getDefinitionsForModel(aClass.ctor).testModel;
|
||||
expect(def).to.not.have.property('required');
|
||||
});
|
||||
});
|
||||
|
||||
// Simulates the format of a remoting class.
|
||||
function buildSwaggerModels(modelProperties, modelOptions) {
|
||||
var aClass = createModelCtor(modelProperties, modelOptions);
|
||||
return modelHelper.generateModelDefinition(aClass.ctor, {}).testModel;
|
||||
}
|
||||
|
||||
function createModelCtor(properties, modelOptions) {
|
||||
Object.keys(properties).forEach(function(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 = {
|
||||
ctor: {
|
||||
definition: definition
|
||||
}
|
||||
};
|
||||
return aClass;
|
||||
}
|
||||
|
||||
function buildSwaggerModelsWithRelations(model) {
|
||||
Object.keys(model).forEach(function(name) {
|
||||
model[name] = {type: model[name]};
|
||||
});
|
||||
// Mock up the related model
|
||||
var relatedModel = {
|
||||
definition: {
|
||||
name: 'relatedModel',
|
||||
properties: {
|
||||
fk: String
|
||||
}
|
||||
}
|
||||
};
|
||||
var aClass = {
|
||||
ctor: {
|
||||
definition: {
|
||||
name: 'testModel',
|
||||
properties: model
|
||||
},
|
||||
// Mock up relations
|
||||
relations: {
|
||||
other: {
|
||||
modelTo: relatedModel
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var registry = new TypeRegistry();
|
||||
modelHelper.registerModelDefinition(aClass.ctor, registry);
|
||||
return registry.getAllDefinitions();
|
||||
}
|
||||
|
||||
function getDefinitionsForModel(modelCtor) {
|
||||
var registry = new TypeRegistry();
|
||||
modelHelper.registerModelDefinition(modelCtor, registry);
|
||||
registry.reference(modelCtor.modelName || modelCtor.definition.name);
|
||||
return registry.getDefinitions();
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var routeHelper = require('../lib/route-helper');
|
||||
var TypeRegistry = require('../lib/type-registry');
|
||||
var expect = require('chai').expect;
|
||||
var _defaults = require('lodash').defaults;
|
||||
|
||||
describe('route-helper', function() {
|
||||
it('returns "object" when a route has multiple return values', function() {
|
||||
var entry = createAPIDoc({
|
||||
returns: [
|
||||
{ arg: 'max', type: 'number' },
|
||||
{ arg: 'min', type: 'number' },
|
||||
{ arg: 'avg', type: 'number' }
|
||||
]
|
||||
});
|
||||
// TODO use a custom (dynamicaly-created) model schema instead of "object"
|
||||
expect(getResponseMessage(entry.operation))
|
||||
.to.have.property('schema').eql({ type: 'object' });
|
||||
});
|
||||
|
||||
it('converts path params when they exist in the route name', function() {
|
||||
var entry = createAPIDoc({
|
||||
accepts: [
|
||||
{arg: 'id', type: 'string'}
|
||||
],
|
||||
path: '/test/:id'
|
||||
});
|
||||
var paramDoc = entry.operation.parameters[0];
|
||||
expect(paramDoc).to.have.property('in', 'path');
|
||||
expect(paramDoc).to.have.property('name', 'id');
|
||||
expect(paramDoc).to.have.property('required', false);
|
||||
});
|
||||
|
||||
// FIXME need regex in routeHelper.acceptToParameter
|
||||
xit('won\'t convert path params when they don\'t exist in the route name', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [
|
||||
{arg: 'id', type: 'string'}
|
||||
],
|
||||
path: '/test/:identifier'
|
||||
});
|
||||
var paramDoc = doc.operation.parameters[0];
|
||||
expect(paramDoc.in).to.equal('query');
|
||||
});
|
||||
|
||||
it('correctly coerces param types', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [
|
||||
{arg: 'binaryData', type: 'buffer'}
|
||||
]
|
||||
});
|
||||
var paramDoc = doc.operation.parameters[0];
|
||||
expect(paramDoc).to.have.property('in', 'query');
|
||||
expect(paramDoc).to.have.property('type', 'string');
|
||||
expect(paramDoc).to.have.property('format', 'byte');
|
||||
});
|
||||
|
||||
it('correctly converts return types (arrays)', function() {
|
||||
var doc = createAPIDoc({
|
||||
returns: [
|
||||
{ arg: 'data', type: ['customType'], root: true }
|
||||
]
|
||||
});
|
||||
var opDoc = doc.operation;
|
||||
|
||||
var responseSchema = getResponseMessage(opDoc).schema;
|
||||
expect(responseSchema).to.have.property('type', 'array');
|
||||
expect(responseSchema).to.have.property('items')
|
||||
.eql({ $ref: '#/definitions/customType' });
|
||||
});
|
||||
|
||||
it('correctly converts return types (format)', function() {
|
||||
var doc = createAPIDoc({
|
||||
returns: [
|
||||
{ arg: 'data', type: 'buffer', root: true }
|
||||
]
|
||||
});
|
||||
|
||||
var responseSchema = getResponseMessage(doc.operation).schema;
|
||||
expect(responseSchema.type).to.equal('string');
|
||||
expect(responseSchema.format).to.equal('byte');
|
||||
});
|
||||
|
||||
it('includes `notes` metadata as `description`', function() {
|
||||
var doc = createAPIDoc({
|
||||
notes: 'some notes'
|
||||
});
|
||||
expect(doc.operation).to.have.property('description', 'some notes');
|
||||
});
|
||||
|
||||
describe('#acceptToParameter', function() {
|
||||
var A_CLASS_DEF = { name: 'TestModelName' };
|
||||
|
||||
it('returns fn converting description from array to string', function() {
|
||||
var f = routeHelper.acceptToParameter(
|
||||
{verb: 'get', path: 'path'},
|
||||
A_CLASS_DEF,
|
||||
new TypeRegistry());
|
||||
var result = f({description: ['1', '2', '3']});
|
||||
expect(result.description).to.eql('1\n2\n3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#routeToPathEntry', function() {
|
||||
it('converts route.description from array to string', function() {
|
||||
var result = routeHelper.routeToPathEntry({
|
||||
method: 'someMethod',
|
||||
verb: 'get',
|
||||
path: 'path',
|
||||
description: ['1', '2', '3']
|
||||
});
|
||||
expect(result.operation.summary).to.eql('1\n2\n3');
|
||||
});
|
||||
|
||||
it('converts route.notes from array of string to string', function() {
|
||||
var result = routeHelper.routeToPathEntry({
|
||||
method: 'someMethod',
|
||||
verb: 'get',
|
||||
path: 'path',
|
||||
notes: ['1', '2', '3']
|
||||
});
|
||||
expect(result.operation.description).to.eql("1\n2\n3");
|
||||
});
|
||||
});
|
||||
|
||||
it('includes `deprecated` metadata', function() {
|
||||
var doc = createAPIDoc({
|
||||
deprecated: 'true'
|
||||
});
|
||||
expect(doc.operation).to.have.property('deprecated', true);
|
||||
});
|
||||
|
||||
it('joins array description/summary', function() {
|
||||
var doc = createAPIDoc({
|
||||
description: [ 'line1', 'line2' ]
|
||||
});
|
||||
expect(doc.operation.summary).to.equal('line1\nline2');
|
||||
});
|
||||
|
||||
it('joins array notes', function() {
|
||||
var doc = createAPIDoc({
|
||||
notes: [ 'line1', 'line2' ]
|
||||
});
|
||||
expect(doc.operation.description).to.equal('line1\nline2');
|
||||
});
|
||||
|
||||
it('joins array description/summary of an input arg', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [{ name: 'arg', description: [ 'line1', 'line2' ] }]
|
||||
});
|
||||
expect(doc.operation.parameters[0].description).to.equal('line1\nline2');
|
||||
});
|
||||
|
||||
it('correctly does not include context params', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [
|
||||
{arg: 'ctx', http: {source: 'context'}}
|
||||
],
|
||||
path: '/test'
|
||||
});
|
||||
var params = doc.operation.parameters;
|
||||
expect(params.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('correctly does not include request params', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [
|
||||
{arg: 'req', http: {source: 'req'}}
|
||||
],
|
||||
path: '/test'
|
||||
});
|
||||
var params = doc.operation.parameters;
|
||||
expect(params.length).to.equal(0);
|
||||
});
|
||||
|
||||
it('correctly does not include response params', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [
|
||||
{arg: 'res', http: {source: 'res'}}
|
||||
],
|
||||
path: '/test'
|
||||
});
|
||||
var params = doc.operation.parameters;
|
||||
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.operation.parameters[0])
|
||||
.to.have.property('enum').eql([1,2,3]);
|
||||
});
|
||||
|
||||
it('includes the default response message with code 200', function() {
|
||||
var doc = createAPIDoc({
|
||||
returns: [{ name: 'result', type: 'object', root: true }]
|
||||
});
|
||||
expect(doc.operation.responses).to.eql({
|
||||
200: {
|
||||
description: 'Request was successful',
|
||||
schema: { type: 'object' }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the response code 204 when `returns` is empty', function() {
|
||||
var doc = createAPIDoc({
|
||||
returns: []
|
||||
});
|
||||
expect(doc.operation.responses).to.eql({
|
||||
204: {
|
||||
description: 'Request was successful',
|
||||
schema: undefined
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('includes custom error response in `responseMessages`', function() {
|
||||
var doc = createAPIDoc({
|
||||
errors: [{
|
||||
code: 422,
|
||||
message: 'Validation failed',
|
||||
responseModel: 'ValidationError'
|
||||
}]
|
||||
});
|
||||
expect(doc.operation.responses).to.have.property(422).eql({
|
||||
description: 'Validation failed',
|
||||
schema: { $ref: '#/definitions/ValidationError' }
|
||||
});
|
||||
});
|
||||
|
||||
it('route operationId DOES include model name.', function() {
|
||||
var doc = createAPIDoc({ method: 'User.login' });
|
||||
expect(doc.operation.operationId).to.equal('User.login');
|
||||
});
|
||||
|
||||
it('adds class name to `tags`', function() {
|
||||
var doc = createAPIDoc(
|
||||
{ method: 'User.login' },
|
||||
{ name: 'User' });
|
||||
expect(doc.operation.tags).to.contain('User');
|
||||
});
|
||||
|
||||
it('converts non-primitive param types to JSON strings', function() {
|
||||
var doc = createAPIDoc({
|
||||
accepts: [{arg: 'filter', type: 'object', http: { source: 'query' }}]
|
||||
});
|
||||
var param = doc.operation.parameters[0];
|
||||
expect(param).to.have.property('type', 'string');
|
||||
expect(param).to.have.property('format', 'JSON');
|
||||
});
|
||||
|
||||
it('converts single "data" body arg to Model type', function() {
|
||||
var doc = createAPIDoc(
|
||||
{
|
||||
accepts: [{arg: 'data', type: 'object', http: { source: 'body' }}],
|
||||
},
|
||||
{ name: 'User' });
|
||||
var param = doc.operation.parameters[0];
|
||||
expect(param)
|
||||
.to.have.property('schema')
|
||||
.eql({ $ref: '#/definitions/User' });
|
||||
});
|
||||
});
|
||||
|
||||
// Easy wrapper around createRoute
|
||||
function createAPIDoc(def, classDef) {
|
||||
return routeHelper.routeToPathEntry(_defaults(def || {}, {
|
||||
path: '/test',
|
||||
verb: 'GET',
|
||||
method: 'test.get'
|
||||
}), classDef, new TypeRegistry());
|
||||
}
|
||||
|
||||
function getResponseMessage(operationDoc) {
|
||||
return operationDoc.responses[200] || operationDoc.responses[204]
|
||||
|| operationDoc.responses.default;
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var schemaBuilder = require('../lib/schema-builder');
|
||||
var TypeRegistry = require('../lib/type-registry');
|
||||
var format = require('util').format;
|
||||
var _defaults = require('lodash').defaults;
|
||||
var loopback = require('loopback');
|
||||
var expect = require('chai').expect;
|
||||
|
||||
var ANY_TYPE = { $ref: '#/definitions/x-any' };
|
||||
|
||||
describe('schema-builder', function() {
|
||||
describeTestCases('for constructor types', [
|
||||
{ in: String, out: { type: 'string' } },
|
||||
{ in: Number, out: { type: 'number', format: 'double' } },
|
||||
{ in: Date, out: { type: 'string', format: 'date' } },
|
||||
{ in: Boolean, out: { type: 'boolean' } },
|
||||
{ in: Buffer, out: { type: 'string', format: 'byte' } }
|
||||
]);
|
||||
|
||||
describeTestCases('for string types', [
|
||||
{ in: 'string', out: { type: 'string' } },
|
||||
{ in: 'number', out: { type: 'number', format: 'double' } },
|
||||
{ in: 'date', out: { type: 'string', format: 'date' } },
|
||||
{ in: 'boolean', out: { type: 'boolean' } },
|
||||
{ in: 'buffer', out: { type: 'string', format: 'byte' } },
|
||||
]);
|
||||
|
||||
describeTestCases('for array definitions', [
|
||||
{ in: [String],
|
||||
out: { type: 'array', items: { type: 'string' } } },
|
||||
{ in: ['string'],
|
||||
out: { type: 'array', items: { type: 'string' } } },
|
||||
{ in: [{ type: 'string', maxLength: 64 }],
|
||||
out: { type: 'array', items: { type: 'string', maxLength: 64 } } },
|
||||
{ in: [{ type: 'date' }],
|
||||
out: { type: 'array', items: { type: 'string', format: 'date' } } },
|
||||
{ in: [],
|
||||
out: { type: 'array', items: ANY_TYPE } },
|
||||
// This value is somehow provided by loopback-boot called from
|
||||
// loopback-workspace.
|
||||
{ in: [undefined],
|
||||
out: { type: 'array', items: ANY_TYPE } },
|
||||
{ in: 'array',
|
||||
out: { type: 'array', items: ANY_TYPE } },
|
||||
]);
|
||||
|
||||
describeTestCases('for complex types', [
|
||||
// Note: User is a built-in loopback model
|
||||
{ in: loopback.User,
|
||||
out: { $ref: '#/definitions/User' } },
|
||||
{ in: { type: 'User' },
|
||||
out: { $ref: '#/definitions/User' } },
|
||||
// Anonymous type
|
||||
{ in: { type: { foo: 'string', bar: 'number' } },
|
||||
out: { type: 'object' } },
|
||||
]);
|
||||
|
||||
describeTestCases('for extra metadata', [
|
||||
{ in: { type: String, doc: 'a-description' },
|
||||
out: { type: 'string', description: 'a-description' } },
|
||||
{ in: { type: String, doc: ['line1', 'line2'] },
|
||||
out: { type: 'string', description: 'line1\nline2' } },
|
||||
{ in: { type: String, description: 'a-description' },
|
||||
out: { type: 'string', description: 'a-description' } },
|
||||
{ in: { type: String, description: ['line1', 'line2'] },
|
||||
out: { type: 'string', description: 'line1\nline2' } },
|
||||
{ in: { type: String, required: true },
|
||||
out: { type: 'string' } }, // the flag required is handled specially
|
||||
{ in: { type: String, length: 10 },
|
||||
out: { type: 'string', maxLength: 10 } },
|
||||
]);
|
||||
|
||||
function describeTestCases(name, testCases) {
|
||||
describe(name, function() {
|
||||
testCases.forEach(function(tc) {
|
||||
var inStr = formatType(tc.in);
|
||||
var outStr = formatType(tc.out);
|
||||
it(format('converts %s to %s', inStr, outStr), function() {
|
||||
var registry = new TypeRegistry();
|
||||
var schema = schemaBuilder.buildFromLoopBackType(tc.in, registry);
|
||||
expect(schema).to.eql(tc.out);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function formatType(type) {
|
||||
if (Array.isArray(type))
|
||||
return '[' + type.map(formatType) + ']';
|
||||
|
||||
if (typeof type === 'function')
|
||||
return type.modelName ?
|
||||
'model ' + type.modelName :
|
||||
'ctor ' + type.name;
|
||||
|
||||
return format(type);
|
||||
}
|
||||
|
||||
});
|
|
@ -1,410 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var url = require('url');
|
||||
var urlJoin = require('../lib/url-join');
|
||||
var loopback = require('loopback');
|
||||
var swagger = require('../lib/swagger');
|
||||
|
||||
var request = require('supertest');
|
||||
var expect = require('chai').expect;
|
||||
|
||||
describe('swagger definition', function() {
|
||||
describe('defaults', function() {
|
||||
var swaggerResource;
|
||||
before(function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
swaggerResource = swagger.createSwaggerObject(app);
|
||||
});
|
||||
|
||||
it('advertises Swagger Spec version 2.0', function() {
|
||||
expect(swaggerResource).to.have.property('swagger', '2.0');
|
||||
});
|
||||
|
||||
it('has "basePath" set to "/api"', function() {
|
||||
expect(swaggerResource).to.have.property('basePath', '/api');
|
||||
});
|
||||
|
||||
it('uses the "host" serving the documentation', function() {
|
||||
// see swagger-spec/2.0.md#fixed-fields
|
||||
// If the host is not included, the host serving the documentation is to
|
||||
// be used (including the port).
|
||||
expect(swaggerResource).to.have.property('host', undefined);
|
||||
});
|
||||
|
||||
it('uses the "schemes" serving the documentation', function() {
|
||||
// see swagger-spec/2.0.md#fixed-fields
|
||||
// If the schemes is not included, the default scheme to be used is the
|
||||
// one used to access the Swagger definition itself.
|
||||
expect(swaggerResource).to.have.property('schemes', undefined);
|
||||
});
|
||||
|
||||
it('provides info.title', function() {
|
||||
expect(swaggerResource.info)
|
||||
.to.have.property('title', 'loopback-explorer');
|
||||
});
|
||||
});
|
||||
|
||||
describe('basePath', function() {
|
||||
it('is "{basePath}" when basePath is a path', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app, {
|
||||
basePath: '/api-root'
|
||||
});
|
||||
|
||||
expect(swaggerResource.basePath).to.equal('/api-root');
|
||||
});
|
||||
|
||||
it('is inferred from app.get("apiRoot")', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
app.set('restApiRoot', '/custom-api-root');
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(swaggerResource.basePath).to.equal('/custom-api-root');
|
||||
});
|
||||
|
||||
it('is reachable when explorer mounting location is changed',
|
||||
function(done) {
|
||||
var explorerRoot = '/erforscher';
|
||||
var app = givenAppWithSwagger({}, {explorerRoot: explorerRoot});
|
||||
|
||||
getSwaggerResource(app, explorerRoot).end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
expect(res.body.basePath).to.be.a('string');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('respects a hardcoded protocol (behind SSL terminator)', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app, {
|
||||
protocol: 'https'
|
||||
});
|
||||
expect(swaggerResource.schemes).to.eql(['https']);
|
||||
});
|
||||
|
||||
it('supports opts.host', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app, {
|
||||
host: 'example.com:8080'
|
||||
});
|
||||
expect(swaggerResource.host).to.equal('example.com:8080');
|
||||
});
|
||||
});
|
||||
|
||||
it('has global "consumes"', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(swaggerResource.consumes).to.have.members([
|
||||
'application/json',
|
||||
'application/x-www-form-urlencoded',
|
||||
'application/xml', 'text/xml'
|
||||
]);
|
||||
});
|
||||
|
||||
it('has global "produces"', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(swaggerResource.produces).to.have.members([
|
||||
'application/json',
|
||||
'application/xml', 'text/xml',
|
||||
// JSONP content types
|
||||
'application/javascript', 'text/javascript'
|
||||
]);
|
||||
});
|
||||
|
||||
describe('tags', function() {
|
||||
it('has one tag for each model', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(swaggerResource.tags).to.eql([
|
||||
{ name: 'Product', description: 'a-description\nline2' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('paths node', function() {
|
||||
it('contains model routes for static methods', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(swaggerResource.paths).to.have.property('/Products');
|
||||
var products = swaggerResource.paths['/Products'];
|
||||
var verbs = Object.keys(products);
|
||||
verbs.sort();
|
||||
expect(verbs).to.eql(['get', 'post', 'put']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('definitions node', function() {
|
||||
it('properly defines basic attributes', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
var data = swaggerResource.definitions.Product;
|
||||
expect(data.required.sort()).to.eql(['aNum', 'foo'].sort());
|
||||
expect(data.properties.foo.type).to.equal('string');
|
||||
expect(data.properties.bar.type).to.equal('string');
|
||||
expect(data.properties.aNum.type).to.equal('number');
|
||||
// These will be Numbers for Swagger 2.0
|
||||
expect(data.properties.aNum.minimum).to.equal(1);
|
||||
expect(data.properties.aNum.maximum).to.equal(10);
|
||||
// Should be Number even in 1.2
|
||||
expect(data.properties.aNum.default).to.equal(5);
|
||||
});
|
||||
|
||||
it('includes models from "accepts" args', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenPrivateAppModel(app, 'Image');
|
||||
givenSharedMethod(app.models.Product, 'setImage', {
|
||||
accepts: { name: 'image', type: 'Image' }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions)).to.include('Image');
|
||||
});
|
||||
|
||||
it('includes models from "returns" args', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenPrivateAppModel(app, 'Image');
|
||||
givenSharedMethod(app.models.Product, 'getImage', {
|
||||
returns: { name: 'image', type: 'Image', root: true }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions)).to.include('Image');
|
||||
});
|
||||
|
||||
it('includes "accepts" models not attached to the app', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
loopback.createModel('Image');
|
||||
givenSharedMethod(app.models.Product, 'setImage', {
|
||||
accepts: { name: 'image', type: 'Image' }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions)).to.include('Image');
|
||||
});
|
||||
|
||||
it('includes "responseMessages" models', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
loopback.createModel('ValidationError');
|
||||
givenSharedMethod(app.models.Product, 'setImage', {
|
||||
errors: [{
|
||||
code: '422',
|
||||
message: 'Validation failed',
|
||||
responseModel: 'ValidationError'
|
||||
}]
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include('ValidationError');
|
||||
});
|
||||
|
||||
it('includes nested model references in properties', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
app.models.Product.defineProperty('location', { type: 'Warehouse' });
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested array model references in properties', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
app.models.Product.defineProperty('location', { type: ['Warehouse'] });
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested model references in modelTo relation', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
app.models.Product.belongsTo(app.models.Warehouse);
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested model references in modelThrough relation', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
givenPrivateAppModel(app, 'ProductLocations');
|
||||
|
||||
app.models.Product.hasMany(app.models.Warehouse,
|
||||
{ through: app.models.ProductLocations });
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse', 'ProductLocations']);
|
||||
});
|
||||
|
||||
it('includes nested model references in accept args', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
givenSharedMethod(app.models.Product, 'aMethod', {
|
||||
accepts: { arg: 'w', type: 'Warehouse' }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested array model references in accept args', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
givenSharedMethod(app.models.Product, 'aMethod', {
|
||||
accepts: { arg: 'w', type: ['Warehouse'] }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested model references in return args', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
givenSharedMethod(app.models.Product, 'aMethod', {
|
||||
returns: { arg: 'w', type: 'Warehouse', root: true }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested array model references in return args', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
givenSharedMethod(app.models.Product, 'aMethod', {
|
||||
returns: { arg: 'w', type: ['Warehouse'], root: true }
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
|
||||
it('includes nested model references in error responses', function() {
|
||||
var app = createLoopbackAppWithModel();
|
||||
givenWarehouseWithAddressModels(app);
|
||||
|
||||
givenSharedMethod(app.models.Product, 'aMethod', {
|
||||
errors: {
|
||||
code: '222',
|
||||
message: 'Warehouse',
|
||||
responseModel: 'Warehouse'
|
||||
}
|
||||
});
|
||||
|
||||
var swaggerResource = swagger.createSwaggerObject(app);
|
||||
expect(Object.keys(swaggerResource.definitions))
|
||||
.to.include.members(['Address', 'Warehouse']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cross-origin resource sharing', function() {
|
||||
it('allows cross-origin requests by default', function(done) {
|
||||
var app = givenAppWithSwagger();
|
||||
request(app)
|
||||
.options('/explorer/swagger.json')
|
||||
.set('Origin', 'http://example.com/')
|
||||
.expect('Access-Control-Allow-Origin', /^http:\/\/example.com\/|\*/)
|
||||
.expect('Access-Control-Allow-Methods', /\bGET\b/)
|
||||
.end(done);
|
||||
});
|
||||
|
||||
it('can be disabled by configuration', function(done) {
|
||||
var app = givenAppWithSwagger({}, {
|
||||
remoting: { cors: { origin: false } }
|
||||
});
|
||||
request(app)
|
||||
.options('/explorer/swagger.json')
|
||||
.end(function(err, res) {
|
||||
if (err) return done(err);
|
||||
var allowOrigin = res.get('Access-Control-Allow-Origin');
|
||||
expect(allowOrigin, 'Access-Control-Allow-Origin')
|
||||
.to.equal(undefined);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getSwaggerResource(app, restPath, classPath) {
|
||||
if (classPath) throw new Error('classPath is no longer supported');
|
||||
return request(app)
|
||||
.get(urlJoin(restPath || '/explorer', '/swagger.json'))
|
||||
.set('Accept', 'application/json')
|
||||
.expect(200)
|
||||
.expect('Content-Type', /json/);
|
||||
}
|
||||
|
||||
function getAPIDeclaration(app, className) {
|
||||
return getSwaggerResource(app, '', urlJoin('/', className));
|
||||
}
|
||||
|
||||
function givenAppWithSwagger(swaggerOptions, appConfig) {
|
||||
appConfig = appConfig || {};
|
||||
var app = createLoopbackAppWithModel(appConfig.apiRoot);
|
||||
|
||||
if (appConfig.remoting) app.set('remoting', appConfig.remoting);
|
||||
if (appConfig.explorerRoot) app.set('explorerRoot', appConfig.explorerRoot);
|
||||
|
||||
mountExplorer(app, swaggerOptions);
|
||||
return app;
|
||||
}
|
||||
|
||||
function mountExplorer(app, options) {
|
||||
var swaggerApp = loopback();
|
||||
swagger.mountSwagger(app, swaggerApp, options);
|
||||
app.use(app.get('explorerRoot') || '/explorer', swaggerApp);
|
||||
return app;
|
||||
}
|
||||
|
||||
function createLoopbackAppWithModel(apiRoot) {
|
||||
var app = loopback();
|
||||
|
||||
app.dataSource('db', { connector: 'memory' });
|
||||
|
||||
var Product = loopback.createModel('Product', {
|
||||
foo: {type: 'string', required: true},
|
||||
bar: 'string',
|
||||
aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}
|
||||
}, { description: ['a-description', 'line2'] });
|
||||
app.model(Product, { dataSource: 'db' });
|
||||
|
||||
// Simulate a restApiRoot set in config
|
||||
app.set('restApiRoot', apiRoot || '/api');
|
||||
app.use(app.get('restApiRoot'), loopback.rest());
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function givenSharedMethod(model, name, metadata) {
|
||||
model[name] = function() {};
|
||||
loopback.remoteMethod(model[name], metadata);
|
||||
}
|
||||
|
||||
function givenPrivateAppModel(app, name, properties) {
|
||||
var model = loopback.createModel(name, properties);
|
||||
app.model(model, { dataSource: 'db', public: false });
|
||||
}
|
||||
|
||||
function givenWarehouseWithAddressModels(app) {
|
||||
givenPrivateAppModel(app, 'Address');
|
||||
givenPrivateAppModel(app, 'Warehouse', {
|
||||
shippingAddress: { type: 'Address' }
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
var tagBuilder = require('../lib/tag-builder');
|
||||
var expect = require('chai').expect;
|
||||
var _defaults = require('lodash').defaults;
|
||||
|
||||
describe('tag-builder', function() {
|
||||
it('joins array descriptions from ctor.settings', function() {
|
||||
var tag = tagBuilder.buildTagFromClass({
|
||||
ctor: { settings: { description: ['line1', 'line2'] } }
|
||||
});
|
||||
|
||||
expect(tag.description).to.equal('line1\nline2');
|
||||
});
|
||||
|
||||
it('joins array descriptions from ctor.sharedCtor', function() {
|
||||
var tag = tagBuilder.buildTagFromClass({
|
||||
ctor: { sharedCtor: { description: ['1', '2', '3'] } }
|
||||
});
|
||||
|
||||
expect(tag.description).to.eql('1\n2\n3');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue