Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
56003f0
Load swagger ui from `swagger-ui` package instead.
Jul 4, 2014
eb31787
Swagger 1.2 compatability. Moved strong-remoting/ext/swagger to this …
Jul 4, 2014
2f8506c
Fix api resource path and type ref to models.
Jul 5, 2014
67bb10b
Some swagger 1.2 migration cleanup.
Jul 5, 2014
d34304a
Make sure body parameter is shown.
Jul 5, 2014
4c0ce42
Refactoring swagger 1.2 rework.
Jul 5, 2014
a4a36f5
Refactor key translations between LDL & Swagger.
Jul 6, 2014
5f22982
Allow easy setting of accessToken in explorer UI.
STRML Jul 9, 2014
a6afe13
Restore existing styles.
STRML Jul 9, 2014
19c3fe3
Fix missing strong-remoting devDependency.
STRML Jul 9, 2014
70dddef
Use express routes instead of modifying remoting.
STRML Jul 9, 2014
77f0167
LDL to Swagger fixes & extensions.
STRML Jul 10, 2014
3ce35e1
Refactor route-helper & add tests.
STRML Jul 10, 2014
781ac2b
Rename translateKeys to translateDataTypeKeys.
STRML Jul 10, 2014
75713f1
Add url-join so path.join() doesn't break windows
STRML Jul 10, 2014
cbf768f
Remove peerDependencies, use express directly.
STRML Jul 10, 2014
34b3983
Remove swagger.test.js license
STRML Jul 10, 2014
6224243
Remove preMiddleware.
STRML Jul 10, 2014
75ba058
More consise type tests
STRML Jul 11, 2014
4b3785c
Simplify `accepts` and `returns` hacks.
STRML Jul 11, 2014
a857fe5
Remove forgotten TODO.
STRML Jul 11, 2014
5c130a4
Fix debug namespace, express version.
STRML Jul 11, 2014
cefbdb3
Fix resources if the explorer is at a deep path.
STRML Jul 11, 2014
fcd1926
s/accessToken/access_token in authorization key name
STRML Jul 14, 2014
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
13 changes: 13 additions & 0 deletions .jshintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"node": true,
"camelcase" : true,
"eqnull" : true,
"indent": 2,
"undef": true,
"quotmark": "single",
"maxlen": 80,
"trailing": true,
"newcap": true,
"nonew": true,
"undef": false
}
72 changes: 69 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,80 @@ Below is a simple LoopBack application. The explorer is mounted at `/explorer`.
```js
var loopback = require('loopback');
var app = loopback();
var explorer = require('loopback-explorer');
var explorer = require('../');
var port = 3000;

var Product = loopback.Model.extend('product');
Product.attachTo(loopback.memory());
app.model(Product);

app.use('/explorer', explorer(app, {basePath: '/api'}));
app.use('/api', loopback.rest());
app.use('/explorer', explorer(app, { basePath: '/api' }));
console.log("Explorer mounted at localhost:" + port + "/explorer");

app.listen(3000);
app.listen(port);
```

## Advanced Usage

Many aspects of the explorer are configurable.

