summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn McLear <john@mclear.co.uk>2013-03-07 08:07:46 -0800
committerJohn McLear <john@mclear.co.uk>2013-03-07 08:07:46 -0800
commit0b16d96c970db4ba6c43afd1763b3dde818006c2 (patch)
tree346a2adfa11c3811728d6eb30db9e32dd437d2b0
parent732ad2878fc1542b03612c56d71922862f8d5ac3 (diff)
parenta5987285e01413ab9f0921011ab57d2615025b63 (diff)
downloadetherpad-lite-0b16d96c970db4ba6c43afd1763b3dde818006c2.zip
Merge pull request #1527 from nelsonsilva/swagger
Initial work on new swagger based REST API
-rw-r--r--src/ep.json3
-rw-r--r--src/node/handler/APIHandler.js5
-rw-r--r--src/node/hooks/express/swagger.js431
-rw-r--r--src/package.json3
4 files changed, 440 insertions, 2 deletions
diff --git a/src/ep.json b/src/ep.json
index 89c8964a..eeb5c640 100644
--- a/src/ep.json
+++ b/src/ep.json
@@ -23,6 +23,7 @@
{ "name": "adminsettings", "hooks": {
"expressCreateServer": "ep_etherpad-lite/node/hooks/express/adminsettings:expressCreateServer",
"socketio": "ep_etherpad-lite/node/hooks/express/adminsettings:socketio" }
- }
+ },
+ { "name": "swagger", "hooks": { "expressCreateServer": "ep_etherpad-lite/node/hooks/express/swagger:expressCreateServer" } }
]
}
diff --git a/src/node/handler/APIHandler.js b/src/node/handler/APIHandler.js
index 8be5b5fe..4b7dd951 100644
--- a/src/node/handler/APIHandler.js
+++ b/src/node/handler/APIHandler.js
@@ -219,6 +219,9 @@ var version =
// set the latest available API version here
exports.latestApiVersion = '1.2.7';
+// exports the versions so it can be used by the new Swagger endpoint
+exports.version = version;
+
/**
* Handles a HTTP API call
* @param functionName the name of the called function
@@ -266,6 +269,8 @@ exports.handle = function(apiVersion, functionName, fields, req, res)
}
//check the api key!
+ fields["apikey"] = fields["apikey"] || fields["api_key"];
+
if(fields["apikey"] != apikey.trim())
{
res.send({code: 4, message: "no or wrong API Key", data: null});
diff --git a/src/node/hooks/express/swagger.js b/src/node/hooks/express/swagger.js
new file mode 100644
index 00000000..f4fc5cff
--- /dev/null
+++ b/src/node/hooks/express/swagger.js
@@ -0,0 +1,431 @@
+var log4js = require('log4js');
+var express = require('express');
+var apiHandler = require('../../handler/APIHandler');
+var apiCaller = require('./apicalls').apiCaller;
+var settings = require("../../utils/Settings");
+
+var swaggerModels = {
+ 'models': {
+ 'SessionInfo' : {
+ "id": 'SessionInfo',
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "authorID": {
+ "type": "string"
+ },
+ "groupID":{
+ "type":"string"
+ },
+ "validUntil":{
+ "type":"long"
+ }
+ }
+ },
+ 'UserInfo' : {
+ "id": 'UserInfo',
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "colorId": {
+ "type": "string"
+ },
+ "name":{
+ "type":"string"
+ },
+ "timestamp":{
+ "type":"long"
+ }
+ }
+ },
+ 'Message' : {
+ "id": 'Message',
+ "properties": {
+ "text": {
+ "type": "string"
+ },
+ "userId": {
+ "type": "string"
+ },
+ "userName":{
+ "type":"string"
+ },
+ "time":{
+ "type":"long"
+ }
+ }
+ }
+ }
+};
+
+function sessionListResponseProcessor(res) {
+ if (res.data) {
+ var sessions = [];
+ for (var sessionId in res.data) {
+ var sessionInfo = res.data[sessionId];
+ sessionId["id"] = sessionId;
+ sessions.push(sessionInfo);
+ }
+ res.data = sessions;
+ }
+
+ return res;
+}
+
+// We'll add some more info to the API methods
+var API = {
+
+ // Group
+ "group": {
+ "create" : {
+ "func" : "createGroup",
+ "description": "creates a new group",
+ "response": {"groupID":{"type":"string"}}
+ },
+ "createIfNotExistsFor" : {
+ "func": "createGroupIfNotExistsFor",
+ "description": "this functions helps you to map your application group ids to etherpad lite group ids",
+ "response": {"groupID":{"type":"string"}}
+ },
+ "delete" : {
+ "func": "deleteGroup",
+ "description": "deletes a group"
+ },
+ "listPads" : {
+ "func": "listPads",
+ "description": "returns all pads of this group",
+ "response": {"padIDs":{"type":"List", "items":{"type":"string"}}}
+ },
+ "createPad" : {
+ "func": "createGroupPad",
+ "description": "creates a new pad in this group"
+ },
+ "listSessions": {
+ "func": "listSessionsOfGroup",
+ "responseProcessor": sessionListResponseProcessor,
+ "description": "",
+ "response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}}
+ },
+ "list": {
+ "func": "listAllGroups",
+ "description": "",
+ "response": {"groupIDs":{"type":"List", "items":{"type":"string"}}}
+ },
+ },
+
+ // Author
+ "author": {
+ "create" : {
+ "func" : "createAuthor",
+ "description": "creates a new author",
+ "response": {"authorID":{"type":"string"}}
+ },
+ "createIfNotExistsFor": {
+ "func": "createAuthorIfNotExistsFor",
+ "description": "this functions helps you to map your application author ids to etherpad lite author ids",
+ "response": {"authorID":{"type":"string"}}
+ },
+ "listPads": {
+ "func": "listPadsOfAuthor",
+ "description": "returns an array of all pads this author contributed to",
+ "response": {"padIDs":{"type":"List", "items":{"type":"string"}}}
+ },
+ "listSessions": {
+ "func": "listSessionsOfAuthor",
+ "responseProcessor": sessionListResponseProcessor,
+ "description": "returns all sessions of an author",
+ "response": {"sessions":{"type":"List", "items":{"type":"SessionInfo"}}}
+ },
+ // We need an operation that return a UserInfo so it can be picked up by the codegen :(
+ "getName" : {
+ "func": "getAuthorName",
+ "description": "Returns the Author Name of the author",
+ "responseProcessor": function(response) {
+ if (response.data) {
+ response["info"] = {"name": response.data.authorName};
+ delete response["data"];
+ }
+ },
+ "response": {"info":{"type":"UserInfo"}}
+ }
+ },
+ "session": {
+ "create" : {
+ "func": "createSession",
+ "description": "creates a new session. validUntil is an unix timestamp in seconds",
+ "response": {"sessionID":{"type":"string"}}
+ },
+ "delete" : {
+ "func": "deleteSession",
+ "description": "deletes a session"
+ },
+ // We need an operation that returns a SessionInfo so it can be picked up by the codegen :(
+ "info": {
+ "func": "getSessionInfo",
+ "description": "returns informations about a session",
+ "responseProcessor": function(response) {
+ // move this to info
+ if (response.data) {
+ response["info"] = response.data;
+ delete response["data"];
+ }
+ },
+ "response": {"info":{"type":"SessionInfo"}}
+ }
+ },
+ "pad": {
+ "listAll" : {
+ "func": "listAllPads",
+ "description": "list all the pads",
+ "response": {"padIDs":{"type":"List", "items": {"type" : "string"}}}
+ },
+ "createDiffHTML" : {
+ "func" : "createDiffHTML",
+ "description": "",
+ "response": {}
+ },
+ "create" : {
+ "func" : "createPad",
+ "description": "creates a new (non-group) pad. Note that if you need to create a group Pad, you should call createGroupPad"
+ },
+ "getText" : {
+ "func" : "getText",
+ "description": "returns the text of a pad",
+ "response": {"text":{"type":"string"}}
+ },
+ "setText" : {
+ "func" : "setText",
+ "description": "sets the text of a pad"
+ },
+ "getHTML": {
+ "func" : "getHTML",
+ "description": "returns the text of a pad formatted as HTML",
+ "response": {"html":{"type":"string"}}
+ },
+ "setHTML": {
+ "func" : "setHTML",
+ "description": "sets the text of a pad with HTML"
+ },
+ "getRevisionsCount": {
+ "func" : "getRevisionsCount",
+ "description": "returns the number of revisions of this pad",
+ "response": {"revisions":{"type":"long"}}
+ },
+ "getLastEdited": {
+ "func" : "getLastEdited",
+ "description": "returns the timestamp of the last revision of the pad",
+ "response": {"lastEdited":{"type":"long"}}
+ },
+ "delete": {
+ "func" : "deletePad",
+ "description": "deletes a pad"
+ },
+ "getReadOnlyID": {
+ "func" : "getReadOnlyID",
+ "description": "returns the read only link of a pad",
+ "response": {"readOnlyID":{"type":"string"}}
+ },
+ "setPublicStatus": {
+ "func": "setPublicStatus",
+ "description": "sets a boolean for the public status of a pad"
+ },
+ "getPublicStatus": {
+ "func": "getPublicStatus",
+ "description": "return true of false",
+ "response": {"publicStatus":{"type":"boolean"}}
+ },
+ "setPassword": {
+ "func": "setPassword",
+ "description": "returns ok or a error message"
+ },
+ "isPasswordProtected": {
+ "func": "isPasswordProtected",
+ "description": "returns true or false",
+ "response": {"passwordProtection":{"type":"boolean"}}
+ },
+ "authors": {
+ "func": "listAuthorsOfPad",
+ "description": "returns an array of authors who contributed to this pad",
+ "response": {"authorIDs":{"type":"List", "items":{"type" : "string"}}}
+ },
+ "usersCount": {
+ "func": "padUsersCount",
+ "description": "returns the number of user that are currently editing this pad",
+ "response": {"padUsersCount":{"type": "long"}}
+ },
+ "users": {
+ "func": "padUsers",
+ "description": "returns the list of users that are currently editing this pad",
+ "response": {"padUsers":{"type":"List", "items":{"type": "UserInfo"}}}
+ },
+ "sendClientsMessage": {
+ "func": "sendClientsMessage",
+ "description": "sends a custom message of type msg to the pad"
+ },
+ "checkToken" : {
+ "func": "checkToken",
+ "description": "returns ok when the current api token is valid"
+ },
+ "getChatHistory": {
+ "func": "getChatHistory",
+ "description": "returns the chat history",
+ "response": {"messages":{"type":"List", "items": {"type" : "Message"}}}
+ },
+ // We need an operation that returns a Message so it can be picked up by the codegen :(
+ "getChatHead": {
+ "func": "getChatHead",
+ "description": "returns the chatHead (chat-message) of the pad",
+ "responseProcessor": function(response) {
+ // move this to info
+ if (response.data) {
+ response["chatHead"] = {"time": response.data["chatHead"]};
+ delete response["data"];
+ }
+ },
+ "response": {"chatHead":{"type":"Message"}}
+ }
+ }
+};
+
+function capitalise(string){
+ return string.charAt(0).toUpperCase() + string.slice(1);
+}
+
+for (var resource in API) {
+ for (var func in API[resource]) {
+
+ // The base response model
+ var responseModel = {
+ "properties": {
+ "code":{
+ "type":"int"
+ },
+ "message":{
+ "type":"string"
+ }
+ }
+ };
+
+ var responseModelId = "Response";
+
+ // Add the data properties (if any) to the response
+ if (API[resource][func]["response"]) {
+ // This is a specific response so let's set a new id
+ responseModelId = capitalise(resource) + capitalise(func) + "Response";
+
+ for(var prop in API[resource][func]["response"]) {
+ var propType = API[resource][func]["response"][prop];
+ responseModel["properties"][prop] = propType;
+ }
+ }
+
+ // Add the id
+ responseModel["id"] = responseModelId;
+
+ // Add this to the swagger models
+ swaggerModels['models'][responseModelId] = responseModel;
+
+ // Store the response model id
+ API[resource][func]["responseClass"] = responseModelId;
+
+ }
+}
+
+function newSwagger() {
+ var swagger_module = require.resolve("swagger-node-express");
+ if (require.cache[swagger_module]) {
+ // delete the child modules from cache
+ require.cache[swagger_module].children.forEach(function(m) {delete require.cache[m.id];});
+ // delete the module from cache
+ delete require.cache[swagger_module];
+ }
+ return require("swagger-node-express");
+}
+
+exports.expressCreateServer = function (hook_name, args, cb) {
+
+ for (var version in apiHandler.version) {
+
+ var swagger = newSwagger();
+ var basePath = "/rest/" + version;
+
+ // Let's put this under /rest for now
+ var subpath = express();
+
+ args.app.use(express.bodyParser());
+ args.app.use(basePath, subpath);
+
+ swagger.setAppHandler(subpath);
+
+ swagger.addModels(swaggerModels);
+
+ for (var resource in API) {
+
+ for (var funcName in API[resource]) {
+ var func = API[resource][funcName];
+
+ // get the api function
+ var apiFunc = apiHandler.version[version][func["func"]];
+
+ // Skip this one if it does not exist in the version
+ if(!apiFunc) {
+ continue;
+ }
+
+ var swaggerFunc = {
+ 'spec': {
+ "description" : func["description"],
+ "path" : "/" + resource + "/" + funcName,
+ "summary" : funcName,
+ "nickname" : funcName,
+ "method": "GET",
+ "params" : apiFunc.map( function(param) {
+ return swagger.queryParam(param, param, "string");
+ }),
+ "responseClass" : func["responseClass"]
+ },
+ 'action': (function(func, responseProcessor) {
+ return function (req,res) {
+ req.params.version = version;
+ req.params.func = func; // call the api function
+
+ //wrap the send function so we can process the response
+ res.__swagger_send = res.send;
+ res.send = function (response) {
+ // ugly but we need to get this as json
+ response = JSON.parse(response);
+ // process the response if needed
+ if (responseProcessor) {
+ response = responseProcessor(response);
+ }
+ // Let's move everything out of "data"
+ if (response.data) {
+ for(var prop in response.data) {
+ response[prop] = response.data[prop];
+ delete response.data;
+ }
+ }
+ response = JSON.stringify(response);
+ res.__swagger_send(response);
+ };
+
+ apiCaller(req, res, req.query);
+ };
+ })(func["func"], func["responseProcessor"]) // must use a closure here
+ };
+
+ swagger.addGet(swaggerFunc);
+ }
+ }
+
+ swagger.setHeaders = function setHeaders(res) {
+ res.header('Access-Control-Allow-Origin', "*");
+ };
+
+ swagger.configureSwaggerPaths("", "/api" , "");
+
+ swagger.configure("http://" + settings.ip + ":" + settings.port + basePath, version);
+ }
+};
diff --git a/src/package.json b/src/package.json
index 13a4de4c..0e418ede 100644
--- a/src/package.json
+++ b/src/package.json
@@ -36,7 +36,8 @@
"tinycon" : "0.0.1",
"underscore" : "1.3.1",
"unorm" : "1.0.0",
- "languages4translatewiki" : "0.1.3"
+ "languages4translatewiki" : "0.1.3",
+ "swagger-node-express" : "1.2.3"
},
"bin": { "etherpad-lite": "./node/server.js" },
"devDependencies": {