Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions lib/model-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,12 @@ var modelHelper = module.exports = {
if (typeof propType === 'function') {
// See https://github.com/strongloop/loopback-explorer/issues/32
// The type can be a model class
propType = propType.modelName || propType.name.toLowerCase();
} else if(Array.isArray(propType)) {
propType = 'array';
return propType.modelName || propType.name.toLowerCase();
} else if (Array.isArray(propType)) {
return 'array';
} else if (typeof propType === 'object') {
// Anonymous objects, they are allowed e.g. in accepts/returns definitions
return 'object';
}
return propType;
},
Expand Down
33 changes: 26 additions & 7 deletions lib/route-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,21 +123,40 @@ var routeHelper = module.exports = {

debug('route %j', route);

var responseDoc = modelHelper.LDLPropToSwaggerDataType(returns);

// Note: Swagger Spec does not provide a way how to specify
// that the responseModel is "array of X". However,
// Swagger UI converts Arrays to the item types anyways,
// therefore it should be ok to do the same here.
var responseModel = responseDoc.type === 'array' ?
responseDoc.items.type : responseDoc.type;

var responseMessages = [{
code: route.returns && route.returns.length ? 200 : 204,
message: 'Request was successful',
responseModel: responseModel
}];

if (route.errors) {
responseMessages.push.apply(responseMessages, route.errors);
}

var apiDoc = {
path: routeHelper.convertPathFragments(route.path),
// Create the operation doc. Use `extendWithType` to add the necessary
// `items` and `format` fields.
operations: [routeHelper.extendWithType({
// Create the operation doc.
// Note that we are not calling `extendWithType`, as the response type
// is specified in the first response message.
operations: [{
method: routeHelper.convertVerb(route.verb),
// [rfeng] Swagger UI doesn't escape '.' for jQuery selector
nickname: route.method.replace(/\./g, '_'),
nickname: route.method.replace(/\./g, '_'),
parameters: accepts,
// TODO(schoon) - We don't have descriptions for this yet.
responseMessages: [],
responseMessages: responseMessages,
summary: typeConverter.convertText(route.description),
notes: typeConverter.convertText(route.notes),
deprecated: route.deprecated
}, returns)]
}]
};

return apiDoc;
Expand Down
55 changes: 51 additions & 4 deletions lib/swagger.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var urlJoin = require('./url-join');
var _defaults = require('lodash.defaults');
var classHelper = require('./class-helper');
var routeHelper = require('./route-helper');
var modelHelper = require('./model-helper');
var cors = require('cors');

/**
Expand All @@ -23,6 +24,9 @@ var cors = require('cors');
* @param {Object} opts Options.
*/
function Swagger(loopbackApplication, swaggerApp, opts) {
if (opts && opts.swaggerVersion)
console.warn('loopback-explorer\'s options.swaggerVersion is deprecated.');

opts = _defaults(opts || {}, {
swaggerVersion: '1.2',
basePath: loopbackApplication.get('restApiRoot') || '/api',
Expand Down Expand Up @@ -81,6 +85,47 @@ function Swagger(loopbackApplication, swaggerApp, opts) {
routeHelper.addRouteToAPIDeclaration(route, classDef, doc);
});

// Add models referenced from routes (e.g. accepts/returns)
Object.keys(apiDocs).forEach(function(className) {
var classDoc = apiDocs[className];
classDoc.apis.forEach(function(api) {
api.operations.forEach(function(routeDoc) {
routeDoc.parameters.forEach(function(param) {
var type = param.type;
if (type === 'array' && param.items)
type = param.items.type;

addTypeToModels(type);
});

addTypeToModels(routeDoc.type);

routeDoc.responseMessages.forEach(function(msg) {
addTypeToModels(msg.responseModel);
});

function addTypeToModels(name) {
if (!name || name === 'void') return;

var model = loopbackApplication.models[name];
if (!model) {
var loopback = loopbackApplication.loopback;
if (!loopback) return;

if (loopback.findModel) {
model = loopback.findModel(name); // LoopBack 2.x
} else {
model = loopback.getModel(name); // LoopBack 1.x
}
}
if (!model) return;

modelHelper.generateModelDefinition(model, classDoc.models);
}
});
});
});

/**
* The topmost Swagger resource is a description of all (non-Swagger)
* resources available on the system, and where to find more
Expand Down Expand Up @@ -114,12 +159,14 @@ function addRoute(app, uri, doc, opts) {
// can't guarantee this path is either reachable or desirable if it's set
// as a part of the options.
//
// The simplest way around this is to reflect the value of the `Host` HTTP
// header as the `basePath`. Because we pre-build the Swagger data, we don't
// know that header at the time the data is built.
// The simplest way around this is to reflect the value of the `Host` and/or
// `X-Forwarded-Host` HTTP headers as the `basePath`.
// Because we pre-build the Swagger data, we don't know that header at
// the time the data is built.
if (hasBasePath) {
var headers = req.headers;
var host = headers.Host || headers.host;
// NOTE header names (keys) are always all-lowercase
var host = headers['x-forwarded-host'] || headers.host;
doc.basePath = (opts.protocol || req.protocol) + '://' +
host + initialPath;
}
Expand Down
11 changes: 8 additions & 3 deletions public/css/loopbackStyles.css
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@
color: #080;
}

/*
FIXME: Separate the overrides from the rest of the styles, rather than override screen.css entirely.
*/
/* Improve spacing when the browser window is small */
#message-bar, #swagger-ui-container {
padding-left: 30px;
padding-right: 30px;
}

#api_selector {
padding: 0px 20px;
}
7 changes: 7 additions & 0 deletions test/model-helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,13 @@ describe('model-helper', function() {
expect(def.properties).to.have.property('visibleProperty');
});
});

