Skip to content

Commit

Permalink
Initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
alexmingoia committed Feb 2, 2014
0 parents commit f528368
Show file tree
Hide file tree
Showing 5 changed files with 494 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**DS_Store
node_modules/
117 changes: 117 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# koa-resource-router

[![Build Status](https://secure.travis-ci.org/alexmingoia/koa-resource-router.png)](http://travis-ci.org/alexmingoia/koa-resource-router)
[![Dependency Status](https://david-dm.org/alexmingoia/koa-resource-router.png)](http://david-dm.org/alexmingoia/koa-resource-router)
[![NPM version](https://badge.fury.io/js/koa-resource-router.png)](http://badge.fury.io/js/koa-resource-router)

RESTful resource routing for [koa](https://github.com/koajs/koa).

* Rails-like REST resource routing.
* Use multiple middleware for resource actions.
* Responds to `OPTIONS` requests with allowed methods.
* Returns `405 Method Not Allowed` when applicable.

## Installation

Install using [npm](https://npmjs.org):

```sh
npm install koa-resource-router
```

## API

### new Resource(path, actions, options)

```javascript
var app = require('koa')()

var users = new Resource('users', {
// GET /users
index: function *(next) {
},
// GET /users/new
new: function *(next) {
},
// POST /users
create: function *(next) {
},
// GET /users/:id
show: function *(next) {
},
// GET /users/:id/edit
edit: function *(next) {
},
// PUT /users/:id
update: function *(next) {
},
// DELETE /users/:id
destroy: function *(next) {
}
});

app.use(users.middleware());
```

### Action mapping

Actions are then mapped accordingly:

```javascript
GET /users -> index
GET /users/new -> new
POST /users -> create
GET /users/:user -> show
GET /users/:user/edit -> edit
PUT /users/:user -> update
DELETE /users/:user -> destroy
```

### Overriding action mapping

```javascript
var users = new Resource('users', actions, {
methods: {
update: 'PATCH'
}
});
```

### Top-level resource

Omit the resource name to specify a top-level resource:

```javascript
var root = new Resource(require('./frontpage'));
```

Top-level controller actions are mapped as follows:

```javascript
GET / -> index
GET /new -> new
POST / -> create
GET /:id -> show
GET /:id/edit -> edit
PUT /:id -> update
DELETE /:id -> destroy
```

### Nesting

Resources can be nested using `resource.add()`:

```javascript
var forums = new Resource('forums', require('./forum'));
var threads = new Resource('threads', require('./threads'));

forums.add(threads);
```

### Multiple middleware

```javascript
var users = new Resource('users', authorize, actions);
```

## MIT Licensed
183 changes: 183 additions & 0 deletions lib/resource.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Dependencies
*/

var compose = require('koa-compose')
, defaults = require('defaults')
, lingo = require('lingo')
, pathToRegExp = require('path-to-regexp');

/**
* Expose `Resource`
*/

module.exports = Resource;

/**
* Initialize a new Resource using given `name` and `actions`.
*
* `options`
* - methods override map of action names to http method
*
* @param {String} name
* @param {Function} actions
* @param {Object} options
* @return {Resource}
* @api private
*/

function Resource(name, actions, options) {
if (!(this instanceof Resource)) {
return new Resource(name, actions, options);
}

if (typeof name === 'object') {
actions = name;
name = null;
}

this.options = {
methods: defaults((options || {}).methods || {}, {
'options': 'OPTIONS',
'new': 'GET',
'create': 'POST',
'edit': 'GET',
'update': 'PUT',
'index': 'GET',
'list': 'GET',
'read': 'GET',
'show': 'GET',
'destroy': 'DELETE',
'remove': 'DELETE'
})
};

this.name = name;
this.id = name ? lingo.en.singularize(name) : 'id';
this.base = this.name ? '/' + this.name : '/';
this.actions = actions;
this.routes = [];
this.resources = [];

// create route definition (used for routing) for each resource action
Object.keys(actions).forEach(function(name) {
var url = this.base;
var urlTrailing = this.base;

if (url[url.length-1] != '/') {
urlTrailing = url + '/';
}

if (name == 'new') {
url = urlTrailing + ':' + this.id;
}
else if (name == 'edit') {
url = urlTrailing + ':' + this.id + '/edit';
}
else if (name.match(/(show|read|update|remove|destroy)/)) {
url = urlTrailing + ':' + this.id;
}

var action = actions[name];
if (action instanceof Array) {
action = compose(actions[name]);
}

var params = [];

this.routes.push({
method: this.options.methods[name].toUpperCase(),
url: url,
regexp: pathToRegExp(url, params),
params: params,
action: action
});
}, this);
};

Resource.prototype.middleware = function() {
var resource = this;

return function *(next) {
var matched;

this.params = [];

if (matched = resource.match(this.url, this.params)) {
var allowedMethods = [];

for (var len = matched.length, i=0; i<len; i++) {
var route = matched[i];

if (this.method == route.method) {
return yield route.action.call(this, next);
}
else {
if (!~allowedMethods.indexOf(route.method)) {
allowedMethods.push(route.method);
}

}
}

this.status = (this.method == 'OPTIONS' ? 204 : 405);
this.set('Allow', Object.keys(methodsAllowed).join(", "));
}

return yield next;
};
};

Resource.prototype.match = function(url, params) {
var matched = [];

for (var len = this.routes.length, i=0; i<len; i++) {
var route = this.routes[i];

if (route.regexp.test(url)) {
var captures = url.match(route.regexp);
if (captures && captures.length) {
captures = captures.slice(1);
}

if (params && route.params.length) {
for (var l = captures.length, n=0; n<l; n++) {
if (route.params[n]) {
params[route.params[n].name] = captures[n];
}
}
}

matched.push(route);
}
}

return matched.length ? matched : false;
};

/**
* Nest given `resource`.
*
* @param {Resource} resource
* @return {Resource}
* @api public
*/

Resource.prototype.add = function(resource) {
var base = this.base[this.base.length-1] == '/' ? this.base : this.base + '/';
this.resources.push(resource);

// Re-define base path for nested resource
resource.base = resource.name ? '/' + resource.name : '/';
resource.base = base + ':' + this.id + resource.base;

// Re-define route paths for nested resource
for (var len = resource.routes.length, i=0; i<len; i++) {
var route = resource.routes[i];
route.url = base + ':' + this.id + route.url;
route.params = [];
route.regexp = pathToRegExp(route.url, route.params);
}

return this;
};
38 changes: 38 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "koa-resource-router",
"description": "RESTful resource routing for koa and koa-router.",
"repository": {
"type": "git",
"url": "https://github.com/alexmingoia/koa-resource.git"
},
"author": "Alex Mingoia <[email protected]>",
"version": "0.1.0",
"keywords": [
"rest",
"resource",
"koa",
"router",
"middleware"
],
"main": "./lib/resource.js",
"dependencies": {
"debug": "~0.7.4",
"lingo": "0.0.5",
"path-to-regexp": "0.0.2",
"defaults": "~1.0.0",
"koa-compose": "~2.2.0"
},
"devDependencies": {
"koa": "0.3.0",
"mocha": "1.12.0",
"should": "1.2.2",
"supertest": "0.7.1"
},
"scripts": {
"test": "NODE_ENV=test node_modules/mocha/bin/mocha --harmony-generators --reporter spec"
},
"engines": {
"node": "> 0.11.4"
},
"license": "MIT"
}
Loading

0 comments on commit f528368

Please sign in to comment.