added server generator

This commit is contained in:
Tony Tam 2012-08-27 20:33:10 -07:00
parent a929fce3a0
commit b4485b1196
9 changed files with 886 additions and 1 deletions

View File

@ -1,4 +1,4 @@
package com.wordnik.client package com.wordnik.petstore
import com.wordnik.swagger.core.util.JsonUtil import com.wordnik.swagger.core.util.JsonUtil
import com.sun.jersey.api.client.Client import com.sun.jersey.api.client.Client

View File

@ -0,0 +1,48 @@
/**
* Copyright 2012 Wordnik, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.wordnik.swagger.codegen.BasicScalaGenerator
import com.wordnik.swagger.core._
import scala.collection.mutable.{ HashMap, ListBuffer }
object NodeServerGenerator extends BasicScalaGenerator {
def main(args: Array[String]) = generateClient(args)
override def templateDir = "samples/server-generator/node/templates"
val outputFolder = "samples/server-generator/node/output"
// where to write generated code
override def destinationDir = outputFolder + "/App"
// template used for apis
apiTemplateFiles ++= Map("api.mustache" -> ".js")
modelTemplateFiles.clear
override def apiPackage = Some("apis")
// supporting classes
override def supportingFiles = List(
("package.json", outputFolder, "package.json"),
("main.mustache", destinationDir, "main.js"),
("models.mustache", destinationDir, "models.js"),
("Common/node/paramTypes.js", destinationDir + "/Common/node", "paramTypes.js"),
("Common/node/randomizer.js", destinationDir + "/Common/node", "randomizer.js"),
("Common/node/swagger.js", destinationDir + "/Common/node", "swagger.js"))
}

View File

@ -0,0 +1,56 @@
function createEnum(input) {
if (input && input.toString().indexOf(",") > 0) {
var output = [];
var array = input.split(",");
array.forEach(function(item) {
output.push(item);
})
return output;
}
}
exports.query = exports.q = function(name, description, dataType, required, allowMultiple, allowableValues, defaultValue) {
return {
"name" : name,
"description" : description,
"dataType" : dataType,
"required" : required,
"allowMultiple" : allowMultiple,
"allowableValues" : createEnum(allowableValues),
"defaultValue" : defaultValue,
"paramType" : "query"
};
};
exports.path = function(name, description, dataType, allowableValues) {
return {
"name" : name,
"description" : description,
"dataType" : dataType,
"required" : true,
"allowMultiple" : false,
"allowableValues" : createEnum(allowableValues),
"paramType" : "path"
};
};
exports.post = function(dataType, description) {
return {
"description" : description,
"dataType" : dataType,
"required" : true,
"paramType" : "body"
};
};
exports.header = function(name, description, dataType, required) {
return {
"name" : name,
"description" : description,
"dataType" : dataType,
"required" : true,
"allowMultiple" : false,
"allowableValues" : createEnum(allowableValues),
"paramType" : "header"
};
};

View File

@ -0,0 +1,68 @@
var Randomizer = {
'intBetween': function(a, b) {
return Math.floor(Math.random()*(b-a+1)+a);
},
'string': function() {
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
var string_length = 24;
var randomstring = '';
for (var i=0; i<string_length; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum,rnum+1);
}
return randomstring;
},
'int': function() {
return this.intBetween(0, 10000);
},
'long': function() {
return this.int();
},
'double': function() {
return Math.random()*1000000;
},
'boolean': function() {
return this.intBetween(1,2) == 1;
},
'__date': function() {
var m = this.intBetween(1, 12);
if (m < 10) { m = '0'+m; }
var d = this.intBetween(1, 28);
if (d < 10) { d = '0'+d; }
return this.intBetween(1800, 2015) + '-' + m + '-' + d;
},
'date': function() {
return this.__date() + 'T' + this.__time();
},
'__time': function() {
var h = this.intBetween(0, 23);
if (h < 10) { h = '0'+h; }
var m = this.intBetween(0, 59);
if (m < 10) { m = '0'+m; }
var s = this.intBetween(0, 59);
if (s < 10) { s = '0'+s; }
return h + ':' + m + ':' + s + '+01:00';
},
'array': function(type, length) {
var out = new Array();
if (!length) {
length = this.intBetween(1, 5); }
for (var i = 0; i < length; i++) {
out.push(this[type]()); }
return out;
}
};
exports.intBetween = Randomizer.intBetween;
exports.string = Randomizer.string;
exports.int = Randomizer.int;
exports.long = Randomizer.long;
exports.double = Randomizer.double;
exports.boolean = Randomizer.boolean;
exports.array = Randomizer.array;
exports.__date = Randomizer.__date;
exports.date = Randomizer.date;
exports.__time = Randomizer.__time;

View File

@ -0,0 +1,612 @@
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 Randomizer = require(__dirname + '/randomizer.js');
var params = require(__dirname + '/paramTypes.js');
var allModels = {};
/**
* Initialize Randomizer Caching
*/
var RandomStorage = {};
for (var i = 0; i < allowedDataTypes.length; i++) {
RandomStorage[allowedDataTypes[i]] = {}; }
/**
* sets the base path, api version
*
* @param app
* @param bp
* @param av
*/
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;
}
}
/**
* creates declarations for each resource
*
* @param app
*/
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 {
res.header('Access-Control-Allow-Origin', "*");
res.header("Content-Type", "application/json; charset=utf-8");
var key = req.url.substr(1).replace('.json', '.{format}').split('?')[0];
var data = applyFilter(req, res, resources[key]);
data.basePath = basePath;
if (data.code) {
res.send(data, data.code); }
else {
res.header('Access-Control-Allow-Origin', "*");
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.header("Access-Control-Allow-Headers", "Content-Type");
res.header("Content-Type", "application/json; charset=utf-8");
res.send(JSON.stringify(applyFilter(req, res, r)));
}
}
});
}
}
/**
* generate random date for type
*
* @param type type of data (must be defined in allowedDataTypes)
* @param withRandom fill with random data
* @return value
*/
function randomDataByType(type, withRandom, subType) {
type = type.toLowerCase();
if (allowedDataTypes.indexOf(type)<0) {
return null; }
return Randomizer[type](subType);
}
/**
* Get cache for type and identifier
* @param type
* @param id
* @param key
* @return value
*/
function getCache(type, id, key) {
if (id && id != -1 && RandomStorage[type] && RandomStorage[type][key+id]) {
return RandomStorage[type][key + id]; }
else {
return null; }
}
/**
* Set cache for type and identifier
* @param type
* @param id
* @param key
* @param value
*/
function setCache(curType, id, key, value) {
if (id && id != -1 && RandomStorage[curType]) {
RandomStorage[curType][key + id] = value; }
}
/**
* try to generate object from model defintion
* @param model
* @param withData fill model with data
* @param withRandom generate random values
* @return object
*/
function containerByModel(model, withData, withRandom) {
var item = {};
for (key in model.properties) {
var curType = model.properties[key].type.toLowerCase();
var value = '';
if (withData && withData[key]) {
value = withData[key]; }
if (value == '' && withRandom) {
var cache = getCache(curType, withRandom, key);
if (cache) {
value = cache; }
else {
if (model.properties[key].enum) {
value = model.properties[key].enum[Randomizer.intBetween(0, model.properties[key].enum.length-1)]; }
else {
var subType = false;
if (model.properties[key].items && model.properties[key].items.type) {
subType = model.properties[key].items.type; }
var curKey = key;
value = randomDataByType(curType, withRandom, subType);
var key = curKey;
}
}
setCache(curType, withRandom, key, value);
}
if (value == '' && curType == 'Array') {
value = []; }
item[key] = value;
}
return item;
}
/**
* filters resource listing by access
*
* @param req
* @param res
* @param r
* @returns
*/
function applyFilter(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 if any
var output = shallowClone(r);
// clone models
var requiredModels = [];
// clone methods that have access
output.apis = new Array();
var apis = JSON.parse(JSON.stringify(r.apis));
for (var i in apis) {
var api = apis[i];
var clonedApi = shallowClone(api);
clonedApi.operations = new Array();
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);
}
}
if (clonedApi.operations.length > 0) {
// only add cloned api if there are operations
output.apis.push(clonedApi);
}
}
// add 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
* @param operation
* @param models
*/
function addModelsFromResponse(operation, models){
var responseModel = operation.responseClass;
if (responseModel) {
responseModel = responseModel.replace(/^List\[/,"").replace(/\]/,"");
if (models.indexOf(responseModel) < 0) {
models.push(responseModel);
}
}
}
function shallowClone(obj) {
var cloned = new Object();
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
*
* @param req
* @param path
* @param httpMethod
* @returns {Boolean}
*/
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(request, response) {
var r = {"apiVersion" : apiVersion, "swaggerVersion": swaggerVersion, "basePath": basePath, "apis": []};
for (var key in resources) {
r.apis.push({"path": "/" + key, "description": "none"});
}
response.header('Access-Control-Allow-Origin', "*");
response.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
response.header("Access-Control-Allow-Headers", "Content-Type");
response.header("Content-Type", "application/json; charset=utf-8");
response.write(JSON.stringify(r));
response.end();
}
/**
* adds a method to the api along with a spec. If the spec fails to validate, it won't be added
*
* @param app
* @param callback
* @param spec
*/
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) {
// found matching path and 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: add some XML support
// 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) {
res.header('Access-Control-Allow-Origin', "*");
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
res.header("Access-Control-Allow-Headers", "Content-Type");
res.header("Content-Type", "application/json; charset=utf-8");
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
* @param app
*/
function setAppHandler(app) {
appHandler = app;
}
/**
* Add swagger handlers to express
* @param type http method
* @param handlers list of handlers to be added
*/
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));
}
function addGet() {
addHandlers('GET', arguments);
return this;
}
function addPost() {
addHandlers('POST', arguments);
return this;
}
function addDelete() {
addHandlers('DELETE', arguments);
return this;
}
function addPut() {
addHandlers('PUT', arguments);
return this;
}
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
* @param int code
* @param string description
* @return obj
*/
function error(code, description) {
return {"code" : code, "description" : description};
}
/**
* Stop express ressource with error code
* @param obj res expresso response
* @param obj error error object with code and description
*/
function stopWithError(res, error) {
res.header('Access-Control-Allow-Origin', "*");
res.header("Content-Type", "application/json; charset=utf-8");
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.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;
exports.containerByModel = containerByModel;
exports.Randomizer = Randomizer;

View File

@ -0,0 +1,52 @@
var sw = require("../Common/node/swagger.js");
var param = require("../Common/node/paramTypes.js");
var url = require("url");
var swe = sw.errors;
/* add model includes */
function writeResponse (response, data) {
response.header('Access-Control-Allow-Origin', "*");
response.header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT");
response.header("Access-Control-Allow-Headers", "Content-Type");
response.header("Content-Type", "application/json; charset=utf-8");
response.send(JSON.stringify(data));
}
exports.models = models = require("../models.js");
{{#operations}}
{{#operation}}
exports.{{nickname}} = {
'spec': {
"description" : "Operations about pets",
"path" : "{{path}}",
"notes" : "{{{notes}}}",
"summary" : "{{{summary}}}",
"method": "{{httpMethod}}",
"params" : [{{#queryParams}}
param.query("{{paramName}}", "{{description}}", "{{dataType}}", {{required}}, {{allowMultiple}}, "{{{allowableValues}}}"{{#defaultValue}}, {{{defaultValue}}}{{/defaultValue}}){{#hasMore}},{{/hasMore}}
{{/queryParams}}].concat([{{#pathParams}}
param.path("{{paramName}}", "{{description}}"){{#hasMore}},{{/hasMore}}
{{/pathParams}}]).concat([{{#headerParams}}
param.header("{{paramName}}", "{{description}}"){{#hasMore}},{{/hasMore}}
{{/headerParams}}]).concat([{{#bodyParams}}
param.post("{{dataType}}", "{{description}}", {{required}})
{{/bodyParams}}]),
"responseClass" : "{{responseClass}}",
"errorResponses" : [swe.invalid('id'), swe.notFound('pet')],
"nickname" : "{{nickname}}"
},
'action': function (req,res) {
{{#requiredParamCount}}
{{#requiredParams}}
if (!req.params.{{baseName}}) {
throw swe.invalid('{{baseName}}');
}
{{/requiredParams}}
{{/requiredParamCount}}
writeResponse(res, {message: "how about implementing {{nickname}} as a {{httpMethod}} method?"});
}
};
{{/operation}}
{{/operations}}

View File

@ -0,0 +1,27 @@
var express = require("express")
, url = require("url")
, swagger = require("./Common/node/swagger.js")
, db = false
var app = express.createServer(
function(req, res, next) { if (req.db === undefined) { req.db = db; } next(); });
app.use(express.bodyParser());
swagger.setAppHandler(app);
// resources for the demo
{{#apis}}
var {{name}} = require("./apis/{{name}}.js");
{{/apis}}
swagger.addModels(models)
{{#apis}}
{{#operations}}
{{#operation}}.add{{httpMethod}}({{name}}.{{nickname}}){{/operation}}{{newline}}
{{/operations}}
{{/apis}};
// configures the app
swagger.configure("http://localhost:8002", "0.1");
// start the server
app.listen(8002);

View File

@ -0,0 +1,5 @@
exports.models = {
{{#models}}
"{{modelName}}": {{{modelJson}}}{{#hasMore}},{{/hasMore}}
{{/models}}
}

View File

@ -0,0 +1,17 @@
{
"name": "sample-app",
"description": "Wordnik node.js server generator",
"version": "1.0.0",
"homepage": "https://github.com/wordnik/swagger-codegen",
"main": "./Common/node/swagger.js",
"directories": {
"lib": "./Common/node"
},
"engines": {
"node": ">= 0.8.x"
},
"dependencies": {
"connect": ">= 1.8.x",
"express": "3.x"
}
}