Skip to content

Commit

Permalink
Merge pull request #59 from MartinNuc/master
Browse files Browse the repository at this point in the history
Automatically add OPTIONS action
  • Loading branch information
yakovkhalinsky committed Jul 2, 2015
2 parents c2c85ad + a609094 commit 4da9479
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 14 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ By default a CORS header is sent, you can disable it with the --disableCORS swit

`drakov -f "../com/foo/contracts/*.md" --disableCORS`

## Automatic response to OPTIONS requests

When you run server for testing API on different port than your app is it's handy to allow cross origin resource sharing (CORS). For this working you need also to listen on every route for OPTIONS requests.

`drakov -f "../com/foo/contracts/*.md" --autoOptions`

## Run on Public Interface

By default Drakov only binds to localhost, to run on the public IP interface use the --public switch.
Expand Down
3 changes: 3 additions & 0 deletions lib/arguments.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ var optimistOptions = {
public: {
description: 'Allow external requests',
default: false
},
autoOptions: {
description: 'Automatically response to OPTIONS request for used routes'
}

};
Expand Down
41 changes: 41 additions & 0 deletions lib/json/auto-options-action.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "",
"description": "",
"method": "OPTIONS",
"parameters": [],
"attributes": {
"relation": "",
"uriTemplate": ""
},
"content": [],
"examples": [
{
"name": "",
"description": "",
"requests": [],
"responses": [
{
"name": "200",
"description": "",
"headers": [
{
"name": "Content-Type",
"value": "text/plain"
}
],
"body": "",
"schema": "",
"content": [
{
"element": "asset",
"attributes": {
"role": "bodyExample"
},
"content": ""
}
]
}
]
}
]
}
3 changes: 2 additions & 1 deletion lib/middleware/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ var bootstrapMiddleware = function(app, argv) {

exports.init = function(app, argv, cb) {
bootstrapMiddleware(app, argv);
routeHandlers(argv.sourceFiles, function(err, middleware) {
var options = {sourceFiles: argv.sourceFiles, autoOptions: argv.autoOptions};
routeHandlers(options, function(err, middleware) {
cb(err, middleware);
});
};
1 change: 1 addition & 0 deletions lib/middleware/response.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ exports.corsHeaders = function(disableCORS) {
return function(req, res, next) {
if (!disableCORS) {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
}
next();
};
Expand Down
46 changes: 45 additions & 1 deletion lib/middleware/route-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ var fs = require('fs');
var protagonist = require('protagonist');
var async = require('async');
var pathToRegexp = require('path-to-regexp');
var _ = require('lodash');

var urlParser = require('../url-parser');
var route = require('../route');
var queryComparator = require('../query-comparator');

var ROUTE_MAP = null;
var autoOptions;
var autoOptionsAction = require('../json/auto-options-action.json');

var parseAction = function(uriTemplate, action) {
var parsedUrl = urlParser.parse(uriTemplate);
Expand All @@ -32,14 +35,53 @@ var parseBlueprint = function(filePath) {
return;
}

var allRoutesList = [];
result.ast.resourceGroups.forEach(function(resourceGroup){
resourceGroup.resources.forEach(function(resource){
resource.actions.forEach(function(action){
parseAction(resource.uriTemplate, action);
saveRouteToTheList(resource, action);
});
});
});

// add OPTIONS route where its missing - this must be done after all routes are parsed
if (autoOptions) {
addOptionsRoutesWhereMissing(allRoutesList);
}

cb();

/**
* Adds route and its action to the allRoutesList. It appends the action when route already exists in the list.
* @param resource Route URI
* @param action HTTP action
*/
function saveRouteToTheList(resource, action) {
// used to add options routes later
if (typeof allRoutesList[resource.uriTemplate] === 'undefined') {
allRoutesList[resource.uriTemplate] = [];
}
allRoutesList[resource.uriTemplate].push(action);
}

function addOptionsRoutesWhereMissing(allRoutes) {
var routesWithoutOptions = [];
// extracts only routes without OPTIONS
_.forIn(allRoutes, function (actions, route) {
var containsOptions = _.reduce(actions, function (previousResult, iteratedAction) {
return previousResult || (iteratedAction.method === 'OPTIONS');
}, false);
if (containsOptions === false) {
routesWithoutOptions.push(route);
}
});

_.forEach(routesWithoutOptions, function (uriTemplate) {
// adds prepared OPTIONS action for route
parseAction(uriTemplate, autoOptionsAction);
});
}
});
};
};
Expand All @@ -65,7 +107,9 @@ var setup = function(sourceFiles, cb) {

};

module.exports = function(sourceFiles, cb) {
module.exports = function(options, cb) {
var sourceFiles = options.sourceFiles;
autoOptions = options.autoOptions;
var middleware = function(req, res, next) {
var handlers = null;
Object.keys(ROUTE_MAP).forEach(function(urlPattern) {
Expand Down
34 changes: 34 additions & 0 deletions test/api/options-auto-response-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
var helper = require('../lib');
var request = helper.getRequest();

describe('Auto OPTIONS', function () {

before(function (done) {
helper.drakov.run({sourceFiles: 'test/example/md/simple-api.md', autoOptions: true}, done);
});

after(function (done) {
helper.drakov.stop(done);
});

it('should respond for OPTIONS despite its not defined in api blueprint', function (done) {
request.options('/api/things/2')
.expect(200)
.expect('Access-Control-Allow-Origin', '*')
.end(helper.endCb(done));
});

it('should not override OPTIONS route specified in api blueprint', function (done) {
request.options('/api/things')
.expect(200)
.expect('Access-Control-Allow-Origin', 'custom-domain.com')
.end(helper.endCb(done));
});

it('should not respond for OPTIONS for paths missing in api blueprint', function (done) {
request.options('/fjselifjsleifjselij')
.expect(404)
.end(helper.endCb(done));
});

});
51 changes: 39 additions & 12 deletions test/example/md/simple-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,33 @@ Lists all the things from the API
"id": "5"
}
]
### Create a new thing [POST]

+ Request (application/json)
Create a new thing

+ Body

{
"text": "Hyperspeed jet",
}

+ Response 200 (application/json;charset=UTF-8)

+ Body

{
"text": "Hyperspeed jet",
"id": "1"
}
### Allow cross site origin [OPTIONS]

+ Response 200
+ Headers

Access-Control-Allow-Origin: custom-domain.com

## Things [/api/things/{thingId}]

Expand All @@ -59,19 +86,19 @@ Update the text of the thing

+ Body

{
"text": "Hyperspeed jet",
"id": "1"
}
{
"text": "Hyperspeed jet",
"id": "1"
}

+ Response 200 (application/json;charset=UTF-8)

+ Body

{
"text": "Hyperspeed jet",
"id": "1"
}
{
"text": "Hyperspeed jet",
"id": "1"
}

## Likes [/api/things/{thingId}/like]

Expand All @@ -95,7 +122,7 @@ Update the text of the thing

+ Body

{
"charset":"not present",
"id": "1"
}
{
"charset":"not present",
"id": "1"
}

0 comments on commit 4da9479

Please sign in to comment.