diff --git a/bower.json b/bower.json index 2769a82370..8afe312ae1 100644 --- a/bower.json +++ b/bower.json @@ -38,13 +38,13 @@ "gemoji": "git://github.com/github/gemoji", "sticky": "*", "bootstrap-tour": "~0.8.0", - "angular-ui-utils": "~0.1.0" + "angular-ui-utils": "~0.1.0", + "swagger-ui": "~2.0.3" }, "resolutions": { "jquery": "~2.0.3", "bootstrap": "v2.3.2", - "angular": "~1.2.1", - "angular-ui-utils": "~0.1.0" + "angular": "~1.2.1" }, "devDependencies": { "angular-mocks": "~1.2.1" diff --git a/package.json b/package.json index 9e2ac50493..a187a7f85d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "pretty-data": "git://github.com/vkiryukhin/pretty-data#master", "js2xmlparser": "~0.1.2", "mongoose": "~3.8.1", - "domain-middleware": "~0.1.0" + "domain-middleware": "~0.1.0", + "swagger-node-express": "~1.3.2" }, "private": true, "subdomain": "habitrpg", diff --git a/src/apidoc.coffee b/src/apidoc.coffee new file mode 100644 index 0000000000..430712519d --- /dev/null +++ b/src/apidoc.coffee @@ -0,0 +1,225 @@ +# see https://github.com/wordnik/swagger-node-express + +_ = require('lodash') + +module.exports = (swagger) -> + + swagger.configureSwaggerPaths("", "/api-docs", "") + + api = + "/content": + description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon)." + method: 'GET' + + "/export/history": + description: "Export user history" + method: 'GET' + + # --------------------------------- + # User + # --------------------------------- + + # Scoring + + "/user/tasks/{id}/{direction}": + description: "Simple scoring of a task" + params: [ + swagger.pathParam("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string") + swagger.pathParam("direction", "Either 'up' or 'down'", "string") + ] + method: 'POST' + + # Tasks + "/user/tasks": + description: "Get all user's tasks" + + "/user/tasks/{id}": + description: "Get an individual task" + params: [ + swagger.pathParam("id", "Task ID", "string") + ] + + "/user/tasks/{id}": + description: "Update a user's task" + method: 'PUT' + params: [ + swagger.pathParam("id", "Task ID", "string") + ] + body: [ + swagger.bodyParam("task","Send up the whole task","string") + ] + + "/user/tasks/{id}": + method: 'DELETE' + "/user/tasks": + method: 'POST' + #body={} + "/user/tasks/{id}/sort": + method: 'POST' + #query={to,from} + "/user/tasks/clear-completed": + method: 'POST' + "/user/tasks/{id}/unlink": + method: 'POST' + + # Inventory + "/user/inventory/buy/{key}": + method: 'POST' + "/user/inventory/sell/{type}/{key}": + method: 'POST' + "/user/inventory/purchase/{type}/{key}": + method: 'POST' + "/user/inventory/feed/{pet}/{food}": + method: 'POST' + "/user/inventory/equip/{type}/{key}": + method: 'POST' + "/user/inventory/hatch/{egg}/{hatchingPotion}": + method: 'POST' + + # User + "/user:GET": + path: '/user' + "/user:PUT": + path: '/user' + method: 'PUT' + # body={} + "/user:DELETE": + path: '/user' + method: 'DELETE' + "/user/revive": + method: 'POST' + "/user/reroll": + method: 'POST' + "/user/reset": + method: 'POST' + "/user/sleep": + method: 'POST' + "/user/rebirth": + method: 'POST' + "/user/class/change": + method: 'POST' + #query={class} + "/user/class/allocate": + method: 'POST' + #query={stat} + "/user/class/cast/:spell": + method: 'POST' + "/user/unlock": + method: 'POST' + "/user/buy-gems": + method: 'POST' + "/user/batch-update": + method: 'POST' + + # Tags + "/user/tags": + method: 'POST' + #body={} + "/user/tags/{id}:PUT": + path: 'user/tags/{id}' + method: 'PUT' + #body={} + "/user/tags/{id}:DELETE": + path: 'user/tags/{id}' + method: 'DELETE' + + # --------------------------------- + # Groups + # --------------------------------- + "/groups:GET": + path: '/groups' + "/groups:POST": + path: '/groups' + method: 'POST' + "/groups/{gid}:GET": + path: '/groups/{gid}' + "/groups/{gid}:POST": + path: '/groups/{gid}' + method: 'POST' + "/groups/{gid}": + path: '/groups/{gid}' + method: 'PUT' + + "/groups/{gid}/join": + method: 'POST' + "/groups/{gid}/leave": + method: 'POST' + "/groups/{gid}/invite": + method: 'POST' + "/groups/{gid}/removeMember": + method: 'POST' + "/groups/{gid}/questAccept": + method: 'POST' + # query={key} (optional. if provided, trigger new invite, if not, accept existing invite) + "/groups/{gid}/questReject": + method: 'POST' + "/groups/{gid}/questAbort": + method: 'POST' + + #GET /groups/:gid/chat + "/groups/{gid}/chat": + method: 'POST' + "/groups/{gid}/chat/{messageId}": + method: 'DELETE' + + + # --------------------------------- + # Members + # --------------------------------- + "/members/{uid}":{} + + # --------------------------------- + # Challenges + # --------------------------------- + + # Note: while challenges belong to groups, and would therefore make sense as a nested resource + # (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab + # without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource + "/challenges:GET": + path: '/challenges' + "/challenges:POST": + path: '/challenges' + method: 'POST' + "/challenges/{cid}:GET": {} + "/challenges/{cid}:POST": + path: '/challenges/{cid}' + method: 'POST' + "/challenges/{cid}:DELETE": + path: '/challenges/{cid}' + method: 'DELETE' + "/challenges/{cid}/close": + method: 'POST' + "/challenges/{cid}/join": + method: 'POST' + "/challenges/{cid}/leave": + method: 'POST' + "/challenges/{cid}/member/{uid}":{} + + _.each api, (spec, path) -> + ## Spec format is: + # spec: + # path: "/pet/{petId}" + # description: "Operations about pets" + # notes: "Returns a pet based on ID" + # summary: "Find pet by ID" + # method: "GET" + # params: [swagger.pathParam("petId", "ID of pet that needs to be fetched", "string")] + # type: "Pet" + # errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")] + # nickname: "getPetById" + + spec.description ?= '' + _.defaults spec, + path: path + nickname: path + notes: spec.description + summary: spec.description + params: [] + #type: 'Pet' + errorResponses: [] + method: 'GET' + route = {spec} + console.log(spec.params) + swagger["add#{route.spec.method}"](route);true + + swagger.configure("http://localhost:3000", "0.1") \ No newline at end of file diff --git a/src/routes/pages.js b/src/routes/pages.js index 72e3f4fb10..de22cf95c8 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -31,6 +31,10 @@ router.get('/static/terms', middleware.locals, function(req, res) { res.render('static/terms', {env: res.locals.habitrpg}); }); +router.get('/static/api', middleware.locals, function(req, res) { + res.render('static/api', {env: res.locals.habitrpg}); +}); + // --------- Redirects -------- router.get('/splash.html', function(req, res) { diff --git a/src/server.js b/src/server.js index 91fa6ea9a9..3d5ad385a2 100644 --- a/src/server.js +++ b/src/server.js @@ -8,6 +8,7 @@ var nconf = require('nconf'); var utils = require('./utils'); var middleware = require('./middleware'); var domainMiddleware = require('domain-middleware'); +var swagger = require("swagger-node-express"); var server; var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14; @@ -110,6 +111,9 @@ app.use('/export', require('./routes/dataexport').middleware); app.use(utils.errorHandler); +swagger.setAppHandler(app); +require('./apidoc.coffee')(swagger); + server = http.createServer(app).listen(app.get("port"), function() { return console.log("Express server listening on port " + app.get("port")); }); diff --git a/views/static/api.jade b/views/static/api.jade new file mode 100644 index 0000000000..ad44676b3b --- /dev/null +++ b/views/static/api.jade @@ -0,0 +1,69 @@ +!!! 5 +html + head + title Swagger UI + link(href='//fonts.googleapis.com/css?family=Droid+Sans:400,700', rel='stylesheet', type='text/css') + link(href='/bower_components/swagger-ui/dist/css/highlight.default.css', media='screen', rel='stylesheet', type='text/css') + link(href='/bower_components/swagger-ui/dist/css/screen.css', media='screen', rel='stylesheet', type='text/css') + script(src='/bower_components/swagger-ui/dist/lib/shred.bundle.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery-1.8.0.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery.slideto.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery.wiggle.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/jquery.ba-bbq.min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/handlebars-1.0.0.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/underscore-min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/backbone-min.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/swagger.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/swagger-ui.js', type='text/javascript') + script(src='/bower_components/swagger-ui/dist/lib/highlight.7.3.pack.js', type='text/javascript') + script(type='text/javascript') + $(function () { + window.swaggerUi = new SwaggerUi({ + url: "/api-docs", + dom_id: "swagger-ui-container", + supportedSubmitMethods: ['get', 'post', 'put', 'delete'], + onComplete: function(swaggerApi, swaggerUi){ + if(console) { + console.log("Loaded SwaggerUI") + } + $('pre code').each(function(i, e) {hljs.highlightBlock(e)}); + }, + onFailure: function(data) { + if(console) { + console.log("Unable to Load SwaggerUI"); + console.log(data); + } + }, + docExpansion: "none" + }); + + $('#input_apiKey').change(function() { + var key = $('#input_apiKey')[0].value; + console.log("key: " + key); + if(key && key.trim() != "") { + console.log("added key " + key); + window.authorizations.add("key", new ApiKeyAuthorization("api_key", key, "query")); + } + }) + window.swaggerUi.load(); + }); + body + #header + .swagger-ui-wrap + a#logo(href='http://swagger.wordnik.com') swagger + //-form#api_selector + //- +
+
+