See [options](#options) for a description of these options:

```js
// Mount middleware before calling `explorer()` to add custom headers, auth, etc.
app.use('/explorer', loopback.basicAuth('user', 'password'));
app.use('/explorer', explorer(app, {
basePath: '/custom-api-root',
swaggerDistRoot: '/swagger',
apiInfo: {
'title': 'My API',
'description': 'Explorer example app.'
},
resourcePath: 'swaggerResources',
version: '0.1-unreleasable'
}));
app.use('/custom-api-root', loopback.rest());
```

## Options

Options are passed to `explorer(app, options)`.

`basePath`: **String**

> Default: `app.get('restAPIRoot')` or `'/api'`.

> Sets the API's base path. This must be set if you are mounting your api
> to a path different than '/api', e.g. with
> `loopback.use('/custom-api-root', loopback.rest());

`swaggerDistRoot`: **String**

> Sets a path within your application for overriding Swagger UI files.

> If present, will search `swaggerDistRoot` first when attempting to load Swagger UI, allowing
> you to pick and choose overrides to the interface. Use this to style your explorer or
> add additional functionality.

> See [index.html](public/index.html), where you may want to begin your overrides.
> The rest of the UI is provided by [Swagger UI](https://github.com/wordnik/swagger-ui).

`apiInfo`: **Object**

> Additional information about your API. See the
> [spec](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#513-info-object).

`resourcePath`: **String**

> Default: `'resources'`

> Sets a different path for the
> [resource listing](https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#51-resource-listing).
> You generally shouldn't have to change this.

`version`: **String**

> Default: Read from package.json

> Sets your API version. If not present, will read from your app's package.json.
15 changes: 11 additions & 4 deletions example/simple.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
var loopback = require('loopback');
var app = loopback();
var explorer = require('../');
var port = 3000;

var Product = loopback.Model.extend('product');
var Product = loopback.Model.extend('product', {
foo: {type: 'string', required: true},
bar: 'string',
aNum: {type: 'number', min: 1, max: 10, required: true, default: 5}
});
Product.attachTo(loopback.memory());
app.model(Product);

app.use(loopback.rest());
app.use('/explorer', explorer(app));
var apiPath = '/api';
app.use('/explorer', explorer(app, {basePath: apiPath}));
app.use(apiPath, loopback.rest());
console.log('Explorer mounted at localhost:' + port + '/explorer');

app.listen(3000);
app.listen(port);
65 changes: 36 additions & 29 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
'use strict';
/*!
* Adds dynamically-updated docs as /explorer
*/
var url = require('url');
var path = require('path');
var extend = require('util')._extend;
var loopback = require('loopback');
var express = requireLoopbackDependency('express');
var urlJoin = require('./lib/url-join');
var _defaults = require('lodash.defaults');
var express = require('express');
var swagger = require('./lib/swagger');
var SWAGGER_UI_ROOT = path.join(__dirname, 'node_modules',
'swagger-ui', 'dist');
var STATIC_ROOT = path.join(__dirname, 'public');

module.exports = explorer;
Expand All @@ -17,41 +22,43 @@ module.exports = explorer;
*/

function explorer(loopbackApplication, options) {
options = extend({}, options);
options.basePath = options.basePath || loopbackApplication.get('restApiRoot');

loopbackApplication.docs(options);
options = _defaults({}, options, {
resourcePath: 'resources',
apiInfo: loopbackApplication.get('apiInfo') || {}
});

var app = express();

swagger(loopbackApplication, app, options);

app.disable('x-powered-by');

// config.json is loaded by swagger-ui. The server should respond
// with the relative URI of the resource doc.
app.get('/config.json', function(req, res) {
// Get the path we're mounted at. It's best to get this from the referer
// in case we're proxied at a deep path.
var source = url.parse(req.headers.referer || '').pathname;
// If no referer is available, use the incoming url.
if (!source) {
source = req.originalUrl.replace(/\/config.json(\?.*)?$/, '');
}
res.send({
discoveryUrl: (options.basePath || '') + '/swagger/resources'
url: urlJoin(source, '/' + options.resourcePath)
});
});
app.use(loopback.static(STATIC_ROOT));
return app;
}

function requireLoopbackDependency(module) {
try {
return require('loopback/node_modules/' + module);
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') throw err;
try {
// Dependencies may be installed outside the loopback module,
// e.g. as peer dependencies. Try to load the dependency from there.
return require(module);
} catch (errPeer) {
if (errPeer.code !== 'MODULE_NOT_FOUND') throw errPeer;
// Rethrow the initial error to make it clear that we were trying
// to load a module that should have been installed inside
// "loopback/node_modules". This should minimise end-user's confusion.
// However, such situation should never happen as `require('loopback')`
// would have failed before this function was even called.
throw err;
}
// Allow specifying a static file root for swagger files. Any files in
// that folder will override those in the swagger-ui distribution.
// In this way one could e.g. make changes to index.html without having
// to worry about constantly pulling in JS updates.
if (options.swaggerDistRoot) {
app.use(express.static(options.swaggerDistRoot));
}
// File in node_modules are overridden by a few customizations
app.use(express.static(STATIC_ROOT));
// Swagger UI distribution
app.use(express.static(SWAGGER_UI_ROOT));

return app;
}
47 changes: 47 additions & 0 deletions lib/class-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
'use strict';

/**
* Module dependencies.
*/
var modelHelper = require('./model-helper');
var urlJoin = require('./url-join');

/**
* Export the classHelper singleton.
*/
var classHelper = module.exports = {
/**
* Given a remoting class, generate an API doc.
* See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#52-api-declaration
* @param {Class} aClass Strong Remoting class.
* @param {Object} opts Options (passed from Swagger(remotes, options))
* @param {String} opts.version API Version.
* @param {String} opts.swaggerVersion Swagger version.
* @param {String} opts.basePath Basepath (usually e.g. http://localhost:3000).
* @param {String} opts.resourcePath Resource path (usually /swagger/resources).
* @return {Object} API Declaration.
*/
generateAPIDoc: function(aClass, opts) {
return {
apiVersion: opts.version,
swaggerVersion: opts.swaggerVersion,
basePath: opts.basePath,
resourcePath: urlJoin('/', opts.resourcePath),
apis: [],
models: modelHelper.generateModelDefinition(aClass)
};
},
/**
* Given a remoting class, generate a reference to an API declaration.
* This is meant for insertion into the Resource declaration.
* See https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#512-resource-object
* @param {Class} aClass Strong Remoting class.
* @return {Object} API declaration reference.
*/
generateResourceDocAPIEntry: function(aClass) {
return {
path: aClass.http.path,
description: aClass.ctor.sharedCtor && aClass.ctor.sharedCtor.description
};
}
};
101 changes: 101 additions & 0 deletions lib/model-helper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
'use strict';

/**
* Module dependencies.
*/
var _cloneDeep = require('lodash.clonedeep');
var translateDataTypeKeys = require('./translate-data-type-keys');

/**
* 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} class Remote class.
* @return {Object} Associated model definition.
*/
generateModelDefinition: function generateModelDefinition(aClass) {
var def = aClass.ctor.definition;
var name = def.name;

var required = [];
// Don't modify original properties.
var properties = _cloneDeep(def.properties);

// Iterate through each property in the model definition.
// Types may be defined as constructors (e.g. String, Date, etc.),
// or as strings; getPropType() will take care of the conversion.
// See more on types:
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives
Object.keys(properties).forEach(function(key) {
var prop = properties[key];

// Eke a type out of the constructors we were passed.
prop = modelHelper.LDLPropToSwaggerDataType(prop);

// Required props sit in a per-model array.
if (prop.required || (prop.id && !prop.generated)) {
required.push(key);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think id:true property are automatically required, they are auto-generated in many cases.

@raymondfeng can you confirm?


// Change mismatched keys.
prop = translateDataTypeKeys(prop);

// Assign this back to the properties object.
properties[key] = prop;
});

var out = {};
out[name] = {
id: name,
properties: properties,
required: required
};
return out;
},

/**
* Given a propType (which may be a function, string, or array),
* get a string type.
* @param {*} propType Prop type description.
* @return {String} Prop type string.
*/
getPropType: function getPropType(propType) {
if (typeof propType === 'function') {
propType = propType.name.toLowerCase();
} else if(Array.isArray(propType)) {
propType = 'array';
}
return propType;
},

// Converts a prop defined with the LDL spec to one conforming to the
// Swagger spec.
// https://github.com/wordnik/swagger-spec/blob/master/versions/1.2.md#431-primitives
LDLPropToSwaggerDataType: function LDLPropToSwaggerDataType(prop) {
var out = _cloneDeep(prop);
out.type = modelHelper.getPropType(out.type);

if (out.type === 'array') {
var arrayProp = prop.type[0];
if (!arrayProp.type) arrayProp = {type: arrayProp};
out.items = modelHelper.LDLPropToSwaggerDataType(arrayProp);
}

if (out.type === 'date') {
out.type = 'string';
out.format = 'date';
} else if (out.type === 'buffer') {
out.type = 'string';
out.format = 'byte';
} else if (out.type === 'number') {
out.format = 'double'; // Since all JS numbers are doubles
}
return out;
}
};



Loading