Merge pull request #6256 from HabitRPG/api-v3-apidoc

API v3 - apidoc wip
This commit is contained in:
Matteo Pagliazzi
2015-11-18 17:12:14 +01:00
16 changed files with 208 additions and 32 deletions
+1
View File
@@ -1,6 +1,7 @@
.DS_Store
website/public/gen
website/public/common
website/public/apidoc
node_modules
*.swp
.idea*
+10 -1
View File
@@ -1,12 +1,13 @@
{
"name": "habitrpg",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "0.0.0-152",
"version": "3.0.0-alpha",
"main": "./website/src/index.js",
"dependencies": {
"accepts": "^1.3.0",
"amazon-payments": "0.0.4",
"amplitude": "^2.0.3",
"apidoc": "^0.13.1",
"async": "^1.5.0",
"aws-sdk": "^2.0.25",
"babel-core": "^5.8.34",
@@ -119,6 +120,7 @@
"mongodb": "^2.0.46",
"mongoskin": "~0.6.1",
"nock": "^2.17.0",
"phantomjs": "^1.9.18",
"protractor": "~2.5.1",
"rewire": "^2.3.3",
"rimraf": "^2.4.3",
@@ -130,5 +132,12 @@
"uuid": "^2.0.1",
"vinyl-source-stream": "^1.0.0",
"vinyl-transform": "^1.0.0"
},
"apidoc": {
"name": "habitica",
"title": "Habitica",
"version": "3.0.0",
"url": "https://habitica.com/api/v3",
"sampleUrl": "https://habitica.com/api/v3"
}
}
+22
View File
@@ -0,0 +1,22 @@
import gulp from 'gulp';
import clean from 'rimraf';
import apidoc from 'apidoc';
const APIDOC_DEST_PATH = './website/public/apidoc';
const APIDOC_SRC_PATH = './website/src';
gulp.task('apidoc:clean', (done) => {
clean(APIDOC_DEST_PATH, done);
});
gulp.task('apidoc', ['apidoc:clean'], (done) => {
let result = apidoc.createDoc({
src: APIDOC_SRC_PATH,
dest: APIDOC_DEST_PATH,
});
if (result === false) {
done(new Error('There was a problem generating apiDoc documentation.'))
} else {
done();
}
});
+1 -1
View File
@@ -17,6 +17,6 @@ gulp.task('build:dev:watch', ['build:dev'], () => {
gulp.watch(['website/public/**/*.styl', 'common/script/*']);
});
gulp.task('build:prod', ['browserify', 'prepare:staticNewStuff'], (done) => {
gulp.task('build:prod', ['browserify', 'prepare:staticNewStuff', 'apidoc'], (done) => {
gulp.start('grunt-build:prod', done);
});
+29 -2
View File
@@ -3,6 +3,7 @@ import {
NotAuthorized,
BadRequest,
InternalServerError,
NotFound,
} from '../../../../../website/src/libs/api-v3/errors';
describe('Custom Errors', () => {
@@ -21,7 +22,7 @@ describe('Custom Errors', () => {
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
});
it('it returns an http code of 400', () => {
it('it returns an http code of 401', () => {
let notAuthorizedError = new NotAuthorized();
expect(notAuthorizedError.httpCode).to.eql(401);
@@ -40,6 +41,32 @@ describe('Custom Errors', () => {
});
});
describe('NotFound', () => {
it('is an instance of CustomError', () => {
let notAuthorizedError = new NotFound();
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
});
it('it returns an http code of 404', () => {
let notAuthorizedError = new NotFound();
expect(notAuthorizedError.httpCode).to.eql(404);
});
it('returns a default message', () => {
let notAuthorizedError = new NotFound();
expect(notAuthorizedError.message).to.eql('Not found.');
});
it('allows a custom message', () => {
let notAuthorizedError = new NotFound('Custom Error Message');
expect(notAuthorizedError.message).to.eql('Custom Error Message');
});
});
describe('BadRequest', () => {
it('is an instance of CustomError', () => {
let badRequestError = new BadRequest();
@@ -82,7 +109,7 @@ describe('Custom Errors', () => {
it('returns a default message', () => {
let internalServerError = new InternalServerError();
expect(internalServerError.message).to.eql('Internal server error.');
expect(internalServerError.message).to.eql('An unexpected error occurred.');
});
it('allows a custom message', () => {
@@ -31,7 +31,7 @@ describe('errorHandler', () => {
expect(res.status).to.be.calledWith(500);
expect(res.json).to.be.calledWith({
error: 'InternalServerError',
message: 'Internal server error.',
message: 'An unexpected error occurred.',
});
});
@@ -63,7 +63,7 @@ describe('errorHandler', () => {
expect(res.status).to.be.calledWith(500);
expect(res.json).to.be.calledWith({
error: 'InternalServerError',
message: 'Internal server error.',
message: 'An unexpected error occurred.',
});
});
@@ -88,7 +88,7 @@ describe('errorHandler', () => {
errorHandler(error, req, res, next);
expect(logger.error).to.be.calledOnce;
expect(logger.error).to.be.calledWith(error.stack, {
expect(logger.error).to.be.calledWithExactly(error.stack, {
originalUrl: req.originalUrl,
headers: req.headers,
body: req.body,
@@ -0,0 +1,14 @@
import { requester } from '../../../../helpers/api-integration.helper';
describe('notFound Middleware', () => {
it('returns a 404 error when the resource is not found', () => {
let request = requester().get('/api/v3/dummy-url');
return request.then((errBody) => {
expect(errBody.error).to.equal('NotFound');
expect(errBody.message).to.equal('Not found.');
}).to.eventually.be.rejected.and.eql({
code: 404,
});
});
});
+23 -4
View File
@@ -1,15 +1,34 @@
// An example file to show how a controller should be structured
let api = {};
/**
* @api {get} /example/:id Request Example information
* @apiVersion 3.0.0
* @apiName GetExample
* @apiGroup Example
*
* @apiParam {Number} id Examples unique ID.
*
* @apiSuccess {String} firstname Firstname of the Example.
* @apiSuccess {String} lastname Lastname of the Example.
*
* @apiSuccessExample Success-Response:
* HTTP/1.1 200 OK
* {
* "firstname": "John",
* "lastname": "Doe"
* }
*
* @apiUse NotFound
*/
api.exampleRoute = {
method: 'GET',
url: '/example/:param',
url: '/example/:id',
middlewares: [],
handler (req, res) {
res.status(200).send({
status: 'ok',
status: req.params.id,
});
},
};
export default api;
export default api;
-2
View File
@@ -14,8 +14,6 @@ var IS_PROD = nconf.get('IS_PROD');
var IS_DEV = nconf.get('IS_DEV');
var cores = Number(nconf.get('WEB_CONCURRENCY')) || 0;
if (IS_DEV) Error.stackTraceLimit = Infinity;
// Setup the cluster module
if (cores !== 0 && cluster.isMaster && (IS_DEV || IS_PROD)) {
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
+54 -8
View File
@@ -7,8 +7,17 @@ export class CustomError extends Error {
}
}
// NotAuthorized error with a 401 http error code
// used when a request is not authorized
/**
* @apiDefine NotFound
* @apiError NotFound The client is not authorized to make this request.
*
* @apiErrorExample Error-Response:
* HTTP/1.1 401 Unauthorized
* {
* "error": "NotAuthorized",
* "message": "Not authorized."
* }
*/
export class NotAuthorized extends CustomError {
constructor (customMessage) {
super();
@@ -18,9 +27,17 @@ export class NotAuthorized extends CustomError {
}
}
// BadRequest error with a 400 http error code
// used for requests not formatted correctly
// TODO use for validation errors too?
/**
* @apiDefine BadRequest
* @apiError BadRequest The request wasn't formatted correctly.
*
* @apiErrorExample Error-Response:
* HTTP/1.1 400 Bad Request
* {
* "error": "BadRequest",
* "message": "Bad request."
* }
*/
export class BadRequest extends CustomError {
constructor (customMessage) {
super();
@@ -30,13 +47,42 @@ export class BadRequest extends CustomError {
}
}
// InternalError error with a 500 http error code
// used when an unexpected, internal server error is thrown
/**
* @apiDefine NotFound
* @apiError NotFound The requested resource was not found.
*
* @apiErrorExample Error-Response:
* HTTP/1.1 404 Not Found
* {
* "error": "NotFound",
* "message": "Not found."
* }
*/
export class NotFound extends CustomError {
constructor (customMessage) {
super();
this.name = this.constructor.name;
this.httpCode = 404;
this.message = customMessage || 'Not found.';
}
}
/**
* @apiDefine InternalServerError
* @apiError InternalServerError An unexpected error occurred.
*
* @apiErrorExample Error-Response:
* HTTP/1.1 500 Internal Server Error
* {
* "error": "InternalServerError",
* "message": "An unexpected error occurred."
* }
*/
export class InternalServerError extends CustomError {
constructor (customMessage) {
super();
this.name = this.constructor.name;
this.httpCode = 500;
this.message = customMessage || 'Internal server error.';
this.message = customMessage || 'An unexpected error occurred.';
}
}
+1
View File
@@ -14,6 +14,7 @@ if (IS_PROD) {
logger
.add(winston.transports.Console, {
colorize: true,
prettyPrint: true,
});
}
+2 -1
View File
@@ -2,6 +2,7 @@ import fs from 'fs';
import path from 'path';
import express from 'express';
import _ from 'lodash';
const CONTROLLERS_PATH = path.join(__dirname, '/../../controllers/api-v3/');
let router = express.Router(); // eslint-disable-line new-cap
@@ -20,4 +21,4 @@ fs
});
});
export default router;
export default router;
+10 -2
View File
@@ -4,9 +4,16 @@ import analytics from './analytics';
import errorHandler from './errorHandler';
import bodyParser from 'body-parser';
import routes from '../../libs/api-v3/setupRoutes';
import notFoundHandler from './notFound';
import nconf from 'nconf';
import morgan from 'morgan';
const IS_PROD = nconf.get('IS_PROD');
const DISABLE_LOGGING = nconf.get('DISABLE_REQUEST_LOGGING');
export default function attachMiddlewares (app) {
// Parse query parameters and json bodies
if (!IS_PROD && !DISABLE_LOGGING) app.use(morgan('dev'));
// TODO handle errors
app.use(bodyParser.urlencoded({
extended: true, // Uses 'qs' library as old connect middleware
@@ -14,7 +21,8 @@ export default function attachMiddlewares (app) {
app.use(bodyParser.json());
app.use(analytics);
app.use(routes);
app.use('/api/v3', routes);
app.use(notFoundHandler);
// Error handler middleware, define as the last one
app.use(errorHandler);
@@ -0,0 +1,7 @@
import {
NotFound,
} from '../../libs/api-v3/errors';
export default function (req, res, next) {
next(new NotFound());
}
+18
View File
@@ -0,0 +1,18 @@
import express from 'express';
import nconf from 'nconf';
import path from 'path';
const IS_PROD = nconf.get('IS_PROD');
const MAX_AGE = IS_PROD ? 31536000000 : 0;
const PUBLIC_DIR = path.join(__dirname, '/../../../public');
const BUILD_DIR = path.join(__dirname, '/../../../build');
export default function staticMiddleware (expressApp) {
// TODO move all static files to a single location (one for public and one for build)
expressApp.use(express.static(BUILD_DIR, { maxAge: MAX_AGE }));
expressApp.use('/common/dist', express.static(`${PUBLIC_DIR}/../../common/dist`, { maxAge: MAX_AGE }));
expressApp.use('/common/audio', express.static(`${PUBLIC_DIR}/../../common/audio`, { maxAge: MAX_AGE }));
expressApp.use('/common/script/public', express.static(`${PUBLIC_DIR}/../../common/script/public`, { maxAge: MAX_AGE }));
expressApp.use('/common/img', express.static(`${PUBLIC_DIR}/../../common/img`, { maxAge: MAX_AGE }));
expressApp.use(express.static(PUBLIC_DIR));
}
+13 -8
View File
@@ -14,6 +14,7 @@ import mongoose from 'mongoose';
import Q from 'q';
import domainMiddleware from './middlewares/api-v3/domain';
import attachMiddlewares from './middlewares/api-v3/index';
import staticMiddleware from './middlewares/api-v3/static';
// Setup translations
// let i18n = require('./libs/api-v2/i18n');
@@ -88,7 +89,7 @@ app.use(domainMiddleware(server, mongoose));
// Matches all request except the ones going to /api/v3/**
app.all(/^(?!\/api\/v3).+/i, oldApp);
// Matches all requests going to /api/v3
app.all('/api/v3', newApp);
app.all('/api/*', newApp);
// Mount middlewares for the new app
attachMiddlewares(newApp);
@@ -144,18 +145,22 @@ oldApp.use('/api/v1', require('./routes/api-v1'));
oldApp.use('/export', require('./routes/dataexport'));
require('./routes/api-v2/swagger')(swagger, v2);
var maxAge = IS_PROD ? 31536000000 : 0;
// Cache emojis without copying them to build, they are too many
oldApp.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
oldApp.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge }));
oldApp.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge }));
oldApp.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge }));
oldApp.use('/common/img', express['static'](publicDir + "/../../common/img", { maxAge: maxAge }));
oldApp.use(express['static'](publicDir));
oldApp.use(require('./middlewares/api-v2/errorHandler'));
*
let maxAge = IS_PROD ? 31536000000 : 0;
oldApp.use(express.static(path.join(__dirname, '/../build'), { maxAge }));
oldApp.use('/common/dist', express.static(`${publicDir}/../../common/dist`, { maxAge }));
oldApp.use('/common/audio', express.static(`${publicDir}/../../common/audio`, { maxAge }));
oldApp.use('/common/script/public', express.static(`${publicDir}/../../common/script/public`, { maxAge }));
oldApp.use('/common/img', express.static(`${publicDir}/../../common/img`, { maxAge }));
oldApp.use(express.static(publicDir));
*/
staticMiddleware(app);
server.on('request', app);
server.listen(app.get('port'), () => {
return logger.info(`Express server listening on port ${app.get('port')}`);