describe('getPropType', function() {
it('converts anonymous object types', function() {
var type = modelHelper.getPropType({ name: 'string', value: 'string' });
expect(type).to.eql('object');
});
});
});

// Simulates the format of a remoting class.
Expand Down
65 changes: 48 additions & 17 deletions test/route-helper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ describe('route-helper', function() {
{ arg: 'avg', type: 'number' }
]
});
expect(doc.operations[0].type).to.equal('object');
expect(doc.operations[0].type).to.equal(undefined);
expect(getResponseType(doc.operations[0])).to.equal('object');
});

it('converts path params when they exist in the route name', function() {
Expand Down Expand Up @@ -60,19 +61,12 @@ describe('route-helper', function() {
]
});
var opDoc = doc.operations[0];
expect(opDoc.type).to.equal('array');
expect(opDoc.items).to.eql({type: 'customType'});
});
// Note: swagger-ui treat arrays of X the same way as object X
expect(getResponseType(opDoc)).to.equal('customType');

it('correctly converts return types (format)', function() {
var doc = createAPIDoc({
returns: [
{arg: 'data', type: 'buffer'}
]
});
var opDoc = doc.operations[0];
expect(opDoc.type).to.equal('string');
expect(opDoc.format).to.equal('byte');
// NOTE(bajtos) this would be the case if there was a single response type
// expect(opDoc.type).to.equal('array');
// expect(opDoc.items).to.eql({type: 'customType'});
});

it('includes `notes` metadata', function() {
Expand Down Expand Up @@ -151,12 +145,45 @@ describe('route-helper', function() {
.to.have.property('enum').eql([1,2,3]);
});

it('preserves `enum` returns arg metadata', function() {
it('includes the default response message with code 200', function() {
var doc = createAPIDoc({
returns: [{ name: 'arg', root: true, type: 'number', enum: [1,2,3] }]
returns: [{ name: 'result', type: 'object', root: true }]
});
expect(doc.operations[0].responseMessages).to.eql([
{
code: 200,
message: 'Request was successful',
responseModel: 'object'
}
]);
});

it('uses the response code 204 when `returns` is empty', function() {
var doc = createAPIDoc({
returns: []
});
expect(doc.operations[0].responseMessages).to.eql([
{
code: 204,
message: 'Request was successful',
responseModel: 'void'
}
]);
});

it('includes custom error response in `responseMessages`', function() {
var doc = createAPIDoc({
errors: [{
code: 422,
message: 'Validation failed',
responseModel: 'ValidationError'
}]
});
expect(doc.operations[0].responseMessages[1]).to.eql({
code: 422,
message: 'Validation failed',
responseModel: 'ValidationError'
});
expect(doc.operations[0])
.to.have.property('enum').eql([1,2,3]);
});
});

Expand All @@ -168,3 +195,7 @@ function createAPIDoc(def) {
method: 'test.get'
}));
}

function getResponseType(operationDoc) {
return operationDoc.responseMessages[0].responseModel;
}
Loading