From 0342077467258b765c91e1dc31165871492c3873 Mon Sep 17 00:00:00 2001 From: Sebastian Tiedtke Date: Mon, 17 Sep 2018 16:27:12 +0200 Subject: [PATCH] Added swagger spec and UI for API docs, plus minor improvements to error handling --- package-lock.json | 26 ++++++++- package.json | 4 +- services/nodevoto-web/app.js | 15 +++-- services/nodevoto-web/swagger.yaml | 89 ++++++++++++++++++++++++++++++ test/nodevoto-web/app.test.js | 9 +++ 5 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 services/nodevoto-web/swagger.yaml diff --git a/package-lock.json b/package-lock.json index b1be8af..3297059 100644 --- a/package-lock.json +++ b/package-lock.json @@ -181,7 +181,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "1.0.3" } @@ -2346,8 +2345,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "stack-trace": { "version": "0.0.10", @@ -2414,6 +2412,19 @@ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true }, + "swagger-ui-dist": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-3.19.0.tgz", + "integrity": "sha512-4/S7ZP8uPVn3vrCls46QG6RF4paqRFkDBcfSlyFetZHdPcjAu0hr38ZkaG4n8689R0tiqWDiubnEQUII+ulymA==" + }, + "swagger-ui-express": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.0.1.tgz", + "integrity": "sha512-KCwnxnn27Me9dBjD5nMjhfrw6WaC/Ad/AaZw6YgIICLirFKCeG23s2Vf0VP/Y3TNt/xmJRvg6rMXN9T7CXClHQ==", + "requires": { + "swagger-ui-dist": "3.19.0" + } + }, "symbol-observable": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", @@ -2623,6 +2634,15 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=" }, + "yamljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/yamljs/-/yamljs-0.3.0.tgz", + "integrity": "sha512-C/FsVVhht4iPQYXOInoxUM/1ELSf9EsgKH34FofQOp6hwCPrW4vG4w5++TED3xRUo8gD7l0P1J1dLlDYzODsTQ==", + "requires": { + "argparse": "1.0.10", + "glob": "7.1.3" + } + }, "yargs": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.32.0.tgz", diff --git a/package.json b/package.json index b666300..e467c84 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,9 @@ "express": "^4.16.3", "grpc": "^1.14.1", "superagent": "^3.8.3", - "winston": "^3.1.0" + "swagger-ui-express": "^4.0.1", + "winston": "^3.1.0", + "yamljs": "^0.3.0" }, "devDependencies": { "eslint": "^5.4.0", diff --git a/services/nodevoto-web/app.js b/services/nodevoto-web/app.js index 2dd700b..a1467a7 100644 --- a/services/nodevoto-web/app.js +++ b/services/nodevoto-web/app.js @@ -6,6 +6,10 @@ const bodyParser = require('body-parser'); const logger = require('../../lib/logger'); const shortcode = require('../../lib/shortcode.json'); +const swaggerUi = require('swagger-ui-express'); +const YAML = require('yamljs'); +const swaggerDocument = YAML.load(path.join(__dirname, './swagger.yaml')); + const wrapOp = (op) => { return (arg) => { let p = new Promise((res, rej) => { @@ -77,16 +81,18 @@ class App { async handleVoteEmoji(req, res) { let emojiShortcode = req.query['choice']; if (emojiShortcode === undefined || emojiShortcode === '') { - logger.error(`Emoji choice [${emojiShortcode}] is mandatory`); - return res.status(400).end(); + let errmsg = `Emoji choice [${emojiShortcode}] is mandatory`; + logger.error(errmsg); + return res.status(400).json(errmsg); } try { let match = await this._FindByShortcode({ Shortcode: emojiShortcode }); if (match === null) { - logger.error(`Choosen emoji shortcode [${emojiShortcode}] doesnt exist`); - return res.status(400).end(); + let errmsg = `Choosen emoji shortcode [${emojiShortcode}] doesnt exist`; + logger.error(errmsg); + return res.status(400).json(errmsg); } let operation = Object.entries(shortcode).filter(sc => { @@ -171,6 +177,7 @@ module.exports.create = async(webPort, webpackDevServerHost, indexBundle, emojiC app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use('/', routes); + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); new App(routes, webPort, webpackDevServerHost, indexBundle, emojiClient, votingClient); diff --git a/services/nodevoto-web/swagger.yaml b/services/nodevoto-web/swagger.yaml new file mode 100644 index 0000000..c5dc239 --- /dev/null +++ b/services/nodevoto-web/swagger.yaml @@ -0,0 +1,89 @@ +swagger: "2.0" +info: + description: "Node.js implementation of emojivoto" + version: "1.0.0" + title: "nodevoto" + termsOfService: "" + contact: + email: "sebastiantiedtke@gmail.com" + license: + name: "Apache 2.0" + url: "http://www.apache.org/licenses/LICENSE-2.0.html" +host: "localhost:8080" +basePath: "/api" +schemes: +- "http" +paths: + /leaderboard: + get: + tags: + - "nodevoto" + summary: Get leaderboard + description: Returns list of emoji in descending order. + produces: + - "application/json" + responses: + 200: + description: Ordered list of emoji + schema: + type: array + items: + required: + - shortcode + - unicode + - votes + properties: + shortcode: + type: string + unicode: + type: string + votes: + type: integer + /vote: + get: + tags: + - "nodevoto" + summary: "Cast vote for specific emoji" + description: "Cast vote for emoji based on choice parameter provided" + produces: + - "application/json" + parameters: + - name: "choice" + in: "query" + description: "Shortcode of specific emoji to cast vote for" + required: true + type: "string" + enum: [":100:",":bacon:",":balloon:",":basketball_man:",":beach_umbrella:",":beer:",":biking_man:",":bowing_man:",":boy:",":bride_with_veil:",":bulb:",":burrito:",":call_me_hand:",":camping:",":cat2:",":champagne:",":checkered_flag:",":clap:",":cloud_with_rain:",":construction_worker_man:",":crossed_swords:",":crystal_ball:",":dancer:",":dancing_women:",":dog:",":doughnut:",":fax:",":fire:",":flight_departure:",":floppy_disk:",":flushed:",":ghost:",":girl:",":golfing_man:",":guardsman:",":hatching_chick:",":hear_no_evil:",":heart_eyes_cat:",":interrobang:",":iphone:",":jack_o_lantern:",":joy:",":man:",":man_dancing:",":man_facepalming:",":man_in_tuxedo:",":mask:",":massage_woman:",":metal:",":money_mouth_face:",":money_with_wings:",":mountain_snow:",":mrs_claus:",":nerd_face:",":no_good_woman:",":ok_woman:",":older_man:",":pager:",":pig:",":pizza:",":point_up_2:",":policeman:",":poop:",":pray:",":prince:",":princess:",":rabbit:",":rainbow:",":raised_hands:",":raising_hand_woman:",":ramen:",":relaxed:",":rocket:",":running_man:",":santa:",":see_no_evil:",":skier:",":skull_and_crossbones:",":snail:",":speak_no_evil:",":star2:",":steam_locomotive:",":stuck_out_tongue_winking_eye:",":sun_behind_small_cloud:",":sunglasses:",":surfing_man:",":taco:",":tada:",":thumbsup:",":trophy:",":tropical_drink:",":tumbler_glass:",":turkey:",":underage:",":vulcan_salute:",":walking_man:",":wave:",":woman:",":woman_shrugging:",":world_map:"] + responses: + 200: + description: "successful operation" + schema: + type: "object" + 400: + description: "Choosen emoji shortcode doesnt exist" + 500: + description: "Unknown Error" + /list: + get: + tags: + - "nodevoto" + summary: "Fetch all emoji supported" + description: "Returns list of all emoji supported on the ballot" + produces: + - "application/json" + responses: + 200: + description: "successful operation" + schema: + type: array + items: + required: + - unicode + - shortcode + properties: + unicode: + type: string + shortcode: + type: string + 500: + description: "Unknown Error" \ No newline at end of file diff --git a/test/nodevoto-web/app.test.js b/test/nodevoto-web/app.test.js index 5d2ca97..1973e7a 100644 --- a/test/nodevoto-web/app.test.js +++ b/test/nodevoto-web/app.test.js @@ -176,4 +176,13 @@ describe('app', () => { }); }); + describe('api docs', () => { + it('should serve the swagger based UI for easy API exploration', async() => { + let response = await superget(`http://127.0.0.1:${WEB_PORT}/api-docs`); + + expect(response.status).equals(200); + expect(response.text).contains('HTML for static distribution bundle build'); + }); + }); + });