diff --git a/samples/petstore/scala/src/main/scala/com/wordnik/client/ApiInvoker.scala b/samples/petstore/scala/src/main/scala/com/wordnik/client/ApiInvoker.scala index 9f5618c99da..a5b6b4a3c5d 100644 --- a/samples/petstore/scala/src/main/scala/com/wordnik/client/ApiInvoker.scala +++ b/samples/petstore/scala/src/main/scala/com/wordnik/client/ApiInvoker.scala @@ -1,4 +1,4 @@ -package com.wordnik.client +package com.wordnik.petstore import com.wordnik.swagger.core.util.JsonUtil import com.sun.jersey.api.client.Client diff --git a/samples/server-generator/node/NodeServerFromSpec.scala b/samples/server-generator/node/NodeServerFromSpec.scala new file mode 100644 index 00000000000..5a00eee3f8d --- /dev/null +++ b/samples/server-generator/node/NodeServerFromSpec.scala @@ -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")) +} diff --git a/samples/server-generator/node/templates/Common/node/paramTypes.js b/samples/server-generator/node/templates/Common/node/paramTypes.js new file mode 100755 index 00000000000..f73ffee2b8b --- /dev/null +++ b/samples/server-generator/node/templates/Common/node/paramTypes.js @@ -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" + }; +}; \ No newline at end of file diff --git a/samples/server-generator/node/templates/Common/node/randomizer.js b/samples/server-generator/node/templates/Common/node/randomizer.js new file mode 100755 index 00000000000..eea17b8223b --- /dev/null +++ b/samples/server-generator/node/templates/Common/node/randomizer.js @@ -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=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; diff --git a/samples/server-generator/node/templates/api.mustache b/samples/server-generator/node/templates/api.mustache new file mode 100644 index 00000000000..2327e031bdc --- /dev/null +++ b/samples/server-generator/node/templates/api.mustache @@ -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}} \ No newline at end of file diff --git a/samples/server-generator/node/templates/main.mustache b/samples/server-generator/node/templates/main.mustache new file mode 100644 index 00000000000..36f0b5f20b8 --- /dev/null +++ b/samples/server-generator/node/templates/main.mustache @@ -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); diff --git a/samples/server-generator/node/templates/models.mustache b/samples/server-generator/node/templates/models.mustache new file mode 100644 index 00000000000..c1e87e5767c --- /dev/null +++ b/samples/server-generator/node/templates/models.mustache @@ -0,0 +1,5 @@ +exports.models = { + {{#models}} + "{{modelName}}": {{{modelJson}}}{{#hasMore}},{{/hasMore}} +{{/models}} +} \ No newline at end of file diff --git a/samples/server-generator/node/templates/package.json b/samples/server-generator/node/templates/package.json new file mode 100755 index 00000000000..871f6a5c3ff --- /dev/null +++ b/samples/server-generator/node/templates/package.json @@ -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" + } +}