swagger.js | |
---|---|
var resourcePath = "/resources.json";
var basePath = "/";
var swaggerVersion = "1.1";
var apiVersion = "0.0";
var resources = {};
var validators = [];
var appHandler = null;
var allowedMethods = ['get', 'post', 'put', 'delete'];
var allowedDataTypes = ['string', 'int', 'long', 'double', 'boolean', 'date', 'array'];
var params = require(__dirname + '/paramTypes.js');
var allModels = {}; | |
Configuring swagger will set the basepath and api version for all subdocuments. It should only be done once, and during bootstrap of the app | function configure(bp, av) {
basePath = bp;
apiVersion = av;
setResourceListingPaths(appHandler);
appHandler.get(resourcePath, resourceListing); |
update resources if already configured | for(key in resources) {
var r = resources[key];
r.apiVersion = av;
r.basePath = bp;
}
} |
Convenience to set default headers in each response. | function setHeaders(res) {
res.header('Access-Control-Allow-Origin', "*");
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.header("Access-Control-Allow-Headers", "Content-Type, api_key");
res.header("Content-Type", "application/json; charset=utf-8");
} |
creates declarations for each resource path. | function setResourceListingPaths(app) {
for (var key in resources) {
app.get("/" + key.replace("\.\{format\}", ".json"), function(req, res) {
var r = resources[req.url.substr(1).split('?')[0].replace('.json', '.{format}')];
if (!r)
return stopWithError(res, {'description': 'internal error', 'code': 500});
else {
setHeaders(res);
var key = req.url.substr(1).replace('.json', '.{format}').split('?')[0];
var data = filterApiListing(req, res, resources[key]);
data.basePath = basePath;
if (data.code) {
res.send(data, data.code); }
else {
res.send(JSON.stringify(filterApiListing(req, res, r)));
}
}
});
}
} |
Applies a filter to an api listing. When done, the api listing will only contain methods and models that the user actually has access to. | function filterApiListing(req, res, r) {
var route = req.route;
var excludedPaths = [];
if (!r || !r.apis) {
return stopWithError(res, {'description': 'internal error', 'code': 500});
}
for (var key in r.apis) {
var api = r.apis[key];
for (var opKey in api.operations) {
var op = api.operations[opKey];
var path = api.path.replace(/{.*\}/, "*");
if (!canAccessResource(req, route + path, op.httpMethod)) {
excludedPaths.push(op.httpMethod + ":" + api.path); }
}
} |
clone attributes in the resource | var output = shallowClone(r);
|
models required in the api listing | var requiredModels = [];
|
clone methods that user can access | output.apis = [];
var apis = JSON.parse(JSON.stringify(r.apis));
for (var i in apis) {
var api = apis[i];
var clonedApi = shallowClone(api);
clonedApi.operations = [];
var shouldAdd = true;
for (var o in api.operations) {
var operation = api.operations[o];
if (excludedPaths.indexOf(operation.httpMethod + ":" + api.path) >= 0) {
break;
}
else {
clonedApi.operations.push(JSON.parse(JSON.stringify(operation)));
addModelsFromResponse(operation, requiredModels);
}
} |
only add cloned api if there are operations | if (clonedApi.operations.length > 0) {
output.apis.push(clonedApi);
}
} |
add required models to output | output.models = {};
for (var i in requiredModels){
var modelName = requiredModels[i];
var model = allModels.models[modelName];
if(model){
output.models[requiredModels[i]] = model;
}
} |
look in object graph | for (key in output.models) {
var model = output.models[key];
if (model && model.properties) {
for (var key in model.properties) {
var t = model.properties[key].type;
switch (t){
case "Array":
if (model.properties[key].items) {
var ref = model.properties[key].items.$ref;
if (ref && requiredModels.indexOf(ref) < 0) {
requiredModels.push(ref);
}
}
break;
case "string":
case "long":
break;
default:
if (requiredModels.indexOf(t) < 0) {
requiredModels.push(t);
}
break;
}
}
}
}
for (var i in requiredModels){
var modelName = requiredModels[i];
if(!output[modelName]) {
var model = allModels.models[modelName];
if(model){
output.models[requiredModels[i]] = model;
}
}
}
return output;
} |
Add model to list and parse List[model] elements | function addModelsFromResponse(operation, models){
var responseModel = operation.responseClass;
if (responseModel) {
responseModel = responseModel.replace(/^List\[/,"").replace(/\]/,"");
if (models.indexOf(responseModel) < 0) {
models.push(responseModel);
}
}
} |
clone anything but objects to avoid shared references | function shallowClone(obj) {
var cloned = {};
for (var i in obj) {
if (typeof (obj[i]) != "object") {
cloned[i] = obj[i];
}
}
return cloned;
} |
function for filtering a resource. override this with your own implementation. if consumer can access the resource, method returns true. | function canAccessResource(req, path, httpMethod) {
for (var i in validators) {
if (!validators[i](req,path,httpMethod))
return false;
}
return true;
}
/**
* returns the json representation of a resource
*
* @param request
* @param response
*/
function resourceListing(req, res) {
var r = {
"apiVersion" : apiVersion,
"swaggerVersion" : swaggerVersion,
"basePath" : basePath,
"apis" : []
};
for (var key in resources)
r.apis.push({"path": "/" + key, "description": "none"});
setHeaders(res);
res.write(JSON.stringify(r));
res.end();
} |
Adds a method to the api along with a spec. If the spec fails to validate, it won't be added | function addMethod(app, callback, spec) {
var rootPath = spec.path.split("/")[1];
var root = resources[rootPath];
if (root && root.apis) {
for (var key in root.apis) {
var api = root.apis[key];
if (api && api.path == spec.path && api.method == spec.method) { |
Add & return | appendToApi(root, api, spec);
return;
}
}
}
var api = {"path" : spec.path};
if (!resources[rootPath]) {
if (!root) {
var resourcePath = "/" + rootPath.replace("\.\{format\}", "");
root = {
"apiVersion" : apiVersion, "swaggerVersion": swaggerVersion, "basePath": basePath, "resourcePath": resourcePath, "apis": [], "models" : []
};
}
resources[rootPath] = root;
}
root.apis.push(api);
appendToApi(root, api, spec); |
TODO: only supports json convert .{format} to .json, make path params happy | var fullPath = spec.path.replace("\.\{format\}", ".json").replace(/\/{/g, "/:").replace(/\}/g,"");
var currentMethod = spec.method.toLowerCase();
if (allowedMethods.indexOf(currentMethod)>-1) {
app[currentMethod](fullPath, function(req,res) {
setHeaders(res);
if (!canAccessResource(req, req.url.substr(1).split('?')[0].replace('.json', '.*'), req.method)) {
res.send(JSON.stringify({"description":"forbidden", "code":403}), 403);
} else {
try {
callback(req,res);
}
catch (ex) {
if (ex.code && ex.description)
res.send(JSON.stringify(ex), ex.code);
else {
console.error(spec.method + " failed for path '" + require('url').parse(req.url).href + "': " + ex);
res.send(JSON.stringify({"description":"unknown error","code":500}), 500);
}
}
}
});
} else {
console.log('unable to add ' + currentMethod.toUpperCase() + ' handler');
return;
}
} |
Set expressjs app handler | function setAppHandler(app) {
appHandler = app;
} |
Add swagger handlers to express | function addHandlers(type, handlers) {
for (var i = 0; i < handlers.length; i++) {
var handler = handlers[i];
handler.spec.method = type;
addMethod(appHandler, handler.action, handler.spec);
}
} |
Discover swagger handler from resource | function discover(resource) {
for (var key in resource) {
if (resource[key].spec && resource[key].spec.method && allowedMethods.indexOf(resource[key].spec.method.toLowerCase())>-1) {
addMethod(appHandler, resource[key].action, resource[key].spec);
}
else
console.log('auto discover failed for: ' + key);
}
} |
Discover swagger handler from resource file path | function discoverFile(file) {
return discover(require(file));
} |
adds get handler | function addGet() {
addHandlers('GET', arguments);
return this;
} |
adds post handler | function addPost() {
addHandlers('POST', arguments);
return this;
} |
adds delete handler | function addDelete() {
addHandlers('DELETE', arguments);
return this;
} |
adds put handler | function addPut() {
addHandlers('PUT', arguments);
return this;
} |
adds models to swagger | function addModels(models) {
allModels = models;
return this;
}
function wrap(callback, req, resp){
callback(req,resp);
}
function appendToApi(rootResource, api, spec) {
if (!api.description) {
api.description = spec.description;
}
var validationErrors = [];
if(!spec.nickname || spec.nickname.indexOf(" ")>=0){ |
nicknames don't allow spaces | validationErrors.push({"path": api.path, "error": "invalid nickname '" + spec.nickname + "'"});
} |
validate params | for ( var paramKey in spec.params) {
var param = spec.params[paramKey];
if(param.allowableValues) {
var avs = param.allowableValues.toString();
var type = avs.split('[')[0];
if(type == 'LIST'){
var values = avs.match(/\[(.*)\]/g).toString().replace('\[','').replace('\]', '').split(',');
param.allowableValues = {valueType: type, values: values};
}
else if (type == 'RANGE') {
var values = avs.match(/\[(.*)\]/g).toString().replace('\[','').replace('\]', '').split(',');
param.allowableValues = {valueType: type, min: values[0], max: values[1]};
}
}
switch (param.paramType) {
case "path":
if (api.path.indexOf("{" + param.name + "}") < 0) {
validationErrors.push({"path": api.path, "name": param.name, "error": "invalid path"});
}
break;
case "query":
break;
case "body":
break;
default:
validationErrors.push({"path": api.path, "name": param.name, "error": "invalid param type " + param.paramType});
break;
}
}
if (validationErrors.length > 0) {
console.log(validationErrors);
return;
}
if (!api.operations) {
api.operations = []; } |
TODO: replace if existing HTTP operation in same api path | var op = {
"parameters" : spec.params,
"httpMethod" : spec.method,
"notes" : spec.notes,
"errorResponses" : spec.errorResponses,
"nickname" : spec.nickname,
"summary" : spec.summary
};
if (spec.responseClass) {
op.responseClass = spec.responseClass;
}
else {
op.responseClass = "void";
}
api.operations.push(op);
if (!rootResource.models) {
rootResource.models = {};
}
}
function addValidator(v) {
validators.push(v);
} |
Create Error JSON by code and text | function error(code, description) {
return {"code" : code, "description" : description};
} |
Stop express ressource with error code | function stopWithError(res, error) {
setHeaders(res);
if (error && error.description && error.code)
res.send(JSON.stringify(error), error.code);
else
res.send(JSON.stringify({'description': 'internal error', 'code': 500}), 500);
} |
Export most needed error types for easier handling | exports.errors = {
'notFound': function(field, res) {
if (!res) {
return {"code": 404, "description": field + ' not found'}; }
else {
res.send({"code": 404, "description": field + ' not found'}, 404); }
},
'invalid': function(field, res) {
if (!res) {
return {"code": 400, "description": 'invalid ' + field}; }
else {
res.send({"code": 400, "description": 'invalid ' + field}, 404); }
},
'forbidden': function(res) {
if (!res) {
return {"code": 403, "description": 'forbidden' }; }
else {
res.send({"code": 403, "description": 'forbidden'}, 403); }
}
};
exports.params = params;
exports.queryParam = exports.params.query;
exports.pathParam = exports.params.path;
exports.postParam = exports.params.post;
exports.getModels = allModels;
exports.error = error;
exports.stopWithError = stopWithError;
exports.stop = stopWithError;
exports.addValidator = addValidator;
exports.configure = configure;
exports.canAccessResource = canAccessResource;
exports.resourcePath = resourcePath;
exports.resourceListing = resourceListing;
exports.setHeaders = setHeaders;
exports.addGet = addGet;
exports.addPost = addPost;
exports.addPut = addPut;
exports.addDelete = addDelete;
exports.addGET = addGet;
exports.addPOST = addPost;
exports.addPUT = addPut;
exports.addDELETE = addDelete;
exports.addModels = addModels;
exports.setAppHandler = setAppHandler;
exports.discover = discover;
exports.discoverFile = discoverFile;
|