Commit b9ffe3e0 authored by Addy Osmani's avatar Addy Osmani Committed by Stephen Sawchuk

Moving Polymer out of labs

parent c488f564
......@@ -9,7 +9,7 @@ body {
body {
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
line-height: 1.4em;
background: #eaeaea url('../components/todomvc-common/bg.png');
background: #eaeaea url('../bower_components/todomvc-common/bg.png');
color: #4d4d4d;
width: 550px;
margin: 0 auto;
......
{
"name": "todomvc-polymer",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.4",
"director": "*",
"polymer": "*"
}
}
<element name="td-item" extends="li" attributes="item editing" on-blur="commitAction">
<link rel="stylesheet" href="td-item.css">
<template>
<div class="view {{completed: item.completed; editing: editing}}" hidden?="{{editing}}" on-dblclick="editAction">
<div class="view {{completed: item.completed; editing: editing}}" hidden?="{{editing}}">
<input type="checkbox" class="toggle" checked="{{item.completed}}" on-click="itemChangeAction">
<label>{{item.title}}</label>
<label on-dblclick="editAction">{{item.title}}</label>
<button class="destroy" on-click="destroyAction"></button>
</div>
<input id="edit" class="edit" value="{{title}}" hidden?="{{!editing}}" on-keyup="keyAction">
......@@ -13,7 +13,7 @@
var ESC_KEY = 27;
Polymer.register(this, {
editing: false,
keyAction: function(e) {
keyAction: function (e) {
switch (e.keyCode) {
case ESC_KEY:
this.cancelAction();
......@@ -23,16 +23,16 @@
break;
}
},
editAction: function() {
editAction: function () {
this.editing = true;
this.title = this.item.title;
// schedule focus for the end of microtask, when the input will be visible
Platform.flush();
this.asyncMethod(function() {
this.asyncMethod(function () {
this.$.edit.focus();
});
},
commitAction: function() {
commitAction: function () {
if (this.editing) {
this.editing = false;
this.item.title = this.title.trim();
......@@ -41,13 +41,13 @@
}
}
},
cancelAction: function() {
cancelAction: function () {
this.editing = false;
},
itemChangeAction: function() {
itemChangeAction: function () {
this.fire('td-item-changed');
},
destroyAction: function() {
destroyAction: function () {
this.fire('td-destroy-item', this.item);
}
});
......
......@@ -5,58 +5,56 @@
completedCount: 0,
activeCount: 0,
allCompleted: false,
ready: function() {
this.asyncMethod(function() {
ready: function () {
this.asyncMethod(function () {
this.items = this.items || [];
});
},
filterChanged: function() {
filterChanged: function () {
this.filterItems();
},
itemsChanged: function() {
this.completedCount =
this.items.filter(this.filters.completed).length;
itemsChanged: function () {
this.completedCount = this.items.filter(this.filters.completed).length;
this.activeCount = this.items.length - this.completedCount;
this.allCompleted = this.completedCount && !this.activeCount;
this.filterItems();
this.fire('td-model-changed');
},
filterItems: function() {
filterItems: function () {
var fn = this.filters[this.filter];
this.filtered = fn ? this.items.filter(fn) : this.items;
},
newItem: function(title) {
newItem: function (title) {
title = String(title).trim();
if (title) {
var item = {
this.items.push({
title: title,
completed: false
};
this.items.push(item);
});
this.itemsChanged();
}
},
destroyItem: function(item) {
destroyItem: function (item) {
var i = this.items.indexOf(item);
if (i >= 0) {
this.items.splice(i, 1);
}
this.itemsChanged();
},
clearItems: function(){
clearItems: function () {
this.items = this.items.filter(this.filters.active);
},
setItemsCompleted: function(completed) {
this.items.forEach(function(item) {
setItemsCompleted: function (completed) {
this.items.forEach(function (item) {
item.completed = completed;
});
this.itemsChanged();
},
filters: {
active: function(item) {
active: function (item) {
return !item.completed;
},
completed: function(item) {
completed: function (item) {
return item.completed;
}
}
......
......@@ -45,30 +45,30 @@
var ENTER_KEY = 13;
var ESC_KEY = 27;
Polymer.register(this, {
keyAction: function(e, detail, sender) {
keyAction: function (e, detail, sender) {
switch (e.keyCode) {
case ENTER_KEY:
this.$.model.newItem(sender.value);
// when polyfilling Object.observe, make sure we update immediately
Platform.flush();
case ESC_KEY:
case ESC_KEY:
sender.value = '';
break;
}
},
modelChangedAction: function() {
modelChangedAction: function () {
this.$.storage.save();
},
itemChangedAction: function() {
itemChangedAction: function () {
this.$.model.itemsChanged();
},
destroyItemAction: function(e, detail) {
destroyItemAction: function (e, detail) {
this.$.model.destroyItem(detail);
},
toggleAllCompletedAction: function(e, detail, sender) {
toggleAllCompletedAction: function (e, detail, sender) {
this.$.model.setItemsCompleted(sender.checked);
},
clearCompletedAction: function() {
clearCompletedAction: function () {
this.$.model.clearItems();
}
});
......
<!doctype html>
<html lang="en" data-framework="polymer">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Polymer • TodoMVC</title>
<link rel="stylesheet" href="app/app.css">
</head>
<body>
<section>
<header>
<h1>todos</h1>
</header>
<td-todos></td-todos>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://www.polymer-project.org">The Polymer Authors</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</section>
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/polymer/polymer.js"></script>
<link rel="import" href="elements/td-todos.html">
</body>
</html>
<script src="../components/director/build/director.min.js"></script>
<script src="../bower_components/director/build/director.min.js"></script>
<element name="flatiron-director" attributes="route">
<script>
var private_router;
Polymer.register(this, {
ready: function() {
this.router.on(/(\w*)/, function(route) {
ready: function () {
this.router.on(/(\w*)/, function (route) {
this.route = route;
}.bind(this));
this.asyncMethod(function() {
this.asyncMethod(function () {
var initialRoute = this.router.getRoute(0);
this.route = initialRoute || '';
});
......@@ -19,7 +19,7 @@
}
return private_router;
},
routeChanged: function() {
routeChanged: function () {
this.fire('route', this.route);
}
});
......
......@@ -21,13 +21,13 @@ license that can be found in the LICENSE file.
<script>
Polymer.register(this, {
isJson: true,
ready: function() {
ready: function () {
this.load();
},
valueChanged: function() {
valueChanged: function () {
this.save();
},
load: function() {
load: function () {
var s = window.localStorage.getItem(this.name);
if (s && this.isJson) {
this.value = JSON.parse(s);
......@@ -35,9 +35,9 @@ license that can be found in the LICENSE file.
this.value = s;
}
},
save: function() {
window.localStorage.setItem(this.name,
this.isJson ? JSON.stringify(this.value) : this.value);
save: function () {
var item = this.isJson ? JSON.stringify(this.value) : this.value;
window.localStorage.setItem(this.name, item);
}
});
</script>
......
......@@ -21,34 +21,35 @@ license that can be found in the LICENSE file.
<script>
Polymer.register(this, {
multi: false,
ready: function() {
ready: function () {
this.clear();
},
clear: function() {
clear: function () {
this.selection = [];
},
getSelection: function() {
getSelection: function () {
return this.multi ? this.selection : this.selection[0];
},
isSelected: function(inItem) {
isSelected: function (inItem) {
return this.selection.indexOf(inItem) >= 0;
},
setItemSelected: function(inItem, inIsSelected) {
setItemSelected: function (inItem, inIsSelected) {
var i;
if (inItem) {
if (inIsSelected) {
this.selection.push(inItem);
} else {
var i = this.selection.indexOf(inItem);
i = this.selection.indexOf(inItem);
if (i >= 0) {
this.selection.splice(i, 1);
}
}
// TODO(sjmiles): consider replacing with summary
// notifications (asynchronous job)
this.asend("select", {isSelected: inIsSelected, item: inItem});
this.asend("select", { isSelected: inIsSelected, item: inItem });
}
},
select: function(inItem) {
select: function (inItem) {
if (this.multi) {
this.toggle(inItem);
} else if (this.getSelection() !== inItem) {
......@@ -56,7 +57,7 @@ license that can be found in the LICENSE file.
this.setItemSelected(inItem, true);
}
},
toggle: function(inItem) {
toggle: function (inItem) {
this.setItemSelected(inItem, !this.isSelected(inItem));
}
});
......
......@@ -23,7 +23,7 @@ license that can be found in the LICENSE file.
*
* polymer-selector is not styled. So one needs to use "selected" CSS class
* to style the selected element.
*
*
* <style>
* .item.selected {
* background: #eee;
......@@ -40,7 +40,7 @@ license that can be found in the LICENSE file.
*/
/**
* Fired when an element is selected via tap.
*
*
* @event activate
* @param {Object} inDetail
* @param {Object} inDetail.item the selected element
......@@ -92,7 +92,7 @@ license that can be found in the LICENSE file.
valueattr: 'name',
/**
* Specifies the CSS class to be used to add to the selected element.
*
*
* @attribute selectedClass
* @type string
* @default 'selected'
......@@ -100,13 +100,13 @@ license that can be found in the LICENSE file.
selectedClass: 'selected',
/**
* Returns the model associated with the selected element.
*
*
* @attribute selectedModel
* @type Object
* @default null
*/
selectedModel: null,
ready: function() {
ready: function () {
this.setAttribute('touch-action', 'none');
},
get items() {
......@@ -115,22 +115,24 @@ license that can be found in the LICENSE file.
get selection() {
return this.$.selection.getSelection();
},
selectedChanged: function() {
selectedChanged: function () {
this.valueToSelection(this.selected);
},
valueToSelection: function(inValue) {
valueToSelection: function (inValue) {
var item = this.items[this.valueToIndex(inValue)];
var template;
if (item) {
this.$.selection.select(item);
var t = item.templateInstance;
this.selectedModel = t ? t.model : undefined;
template = item.templateInstance;
this.selectedModel = template ? template.model : undefined;
} else {
this.selectedModel = null;
}
},
valueToIndex: function(inValue) {
valueToIndex: function (inValue) {
// find an item with value == inValue and return it's index
for (var i=0, items=this.items, c; (c=items[i]); i++) {
var i, items, c;
for (i = 0, items = this.items, c; (c = items[i]); i++) {
if (this.valueForNode(c) == inValue) {
return i;
}
......@@ -138,30 +140,31 @@ license that can be found in the LICENSE file.
// if no item found, the value itself is probably the index
return inValue;
},
valueForNode: function(inNode) {
valueForNode: function (inNode) {
return inNode[this.valueattr] || inNode.getAttribute(this.valueattr);
},
// events fired from <polymer-selection> object
selectionSelect: function(inEvent, inInfo) {
selectionSelect: function (inEvent, inInfo) {
if (inInfo.item && this.selectedClass) {
inInfo.item.classList.toggle(this.selectedClass, inInfo.isSelected);
}
},
// event fired from host
activateHandler: function(inEvent) {
activateHandler: function (inEvent) {
if (this.notap) {
return;
}
var items = this.items;
var i = Polymer.findDistributedTarget(inEvent.target, items);
var selected;
if (i >= 0) {
var selected = this.valueForNode(items[i]) || i;
selected = this.valueForNode(items[i]) || i;
if (this.multi) {
this.valueToSelection(selected);
} else {
this.selected = selected;
}
this.asend('activate', {item: items[i]});
this.asend('activate', { item: items[i] });
}
}
});
......
......@@ -80,8 +80,8 @@
<li class="routing">
<a href="architecture-examples/maria/" data-source="https://github.com/petermichaux/maria" data-content="An MVC framework for JavaScript applications. The real MVC. The Smalltalk MVC. The Gang of Four MVC. The three core design patterns of MVC (observer, composite, and strategy) are embedded in Maria's Model, View, and Controller objects. Other patterns traditionally included in MVC implementations (e.g. factory method and template) make appearances too.">Maria</a>
</li>
<li class="labs routing">
<a href="labs/architecture-examples/polymer/index.html" data-source="http://polymer-project.org" data-content="Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers. It is comprised of core platform features (e.g Shadow DOM, Custom Elements, MDV) enabled with polyfills and a next generation web application framework built on these technologies.">Polymer</a>
<li class="routing">
<a href="architecture-examples/polymer/index.html" data-source="http://polymer-project.org" data-content="Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers. It is comprised of core platform features (e.g Shadow DOM, Custom Elements, MDV) enabled with polyfills and a next generation web application framework built on these technologies.">Polymer</a>
</li>
<li class="labs">
<a href="labs/architecture-examples/cujo/index.html" data-source="http://cujojs.com" data-content="cujoJS is an architectural framework for building highly modular, scalable, maintainable applications in Javascript. It provides architectural plumbing, such as modules (AMD and CommonJS), declarative application composition, declarative connections, and aspect oriented programming.">cujoJS</a>
......
{
"name": "todomvc-template",
"version": "0.0.0",
"dependencies": {
"todomvc-common": "~0.1.4",
"director": "*",
"polymer": "*"
}
}
/.idea/
node_modules
npm-debug.log
.DS_Store
/test/browser/browserified-bundle.js
language: node_js
node_js:
- 0.6
- 0.8
- "0.10"
notifications:
email:
- travis@nodejitsu.com
irc: "irc.freenode.org#nodejitsu"
Copyright (c) 2011 Nodejitsu Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
\ No newline at end of file
<img src="https://github.com/flatiron/director/raw/master/img/director.png" />
# Synopsis
Director is a router. Routing is the process of determining what code to run when a URL is requested.
# Motivation
A routing library that works in both the browser and node.js environments with as few differences as possible. Simplifies the development of Single Page Apps and Node.js applications. Dependency free (doesn't require jQuery or Express, etc).
# Status
[![Build Status](https://secure.travis-ci.org/flatiron/director.png?branch=master)](http://travis-ci.org/flatiron/director)
# Features
* [Client-Side Routing](#client-side)
* [Server-Side HTTP Routing](#http-routing)
* [Server-Side CLI Routing](#cli-routing)
# Usage
* [API Documentation](#api-documentation)
* [Frequently Asked Questions](#faq)
<a name="client-side"></a>
## Client-side Routing
It simply watches the hash of the URL to determine what to do, for example:
```
http://foo.com/#/bar
```
Client-side routing (aka hash-routing) allows you to specify some information about the state of the application using the URL. So that when the user visits a specific URL, the application can be transformed accordingly.
<img src="https://github.com/flatiron/director/raw/master/img/hashRoute.png" />
Here is a simple example:
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A Gentle Introduction</title>
<script src="https://raw.github.com/flatiron/director/master/build/director.min.js"></script>
<script>
var author = function () { console.log("author"); },
books = function () { console.log("books"); },
viewBook = function(bookId) { console.log("viewBook: bookId is populated: " + bookId); };
var routes = {
'/author': author,
'/books': [books, function() { console.log("An inline route handler."); }],
'/books/view/:bookId': viewBook
};
var router = Router(routes);
router.init();
</script>
</head>
<body>
<ul>
<li><a href="#/author">#/author</a></li>
<li><a href="#/books">#/books</a></li>
<li><a href="#/books/view/1">#/books/view/1</a></li>
</ul>
</body>
</html>
```
Director works great with your favorite DOM library, such as jQuery.
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>A Gentle Introduction 2</title>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
<script src="https://raw.github.com/flatiron/director/master/build/director.min.js"></script>
<script>
$('document').ready(function(){
//
// create some functions to be executed when
// the correct route is issued by the user.
//
var showAuthorInfo = function () { console.log("showAuthorInfo"); },
listBooks = function () { console.log("listBooks"); },
allroutes = function() {
var route = window.location.hash.slice(2),
sections = $('section'),
section;
if ((section = sections.filter('[data-route=' + route + ']')).length) {
sections.hide(250);
section.show(250);
}
};
//
// define the routing table.
//
var routes = {
'/author': showAuthorInfo,
'/books': listBooks
};
//
// instantiate the router.
//
var router = Router(routes);
//
// a global configuration setting.
//
router.configure({
on: allroutes
});
router.init();
});
</script>
</head>
<body>
<section data-route="author">Author Name</section>
<section data-route="books">Book1, Book2, Book3</section>
<ul>
<li><a href="#/author">#/author</a></li>
<li><a href="#/books">#/books</a></li>
</ul>
</body>
</html>
```
You can find a browser-specific build of `director` [here][1] which has all of the server code stripped away.
<a name="http-routing"></a>
## Server-Side HTTP Routing
Director handles routing for HTTP requests similar to `journey` or `express`:
```js
//
// require the native http module, as well as director.
//
var http = require('http'),
director = require('director');
//
// create some logic to be routed to.
//
function helloWorld() {
this.res.writeHead(200, { 'Content-Type': 'text/plain' })
this.res.end('hello world');
}
//
// define a routing table.
//
var router = new director.http.Router({
'/hello': {
get: helloWorld
}
});
//
// setup a server and when there is a request, dispatch the
// route that was requested in the request object.
//
var server = http.createServer(function (req, res) {
router.dispatch(req, res, function (err) {
if (err) {
res.writeHead(404);
res.end();
}
});
});
//
// You can also do ad-hoc routing, similar to `journey` or `express`.
// This can be done with a string or a regexp.
//
router.get('/bonjour', helloWorld);
router.get(/hola/, helloWorld);
//
// set the server to listen on port `8080`.
//
server.listen(8080);
```
### See Also:
- Auto-generated Node.js API Clients for routers using [Director-Reflector](http://github.com/flatiron/director-reflector)
- RESTful Resource routing using [restful](http://github.com/flatiron/restful)
- HTML / Plain Text views of routers using [Director-Explorer](http://github.com/flatiron/director-explorer)
<a name="cli-routing"></a>
## CLI Routing
Director supports Command Line Interface routing. Routes for cli options are based on command line input (i.e. `process.argv`) instead of a URL.
``` js
var director = require('director');
var router = new director.cli.Router();
router.on('create', function () {
console.log('create something');
});
router.on(/destroy/, function () {
console.log('destroy something');
});
// You will need to dispatch the cli arguments yourself
router.dispatch('on', process.argv.slice(2).join(' '));
```
Using the cli router, you can dispatch commands by passing them as a string. For example, if this example is in a file called `foo.js`:
``` bash
$ node foo.js create
create something
$ node foo.js destroy
destroy something
```
<a name="api-documentation"></a>
# API Documentation
* [Constructor](#constructor)
* [Routing Table](#routing-table)
* [Adhoc Routing](#adhoc-routing)
* [Scoped Routing](#scoped-routing)
* [Routing Events](#routing-events)
* [Configuration](#configuration)
* [URL Matching](#url-matching)
* [URL Params](#url-params)
* [Route Recursion](#route-recursion)
* [Async Routing](#async-routing)
* [Resources](#resources)
* [History API](#history-api)
* [Instance Methods](#instance-methods)
* [Attach Properties to `this`](#attach-to-this)
* [HTTP Streaming and Body Parsing](#http-streaming-body-parsing)
<a name="constructor"></a>
## Constructor
``` js
var router = Router(routes);
```
<a name="routing-table"></a>
## Routing Table
An object literal that contains nested route definitions. A potentially nested set of key/value pairs. The keys in the object literal represent each potential part of the URL. The values in the object literal contain references to the functions that should be associated with them. *bark* and *meow* are two functions that you have defined in your code.
``` js
//
// Assign routes to an object literal.
//
var routes = {
//
// a route which assigns the function `bark`.
//
'/dog': bark,
//
// a route which assigns the functions `meow` and `scratch`.
//
'/cat': [meow, scratch]
};
//
// Instantiate the router.
//
var router = Router(routes);
```
<a name="adhoc-routing"></a>
## Adhoc Routing
When developing large client-side or server-side applications it is not always possible to define routes in one location. Usually individual decoupled components register their own routes with the application router. We refer to this as _Adhoc Routing._ Lets take a look at the API `director` exposes for adhoc routing:
**Client-side Routing**
``` js
var router = new Router().init();
router.on('/some/resource', function () {
//
// Do something on `/#/some/resource`
//
});
```
**HTTP Routing**
``` js
var router = new director.http.Router();
router.get(/\/some\/resource/, function () {
//
// Do something on an GET to `/some/resource`
//
});
```
<a name="scoped-routing"></a>
## Scoped Routing
In large web appliations, both [Client-side](#client-side) and [Server-side](#http-routing), routes are often scoped within a few individual resources. Director exposes a simple way to do this for [Adhoc Routing](#adhoc-routing) scenarios:
``` js
var router = new director.http.Router();
//
// Create routes inside the `/users` scope.
//
router.path(/\/users\/(\w+)/, function () {
//
// The `this` context of the function passed to `.path()`
// is the Router itself.
//
this.post(function (id) {
//
// Create the user with the specified `id`.
//
});
this.get(function (id) {
//
// Retreive the user with the specified `id`.
//
});
this.get(/\/friends/, function (id) {
//
// Get the friends for the user with the specified `id`.
//
});
});
```
<a name="routing-events"></a>
## Routing Events
In `director`, a "routing event" is a named property in the [Routing Table](#routing-table) which can be assigned to a function or an Array of functions to be called when a route is matched in a call to `router.dispatch()`.
* **on:** A function or Array of functions to execute when the route is matched.
* **before:** A function or Array of functions to execute before calling the `on` method(s).
**Client-side only**
* **after:** A function or Array of functions to execute when leaving a particular route.
* **once:** A function or Array of functions to execute only once for a particular route.
<a name="configuration"></a>
## Configuration
Given the flexible nature of `director` there are several options available for both the [Client-side](#client-side) and [Server-side](#http-routing). These options can be set using the `.configure()` method:
``` js
var router = new director.Router(routes).configure(options);
```
The `options` are:
* **recurse:** Controls [route recursion](#route-recursion). Use `forward`, `backward`, or `false`. Default is `false` Client-side, and `backward` Server-side.
* **strict:** If set to `false`, then trailing slashes (or other delimiters) are allowed in routes. Default is `true`.
* **async:** Controls [async routing](#async-routing). Use `true` or `false`. Default is `false`.
* **delimiter:** Character separator between route fragments. Default is `/`.
* **notfound:** A function to call if no route is found on a call to `router.dispatch()`.
* **on:** A function (or list of functions) to call on every call to `router.dispatch()` when a route is found.
* **before:** A function (or list of functions) to call before every call to `router.dispatch()` when a route is found.
**Client-side only**
* **resource:** An object to which string-based routes will be bound. This can be especially useful for late-binding to route functions (such as async client-side requires).
* **after:** A function (or list of functions) to call when a given route is no longer the active route.
* **html5history:** If set to `true` and client supports `pushState()`, then uses HTML5 History API instead of hash fragments. See [History API](#history-api) for more information.
* **run_handler_in_init:** If `html5history` is enabled, the route handler by default is executed upon `Router.init()` since with real URIs the router can not know if it should call a route handler or not. Setting this to `false` disables the route handler initial execution.
<a name="url-matching"></a>
## URL Matching
``` js
var router = Router({
//
// given the route '/dog/yella'.
//
'/dog': {
'/:color': {
//
// this function will return the value 'yella'.
//
on: function (color) { console.log(color) }
}
}
});
```
Routes can sometimes become very complex, `simple/:tokens` don't always suffice. Director supports regular expressions inside the route names. The values captured from the regular expressions are passed to your listener function.
``` js
var router = Router({
//
// given the route '/hello/world'.
//
'/hello': {
'/(\\w+)': {
//
// this function will return the value 'world'.
//
on: function (who) { console.log(who) }
}
}
});
```
``` js
var router = Router({
//
// given the route '/hello/world/johny/appleseed'.
//
'/hello': {
'/world/?([^\/]*)\/([^\/]*)/?': function (a, b) {
console.log(a, b);
}
}
});
```
<a name="url-params"></a>
## URL Parameters
When you are using the same route fragments it is more descriptive to define these fragments by name and then use them in your [Routing Table](#routing-table) or [Adhoc Routes](#adhoc-routing). Consider a simple example where a `userId` is used repeatedly.
``` js
//
// Create a router. This could also be director.cli.Router() or
// director.http.Router().
//
var router = new director.Router();
//
// A route could be defined using the `userId` explicitly.
//
router.on(/([\w-_]+)/, function (userId) { });
//
// Define a shorthand for this fragment called `userId`.
//
router.param('userId', /([\\w\\-]+)/);
//
// Now multiple routes can be defined with the same
// regular expression.
//
router.on('/anything/:userId', function (userId) { });
router.on('/something-else/:userId', function (userId) { });
```
<a name="route-recursion"></a>
## Route Recursion
Can be assigned the value of `forward` or `backward`. The recurse option will determine the order in which to fire the listeners that are associated with your routes. If this option is NOT specified or set to null, then only the listeners associated with an exact match will be fired.
### No recursion, with the URL /dog/angry
``` js
var routes = {
'/dog': {
'/angry': {
//
// Only this method will be fired.
//
on: growl
},
on: bark
}
};
var router = Router(routes);
```
### Recursion set to `backward`, with the URL /dog/angry
``` js
var routes = {
'/dog': {
'/angry': {
//
// This method will be fired first.
//
on: growl
},
//
// This method will be fired second.
//
on: bark
}
};
var router = Router(routes).configure({ recurse: 'backward' });
```
### Recursion set to `forward`, with the URL /dog/angry
``` js
var routes = {
'/dog': {
'/angry': {
//
// This method will be fired second.
//
on: growl
},
//
// This method will be fired first.
//
on: bark
}
};
var router = Router(routes).configure({ recurse: 'forward' });
```
### Breaking out of recursion, with the URL /dog/angry
``` js
var routes = {
'/dog': {
'/angry': {
//
// This method will be fired first.
//
on: function() { return false; }
},
//
// This method will not be fired.
//
on: bark
}
};
//
// This feature works in reverse with recursion set to true.
//
var router = Router(routes).configure({ recurse: 'backward' });
```
<a name="async-routing"></a>
## Async Routing
Before diving into how Director exposes async routing, you should understand [Route Recursion](#route-recursion). At it's core route recursion is about evaluating a series of functions gathered when traversing the [Routing Table](#routing-table).
Normally this series of functions is evaluated synchronously. In async routing, these functions are evaluated asynchronously. Async routing can be extremely useful both on the client-side and the server-side:
* **Client-side:** To ensure an animation or other async operations (such as HTTP requests for authentication) have completed before continuing evaluation of a route.
* **Server-side:** To ensure arbitrary async operations (such as performing authentication) have completed before continuing the evaluation of a route.
The method signatures for route functions in synchronous and asynchronous evaluation are different: async route functions take an additional `next()` callback.
### Synchronous route functions
``` js
var router = new director.Router();
router.on('/:foo/:bar/:bazz', function (foo, bar, bazz) {
//
// Do something asynchronous with `foo`, `bar`, and `bazz`.
//
});
```
### Asynchronous route functions
``` js
var router = new director.http.Router().configure({ async: true });
router.on('/:foo/:bar/:bazz', function (foo, bar, bazz, next) {
//
// Go do something async, and determine that routing should stop
//
next(false);
});
```
<a name="resources"></a>
## Resources
**Available on the Client-side only.** An object literal containing functions. If a host object is specified, your route definitions can provide string literals that represent the function names inside the host object. A host object can provide the means for better encapsulation and design.
``` js
var router = Router({
'/hello': {
'/usa': 'americas',
'/china': 'asia'
}
}).configure({ resource: container }).init();
var container = {
americas: function() { return true; },
china: function() { return true; }
};
```
<a name="history-api"></a>
## History API
**Available on the Client-side only.** Director supports using HTML5 History API instead of hash fragments for navigation. To use the API, pass `{html5history: true}` to `configure()`. Use of the API is enabled only if the client supports `pushState()`.
Using the API gives you cleaner URIs but they come with a cost. Unlike with hash fragments your route URIs must exist. When the client enters a page, say http://foo.com/bar/baz, the web server must respond with something meaningful. Usually this means that your web server checks the URI points to something that, in a sense, exists, and then serves the client the JavaScript application.
If you're after a single-page application you can not use plain old `<a href="/bar/baz">` tags for navigation anymore. When such link is clicked, web browsers try to ask for the resource from server which is not of course desired for a single-page application. Instead you need to use e.g. click handlers and call the `setRoute()` method yourself.
<a name="attach-to-this"></a>
## Attach Properties To `this`
Generally, the `this` object bound to route handlers, will contain the request in `this.req` and the response in `this.res`. One may attach additional properties to `this` with the `router.attach` method:
```js
var director = require('director');
var router = new director.http.Router().configure(options);
//
// Attach properties to `this`
//
router.attach(function () {
this.data = [1,2,3];
});
//
// Access properties attached to `this` in your routes!
//
router.get('/hello', function () {
this.res.writeHead(200, { 'content-type': 'text/plain' });
//
// Response will be `[1,2,3]`!
//
this.res.end(this.data);
});
```
This API may be used to attach convenience methods to the `this` context of route handlers.
<a name="http-streaming-body-parsing">
## HTTP Streaming and Body Parsing
When you are performing HTTP routing there are two common scenarios:
* Buffer the request body and parse it according to the `Content-Type` header (usually `application/json` or `application/x-www-form-urlencoded`).
* Stream the request body by manually calling `.pipe` or listening to the `data` and `end` events.
By default `director.http.Router()` will attempt to parse either the `.chunks` or `.body` properties set on the request parameter passed to `router.dispatch(request, response, callback)`. The router instance will also wait for the `end` event before firing any routes.
**Default Behavior**
``` js
var director = require('director');
var router = new director.http.Router();
router.get('/', function () {
//
// This will not work, because all of the data
// events and the end event have already fired.
//
this.req.on('data', function (chunk) {
console.log(chunk)
});
});
```
In [flatiron][2], `director` is used in conjunction with [union][3] which uses a `BufferedStream` proxy to the raw `http.Request` instance. [union][3] will set the `req.chunks` property for you and director will automatically parse the body. If you wish to perform this buffering yourself directly with `director` you can use a simple request handler in your http server:
``` js
var http = require('http'),
director = require('director');
var router = new director.http.Router();
var server = http.createServer(function (req, res) {
req.chunks = [];
req.on('data', function (chunk) {
req.chunks.push(chunk.toString());
});
router.dispatch(req, res, function (err) {
if (err) {
res.writeHead(404);
res.end();
}
console.log('Served ' + req.url);
});
});
router.post('/', function () {
this.res.writeHead(200, { 'Content-Type': 'application/json' })
this.res.end(JSON.stringify(this.req.body));
});
```
**Streaming Support**
If you wish to get access to the request stream before the `end` event is fired, you can pass the `{ stream: true }` options to the route.
``` js
var director = require('director');
var router = new director.http.Router();
router.get('/', { stream: true }, function () {
//
// This will work because the route handler is invoked
// immediately without waiting for the `end` event.
//
this.req.on('data', function (chunk) {
console.log(chunk);
});
});
```
<a name="instance-methods"></a>
## Instance methods
### configure(options)
* `options` {Object}: Options to configure this instance with.
Configures the Router instance with the specified `options`. See [Configuration](#configuration) for more documentation.
### param(token, matcher)
* token {string}: Named parameter token to set to the specified `matcher`
* matcher {string|Regexp}: Matcher for the specified `token`.
Adds a route fragment for the given string `token` to the specified regex `matcher` to this Router instance. See [URL Parameters](#url-params) for more documentation.
### on(method, path, route)
* `method` {string}: Method to insert within the Routing Table (e.g. `on`, `get`, etc.).
* `path` {string}: Path within the Routing Table to set the `route` to.
* `route` {function|Array}: Route handler to invoke for the `method` and `path`.
Adds the `route` handler for the specified `method` and `path` within the [Routing Table](#routing-table).
### path(path, routesFn)
* `path` {string|Regexp}: Scope within the Routing Table to invoke the `routesFn` within.
* `routesFn` {function}: Adhoc Routing function with calls to `this.on()`, `this.get()` etc.
Invokes the `routesFn` within the scope of the specified `path` for this Router instance.
### dispatch(method, path[, callback])
* method {string}: Method to invoke handlers for within the Routing Table
* path {string}: Path within the Routing Table to match
* callback {function}: Invoked once all route handlers have been called.
Dispatches the route handlers matched within the [Routing Table](#routing-table) for this instance for the specified `method` and `path`.
### mount(routes, path)
* routes {object}: Partial routing table to insert into this instance.
* path {string|Regexp}: Path within the Routing Table to insert the `routes` into.
Inserts the partial [Routing Table](#routing-table), `routes`, into the Routing Table for this Router instance at the specified `path`.
## Instance methods (Client-side only)
### init([redirect])
* `redirect` {String}: This value will be used if '/#/' is not found in the URL. (e.g., init('/') will resolve to '/#/', init('foo') will resolve to '/#foo').
Initialize the router, start listening for changes to the URL.
### getRoute([index])
* `index` {Number}: The hash value is divided by forward slashes, each section then has an index, if this is provided, only that section of the route will be returned.
Returns the entire route or just a section of it.
### setRoute(route)
* `route` {String}: Supply a route value, such as `home/stats`.
Set the current route.
### setRoute(start, length)
* `start` {Number} - The position at which to start removing items.
* `length` {Number} - The number of items to remove from the route.
Remove a segment from the current route.
### setRoute(index, value)
* `index` {Number} - The hash value is divided by forward slashes, each section then has an index.
* `value` {String} - The new value to assign the the position indicated by the first parameter.
Set a segment of the current route.
<a name="faq"></a>
# Frequently Asked Questions
## What About SEO?
Is using a Client-side router a problem for SEO? Yes. If advertising is a requirement, you are probably building a "Web Page" and not a "Web Application". Director on the client is meant for script-heavy Web Applications.
# Licence
(The MIT License)
Copyright (c) 2010 Nodejitsu Inc. <http://www.twitter.com/nodejitsu>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
[0]: http://github.com/flatiron/director
[1]: https://github.com/flatiron/director/blob/master/build/director.min.js
[2]: http://github.com/flatiron/flatiron
[3]: http://github.com/flatiron/union
#!/usr/bin/env node
var Codesurgeon = require('codesurgeon').Codesurgeon;
var surgeon = new Codesurgeon;
var path = require('path');
var root = path.join(__dirname, '..');
var lib = path.join(root, 'lib', 'director');
//
// Distill and package the browser version.
//
surgeon
//
.configure({
package: root + '/package.json',
owner: 'Nodejitsu, Inc (Using Codesurgeon).',
noVersion: true
})
.read(
path.join(lib, 'browser.js')
)
//
// we want everything so far. specify extract with no
// parameters to get everything into the output buffer.
//
.extract()
//
// clear the input so far, but don't clear the output.
//
.clear('inputs')
//
// read the `router.js` file
//
.read(
path.join(lib, 'router.js')
)
//
// the current input buffer contains stuff that we dont
// want in the browser build, so let's cherry pick from
// the buffer.
//
.extract(
'_every',
'_flatten',
'_asyncEverySeries',
'paramifyString',
'regifyString',
'terminator',
'Router.prototype.configure',
'Router.prototype.param',
'Router.prototype.on',
'Router.prototype.dispatch',
'Router.prototype.invoke',
'Router.prototype.traverse',
'Router.prototype.insert',
'Router.prototype.insertEx',
'Router.prototype.extend',
'Router.prototype.runlist',
'Router.prototype.mount'
)
//
// wrap everything that is in the current buffer with a
// closure so that we dont get any collisions with other
// libraries
//
.wrap()
//
// write the debuggable version of the file. This file will
// get renamed to include the version from the package.json
//
.write(root + '/build/director.js')
//
// now lets make a minified version for production use.
//
.uglify()
.write(root + '/build/director.min.js')
;
{
"name": "director",
"version": "1.2.0",
"repository": {
"type": "git",
"url": "git://github.com/flatiron/director.git"
}
}
\ No newline at end of file
var http = require('http'),
director = require('../lib/director');
var router = new director.http.Router();
var server = http.createServer(function (req, res) {
req.chunks = [];
req.on('data', function (chunk) {
req.chunks.push(chunk.toString());
});
router.dispatch(req, res, function (err) {
if (err) {
res.writeHead(404);
res.end();
}
console.log('Served ' + req.url);
});
});
router.get(/foo/, function () {
this.res.writeHead(200, { 'Content-Type': 'text/plain' });
this.res.end('hello world\n');
});
router.post(/foo/, function () {
this.res.writeHead(200, { 'Content-Type': 'application/json' });
this.res.end(JSON.stringify(this.req.body));
});
server.listen(8080);
console.log('vanilla http server with director running on 8080');
exports.Router = require('./director/router').Router;
exports.http = require('./director/http');
exports.cli = require('./director/cli');
/*
* browser.js: Browser specific functionality for director.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
if (!Array.prototype.filter) {
Array.prototype.filter = function(filter, that) {
var other = [], v;
for (var i = 0, n = this.length; i < n; i++) {
if (i in this && filter.call(that, v = this[i], i, this)) {
other.push(v);
}
}
return other;
};
}
if (!Array.isArray){
Array.isArray = function(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
};
}
var dloc = document.location;
function dlocHashEmpty() {
// Non-IE browsers return '' when the address bar shows '#'; Director's logic
// assumes both mean empty.
return dloc.hash === '' || dloc.hash === '#';
}
var listener = {
mode: 'modern',
hash: dloc.hash,
history: false,
check: function () {
var h = dloc.hash;
if (h != this.hash) {
this.hash = h;
this.onHashChanged();
}
},
fire: function () {
if (this.mode === 'modern') {
this.history === true ? window.onpopstate() : window.onhashchange();
}
else {
this.onHashChanged();
}
},
init: function (fn, history) {
var self = this;
this.history = history;
if (!Router.listeners) {
Router.listeners = [];
}
function onchange(onChangeEvent) {
for (var i = 0, l = Router.listeners.length; i < l; i++) {
Router.listeners[i](onChangeEvent);
}
}
//note IE8 is being counted as 'modern' because it has the hashchange event
if ('onhashchange' in window && (document.documentMode === undefined
|| document.documentMode > 7)) {
// At least for now HTML5 history is available for 'modern' browsers only
if (this.history === true) {
// There is an old bug in Chrome that causes onpopstate to fire even
// upon initial page load. Since the handler is run manually in init(),
// this would cause Chrome to run it twise. Currently the only
// workaround seems to be to set the handler after the initial page load
// http://code.google.com/p/chromium/issues/detail?id=63040
setTimeout(function() {
window.onpopstate = onchange;
}, 500);
}
else {
window.onhashchange = onchange;
}
this.mode = 'modern';
}
else {
//
// IE support, based on a concept by Erik Arvidson ...
//
var frame = document.createElement('iframe');
frame.id = 'state-frame';
frame.style.display = 'none';
document.body.appendChild(frame);
this.writeFrame('');
if ('onpropertychange' in document && 'attachEvent' in document) {
document.attachEvent('onpropertychange', function () {
if (event.propertyName === 'location') {
self.check();
}
});
}
window.setInterval(function () { self.check(); }, 50);
this.onHashChanged = onchange;
this.mode = 'legacy';
}
Router.listeners.push(fn);
return this.mode;
},
destroy: function (fn) {
if (!Router || !Router.listeners) {
return;
}
var listeners = Router.listeners;
for (var i = listeners.length - 1; i >= 0; i--) {
if (listeners[i] === fn) {
listeners.splice(i, 1);
}
}
},
setHash: function (s) {
// Mozilla always adds an entry to the history
if (this.mode === 'legacy') {
this.writeFrame(s);
}
if (this.history === true) {
window.history.pushState({}, document.title, s);
// Fire an onpopstate event manually since pushing does not obviously
// trigger the pop event.
this.fire();
} else {
dloc.hash = (s[0] === '/') ? s : '/' + s;
}
return this;
},
writeFrame: function (s) {
// IE support...
var f = document.getElementById('state-frame');
var d = f.contentDocument || f.contentWindow.document;
d.open();
d.write("<script>_hash = '" + s + "'; onload = parent.listener.syncHash;<script>");
d.close();
},
syncHash: function () {
// IE support...
var s = this._hash;
if (s != dloc.hash) {
dloc.hash = s;
}
return this;
},
onHashChanged: function () {}
};
var Router = exports.Router = function (routes) {
if (!(this instanceof Router)) return new Router(routes);
this.params = {};
this.routes = {};
this.methods = ['on', 'once', 'after', 'before'];
this.scope = [];
this._methods = {};
this._insert = this.insert;
this.insert = this.insertEx;
this.historySupport = (window.history != null ? window.history.pushState : null) != null
this.configure();
this.mount(routes || {});
};
Router.prototype.init = function (r) {
var self = this;
this.handler = function(onChangeEvent) {
var newURL = onChangeEvent && onChangeEvent.newURL || window.location.hash;
var url = self.history === true ? self.getPath() : newURL.replace(/.*#/, '');
self.dispatch('on', url);
};
listener.init(this.handler, this.history);
if (this.history === false) {
if (dlocHashEmpty() && r) {
dloc.hash = r;
} else if (!dlocHashEmpty()) {
self.dispatch('on', dloc.hash.replace(/^#/, ''));
}
}
else {
var routeTo = dlocHashEmpty() && r ? r : !dlocHashEmpty() ? dloc.hash.replace(/^#/, '') : null;
if (routeTo) {
window.history.replaceState({}, document.title, routeTo);
}
// Router has been initialized, but due to the chrome bug it will not
// yet actually route HTML5 history state changes. Thus, decide if should route.
if (routeTo || this.run_in_init === true) {
this.handler();
}
}
return this;
};
Router.prototype.explode = function () {
var v = this.history === true ? this.getPath() : dloc.hash;
if (v.charAt(1) === '/') { v=v.slice(1) }
return v.slice(1, v.length).split("/");
};
Router.prototype.setRoute = function (i, v, val) {
var url = this.explode();
if (typeof i === 'number' && typeof v === 'string') {
url[i] = v;
}
else if (typeof val === 'string') {
url.splice(i, v, s);
}
else {
url = [i];
}
listener.setHash(url.join('/'));
return url;
};
//
// ### function insertEx(method, path, route, parent)
// #### @method {string} Method to insert the specific `route`.
// #### @path {Array} Parsed path to insert the `route` at.
// #### @route {Array|function} Route handlers to insert.
// #### @parent {Object} **Optional** Parent "routes" to insert into.
// insert a callback that will only occur once per the matched route.
//
Router.prototype.insertEx = function(method, path, route, parent) {
if (method === "once") {
method = "on";
route = function(route) {
var once = false;
return function() {
if (once) return;
once = true;
return route.apply(this, arguments);
};
}(route);
}
return this._insert(method, path, route, parent);
};
Router.prototype.getRoute = function (v) {
var ret = v;
if (typeof v === "number") {
ret = this.explode()[v];
}
else if (typeof v === "string"){
var h = this.explode();
ret = h.indexOf(v);
}
else {
ret = this.explode();
}
return ret;
};
Router.prototype.destroy = function () {
listener.destroy(this.handler);
return this;
};
Router.prototype.getPath = function () {
var path = window.location.pathname;
if (path.substr(0, 1) !== '/') {
path = '/' + path;
}
return path;
};
var util = require('util'),
director = require('../director');
var Router = exports.Router = function (routes) {
director.Router.call(this, routes);
this.recurse = false;
};
//
// Inherit from `director.Router`.
//
util.inherits(Router, director.Router);
//
// ### function configure (options)
// #### @options {Object} **Optional** Options to configure this instance with
// Configures this instance with the specified `options`.
//
Router.prototype.configure = function (options) {
options = options || {};
director.Router.prototype.configure.call(this, options);
//
// Delimiter must always be `\s` in CLI routing.
// e.g. `jitsu users create`
//
this.delimiter = '\\s';
return this;
};
//
// ### function dispatch (method, path)
// #### @method {string} Method to dispatch
// #### @path {string} Path to dispatch
// Finds a set of functions on the traversal towards
// `method` and `path` in the core routing table then
// invokes them based on settings in this instance.
//
Router.prototype.dispatch = function (method, path, tty, callback) {
//
// Prepend a single space onto the path so that the traversal
// algorithm will recognize it. This is because we always assume
// that the `path` begins with `this.delimiter`.
//
path = ' ' + path;
var fns = this.traverse(method, path, this.routes, '');
if (!fns || fns.length === 0) {
if (typeof this.notfound === 'function') {
this.notfound.call({ tty: tty, cmd: path }, callback);
}
else if (callback) {
callback(new Error('Could not find path: ' + path));
}
return false;
}
if (this.recurse === 'forward') {
fns = fns.reverse();
}
this.invoke(this.runlist(fns), { tty: tty, cmd: path }, callback);
return true;
};
var events = require('events'),
qs = require('querystring'),
util = require('util'),
director = require('../../director'),
responses = require('./responses');
//
// ### Expose all HTTP methods and responses
//
exports.methods = require('./methods');
Object.keys(responses).forEach(function (name) {
exports[name] = responses[name];
});
//
// ### function Router (routes)
// #### @routes {Object} **Optional** Routing table for this instance.
// Constuctor function for the HTTP Router object responsible for building
// and dispatching from a given routing table.
//
var Router = exports.Router = function (routes) {
//
// ### Extend the `Router` prototype with all of the RFC methods.
//
this.params = {};
this.routes = {};
this.methods = ['on', 'after', 'before'];
this.scope = [];
this._methods = {};
this.recurse = 'forward';
this._attach = [];
this.extend(exports.methods.concat(['before', 'after']));
this.configure();
this.mount(routes || {});
};
//
// Inherit from `director.Router`.
//
util.inherits(Router, director.Router);
//
// ### function configure (options)
// #### @options {Object} **Optional** Options to configure this instance with
// Configures this instance with the specified `options`.
//
Router.prototype.configure = function (options) {
options = options || {};
// useful when using connect's bodyParser
this.stream = options.stream || false;
return director.Router.prototype.configure.call(this, options);
};
//
// ### function on (method, path, route)
// #### @method {string} **Optional** Method to use
// #### @path {string} Path to set this route on.
// #### @route {Array|function} Handler for the specified method and path.
// Adds a new `route` to this instance for the specified `method`
// and `path`.
//
Router.prototype.on = function (method, path) {
var args = Array.prototype.slice.call(arguments, 2),
route = args.pop(),
options = args.pop(),
accept;
if (options) {
if (options.stream) {
route.stream = true;
}
if (options.accept) {
this._hasAccepts = true;
accept = options.accept;
route.accept = (Array.isArray(accept) ? accept : [accept]).map(function (a) {
return typeof a === 'string' ? RegExp(a) : a;
});
}
}
if (typeof path !== 'string' && !path.source) {
path = '';
}
director.Router.prototype.on.call(this, method, path, route);
};
//
// ### function attach (func)
// ### @func {function} Function to execute on `this` before applying to router function
// Ask the router to attach objects or manipulate `this` object on which the
// function passed to the http router will get applied
Router.prototype.attach = function (func) {
this._attach.push(func);
};
//
// ### function dispatch (method, path)
// #### @req {http.ServerRequest} Incoming request to dispatch.
// #### @res {http.ServerResponse} Outgoing response to dispatch.
// #### @callback {function} **Optional** Continuation to respond to for async scenarios.
// Finds a set of functions on the traversal towards
// `method` and `path` in the core routing table then
// invokes them based on settings in this instance.
//
Router.prototype.dispatch = function (req, res, callback) {
//
// Dispatch `HEAD` requests to `GET`
//
var method = req.method === 'HEAD' ? 'get' : req.method.toLowerCase(),
thisArg = { req: req, res: res },
self = this,
contentType,
runlist,
stream,
error,
fns,
url;
//
// Trap bad URLs from `decodeUri`
//
try { url = decodeURI(req.url.split('?', 1)[0]) }
catch (ex) { url = null }
if (url && this._hasAccepts) {
contentType = req.headers['content-type'];
fns = this.traverse(method, url, this.routes, '', function (route) {
return !route.accept || route.accept.some(function (a) {
return a.test(contentType);
});
});
}
else if (url) {
fns = this.traverse(method, url, this.routes, '');
}
if (this._attach) {
for (var i in this._attach) {
this._attach[i].call(thisArg);
}
}
if (!fns || fns.length === 0) {
error = new exports.NotFound('Could not find path: ' + req.url);
if (typeof this.notfound === 'function') {
this.notfound.call(thisArg, callback);
}
else if (callback) {
callback.call(thisArg, error, req, res);
}
return false;
}
if (this.recurse === 'forward') {
fns = fns.reverse();
}
runlist = this.runlist(fns);
stream = this.stream || runlist.some(function (fn) { return fn.stream === true; });
function parseAndInvoke() {
error = self.parse(req);
if (error) {
if (callback) {
callback.call(thisArg, error, req, res);
}
return false;
}
self.invoke(runlist, thisArg, callback);
}
if (!stream) {
//
// If there is no streaming required on any of the functions on the
// way to `path`, then attempt to parse the fully buffered request stream
// once it has emitted the `end` event.
//
if (req.readable) {
//
// If the `http.ServerRequest` is still readable, then await
// the end event and then continue
//
req.once('end', parseAndInvoke);
// Streams2 requires us to start the stream if we're not explicitly
// reading from it.
req.resume();
}
else {
//
// Otherwise, just parse the body now.
//
parseAndInvoke();
}
}
else {
this.invoke(runlist, thisArg, callback);
}
return true;
};
//
// ### @parsers {Object}
// Lookup table of parsers to use when attempting to
// parse incoming responses.
//
Router.prototype.parsers = {
'application/x-www-form-urlencoded': qs.parse,
'application/json': JSON.parse
};
//
// ### function parse (req)
// #### @req {http.ServerResponse|BufferedStream} Incoming HTTP request to parse
// Attempts to parse `req.body` using the value found at `req.headers['content-type']`.
//
Router.prototype.parse = function (req) {
function mime(req) {
var str = req.headers['content-type'] || '';
return str.split(';')[0];
}
var parser = this.parsers[mime(req)],
body;
if (parser) {
req.body = req.body || '';
if (req.chunks) {
req.chunks.forEach(function (chunk) {
req.body += chunk;
});
}
try {
req.body = req.body && req.body.length
? parser(req.body)
: {};
}
catch (err) {
return new exports.BadRequest('Malformed data');
}
}
};
/*!
* Express - router - methods
* Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
* MIT Licensed
*
* Adapted for SS
* (C) 2011 Nodejitsu Inc. <info@nodejitsu.com>
*
*/
/**
* Hypertext Transfer Protocol -- HTTP/1.1
* http://www.ietf.org/rfc/rfc2616.txt
*/
var RFC2616 = ['OPTIONS', 'GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'CONNECT'];
/**
* HTTP Extensions for Distributed Authoring -- WEBDAV
* http://www.ietf.org/rfc/rfc2518.txt
*/
var RFC2518 = ['PROPFIND', 'PROPPATCH', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK'];
/**
* Versioning Extensions to WebDAV
* http://www.ietf.org/rfc/rfc3253.txt
*/
var RFC3253 = ['VERSION-CONTROL', 'REPORT', 'CHECKOUT', 'CHECKIN', 'UNCHECKOUT', 'MKWORKSPACE', 'UPDATE', 'LABEL', 'MERGE', 'BASELINE-CONTROL', 'MKACTIVITY'];
/**
* Ordered Collections Protocol (WebDAV)
* http://www.ietf.org/rfc/rfc3648.txt
*/
var RFC3648 = ['ORDERPATCH'];
/**
* Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol
* http://www.ietf.org/rfc/rfc3744.txt
*/
var RFC3744 = ['ACL'];
/**
* Web Distributed Authoring and Versioning (WebDAV) SEARCH
* http://www.ietf.org/rfc/rfc5323.txt
*/
var RFC5323 = ['SEARCH'];
/**
* PATCH Method for HTTP
* http://www.ietf.org/rfc/rfc5789.txt
*/
var RFC5789 = ['PATCH'];
/**
* Expose the methods.
*/
module.exports = [].concat(
RFC2616,
RFC2518,
RFC3253,
RFC3648,
RFC3744,
RFC5323,
RFC5789
).map(function (method) {
return method.toLowerCase();
});
\ No newline at end of file
//
// HTTP Error objectst
//
var util = require('util');
exports.NotModified = function () {
this.status = 304;
this.options = {
removeContentHeaders: true
};
};
util.inherits(exports.NotModified, Error);
exports.BadRequest = function (msg) {
msg = msg || 'Bad request';
this.status = 400;
this.headers = {};
this.message = msg;
this.body = { error: msg };
};
util.inherits(exports.BadRequest, Error);
exports.NotAuthorized = function (msg) {
msg = msg || 'Not Authorized';
this.status = 401;
this.headers = {};
this.message = msg;
this.body = { error: msg };
};
util.inherits(exports.NotAuthorized, Error);
exports.Forbidden = function (msg) {
msg = msg || 'Not Authorized';
this.status = 403;
this.headers = {};
this.message = msg;
this.body = { error: msg };
};
util.inherits(exports.Forbidden, Error);
exports.NotFound = function (msg) {
msg = msg || 'Not Found';
this.status = 404;
this.headers = {};
this.message = msg;
this.body = { error: msg };
};
util.inherits(exports.NotFound, Error);
exports.MethodNotAllowed = function (allowed) {
var msg = 'method not allowed.';
this.status = 405;
this.headers = { allow: allowed };
this.message = msg;
this.body = { error: msg };
};
util.inherits(exports.MethodNotAllowed, Error);
exports.NotAcceptable = function (accept) {
var msg = 'cannot generate "' + accept + '" response';
this.status = 406;
this.headers = {};
this.message = msg;
this.body = {
error: msg,
only: 'application/json'
};
};
util.inherits(exports.NotAcceptable, Error);
exports.NotImplemented = function (msg) {
msg = msg || 'Not Implemented';
this.status = 501;
this.headers = {};
this.message = msg;
this.body = { error: msg };
};
util.inherits(exports.NotImplemented, Error);
/*
* router.js: Base functionality for the router.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
//
// Helper function to turn flatten an array.
//
function _flatten (arr) {
var flat = [];
for (var i = 0, n = arr.length; i < n; i++) {
flat = flat.concat(arr[i]);
}
return flat;
}
//
// Helper function for wrapping Array.every
// in the browser.
//
function _every (arr, iterator) {
for (var i = 0; i < arr.length; i += 1) {
if (iterator(arr[i], i, arr) === false) {
return;
}
}
}
//
// Helper function for performing an asynchronous every
// in series in the browser and the server.
//
function _asyncEverySeries (arr, iterator, callback) {
if (!arr.length) {
return callback();
}
var completed = 0;
(function iterate() {
iterator(arr[completed], function (err) {
if (err || err === false) {
callback(err);
callback = function () {};
}
else {
completed += 1;
if (completed === arr.length) {
callback();
}
else {
iterate();
}
}
});
})();
}
//
// Helper function for expanding "named" matches
// (e.g. `:dog`, etc.) against the given set
// of params:
//
// {
// ':dog': function (str) {
// return str.replace(/:dog/, 'TARGET');
// }
// ...
// }
//
function paramifyString(str, params, mod) {
mod = str;
for (var param in params) {
if (params.hasOwnProperty(param)) {
mod = params[param](str);
if (mod !== str) { break; }
}
}
return mod === str
? '([._a-zA-Z0-9-]+)'
: mod;
}
//
// Helper function for expanding wildcards (*) and
// "named" matches (:whatever)
//
function regifyString(str, params) {
var matches,
last = 0,
out = '';
while (matches = str.substr(last).match(/[^\w\d\- %@&]*\*[^\w\d\- %@&]*/)) {
last = matches.index + matches[0].length;
matches[0] = matches[0].replace(/^\*/, '([_\.\(\)!\\ %@&a-zA-Z0-9-]+)');
out += str.substr(0, matches.index) + matches[0];
}
str = out += str.substr(last);
var captures = str.match(/:([^\/]+)/ig),
length;
if (captures) {
length = captures.length;
for (var i = 0; i < length; i++) {
str = str.replace(captures[i], paramifyString(captures[i], params));
}
}
return str;
}
//
// ### Fix unterminated RegExp groups in routes.
//
function terminator(routes, delimiter, start, stop) {
var last = 0,
left = 0,
right = 0,
start = (start || '(').toString(),
stop = (stop || ')').toString(),
i;
for (i = 0; i < routes.length; i++) {
var chunk = routes[i];
if ((chunk.indexOf(start, last) > chunk.indexOf(stop, last)) ||
(~chunk.indexOf(start, last) && !~chunk.indexOf(stop, last)) ||
(!~chunk.indexOf(start, last) && ~chunk.indexOf(stop, last))) {
left = chunk.indexOf(start, last);
right = chunk.indexOf(stop, last);
if ((~left && !~right) || (!~left && ~right)) {
var tmp = routes.slice(0, (i || 1) + 1).join(delimiter);
routes = [tmp].concat(routes.slice((i || 1) + 1));
}
last = (right > left ? right : left) + 1;
i = 0;
}
else {
last = 0;
}
}
return routes;
}
//
// ### function Router (routes)
// #### @routes {Object} **Optional** Routing table for this instance.
// Constuctor function for the Router object responsible for building
// and dispatching from a given routing table.
//
var Router = exports.Router = function (routes) {
this.params = {};
this.routes = {};
this.methods = ['on', 'after', 'before'];
this.scope = [];
this._methods = {};
this.configure();
this.mount(routes || {});
};
//
// ### function configure (options)
// #### @options {Object} **Optional** Options to configure this instance with
// Configures this instance with the specified `options`.
//
Router.prototype.configure = function (options) {
options = options || {};
for (var i = 0; i < this.methods.length; i++) {
this._methods[this.methods[i]] = true;
}
this.recurse = options.recurse || this.recurse || false;
this.async = options.async || false;
this.delimiter = options.delimiter || '\/';
this.strict = typeof options.strict === 'undefined' ? true : options.strict;
this.notfound = options.notfound;
this.resource = options.resource;
// Client only, but browser.js does not include a super implementation
this.history = (options.html5history && this.historySupport) || false;
this.run_in_init = (this.history === true && options.run_handler_in_init !== false);
//
// TODO: Global once
//
this.every = {
after: options.after || null,
before: options.before || null,
on: options.on || null
};
return this;
};
//
// ### function param (token, regex)
// #### @token {string} Token which to replace (e.g. `:dog`, 'cat')
// #### @matcher {string|RegExp} Target to replace the token with.
// Setups up a `params` function which replaces any instance of `token`,
// inside of a given `str` with `matcher`. This is very useful if you
// have a common regular expression throughout your code base which
// you wish to be more DRY.
//
Router.prototype.param = function (token, matcher) {
if (token[0] !== ':') {
token = ':' + token;
}
var compiled = new RegExp(token, 'g');
this.params[token] = function (str) {
return str.replace(compiled, matcher.source || matcher);
};
};
//
// ### function on (method, path, route)
// #### @method {string} **Optional** Method to use
// #### @path {Array|string} Path to set this route on.
// #### @route {Array|function} Handler for the specified method and path.
// Adds a new `route` to this instance for the specified `method`
// and `path`.
//
Router.prototype.on = Router.prototype.route = function (method, path, route) {
var self = this;
if (!route && typeof path == 'function') {
//
// If only two arguments are supplied then assume this
// `route` was meant to be a generic `on`.
//
route = path;
path = method;
method = 'on';
}
if (Array.isArray(path)) {
return path.forEach(function(p) {
self.on(method, p, route);
});
}
if (path.source) {
path = path.source.replace(/\\\//ig, '/');
}
if (Array.isArray(method)) {
return method.forEach(function (m) {
self.on(m.toLowerCase(), path, route);
});
}
//
// ### Split the route up by the delimiter.
//
path = path.split(new RegExp(this.delimiter));
//
// ### Fix unterminated groups. Fixes #59
//
path = terminator(path, this.delimiter);
this.insert(method, this.scope.concat(path), route);
};
//
// ### function path (path, routesFn)
// #### @path {string|RegExp} Nested scope in which to path
// #### @routesFn {function} Function to evaluate in the new scope
// Evalutes the `routesFn` in the given path scope.
//
Router.prototype.path = function (path, routesFn) {
var self = this,
length = this.scope.length;
if (path.source) {
path = path.source.replace(/\\\//ig, '/');
}
//
// ### Split the route up by the delimiter.
//
path = path.split(new RegExp(this.delimiter));
//
// ### Fix unterminated groups.
//
path = terminator(path, this.delimiter);
this.scope = this.scope.concat(path);
routesFn.call(this, this);
this.scope.splice(length, path.length);
};
//
// ### function dispatch (method, path[, callback])
// #### @method {string} Method to dispatch
// #### @path {string} Path to dispatch
// #### @callback {function} **Optional** Continuation to respond to for async scenarios.
// Finds a set of functions on the traversal towards
// `method` and `path` in the core routing table then
// invokes them based on settings in this instance.
//
Router.prototype.dispatch = function (method, path, callback) {
var self = this,
fns = this.traverse(method, path, this.routes, ''),
invoked = this._invoked,
after;
this._invoked = true;
if (!fns || fns.length === 0) {
this.last = [];
if (typeof this.notfound === 'function') {
this.invoke([this.notfound], { method: method, path: path }, callback);
}
return false;
}
if (this.recurse === 'forward') {
fns = fns.reverse();
}
function updateAndInvoke() {
self.last = fns.after;
self.invoke(self.runlist(fns), self, callback);
}
//
// Builds the list of functions to invoke from this call
// to dispatch conforming to the following order:
//
// 1. Global after (if any)
// 2. After functions from the last call to dispatch
// 3. Global before (if any)
// 4. Global on (if any)
// 5. Matched functions from routing table (`['before', 'on'], ['before', 'on`], ...]`)
//
after = this.every && this.every.after
? [this.every.after].concat(this.last)
: [this.last];
if (after && after.length > 0 && invoked) {
if (this.async) {
this.invoke(after, this, updateAndInvoke);
}
else {
this.invoke(after, this);
updateAndInvoke();
}
return true;
}
updateAndInvoke();
return true;
};
//
// ### function runlist (fns)
// #### @fns {Array} List of functions to include in the runlist
// Builds the list of functions to invoke from this call
// to dispatch conforming to the following order:
//
// 1. Global before (if any)
// 2. Global on (if any)
// 3. Matched functions from routing table (`['before', 'on'], ['before', 'on`], ...]`)
//
Router.prototype.runlist = function (fns) {
var runlist = this.every && this.every.before
? [this.every.before].concat(_flatten(fns))
: _flatten(fns);
if (this.every && this.every.on) {
runlist.push(this.every.on);
}
runlist.captures = fns.captures;
runlist.source = fns.source;
return runlist;
};
//
// ### function invoke (fns, thisArg)
// #### @fns {Array} Set of functions to invoke in order.
// #### @thisArg {Object} `thisArg` for each function.
// #### @callback {function} **Optional** Continuation to pass control to for async `fns`.
// Invokes the `fns` synchronously or asynchronously depending on the
// value of `this.async`. Each function must **not** return (or respond)
// with false, or evaluation will short circuit.
//
Router.prototype.invoke = function (fns, thisArg, callback) {
var self = this;
if (this.async) {
_asyncEverySeries(fns, function apply(fn, next) {
if (Array.isArray(fn)) {
return _asyncEverySeries(fn, apply, next);
}
else if (typeof fn == 'function') {
fn.apply(thisArg, fns.captures.concat(next));
}
}, function () {
//
// Ignore the response here. Let the routed take care
// of themselves and eagerly return true.
//
if (callback) {
callback.apply(thisArg, arguments);
}
});
}
else {
_every(fns, function apply(fn) {
if (Array.isArray(fn)) {
return _every(fn, apply);
}
else if (typeof fn === 'function') {
return fn.apply(thisArg, fns.captures || []);
}
else if (typeof fn === 'string' && self.resource) {
self.resource[fn].apply(thisArg, fns.captures || []);
}
});
}
};
//
// ### function traverse (method, path, routes, regexp)
// #### @method {string} Method to find in the `routes` table.
// #### @path {string} Path to find in the `routes` table.
// #### @routes {Object} Partial routing table to match against
// #### @regexp {string} Partial regexp representing the path to `routes`.
// #### @filter {function} Filter function for filtering routes (expensive).
// Core routing logic for `director.Router`: traverses the
// specified `path` within `this.routes` looking for `method`
// returning any `fns` that are found.
//
Router.prototype.traverse = function (method, path, routes, regexp, filter) {
var fns = [],
current,
exact,
match,
next,
that;
function filterRoutes(routes) {
if (!filter) {
return routes;
}
function deepCopy(source) {
var result = [];
for (var i = 0; i < source.length; i++) {
result[i] = Array.isArray(source[i]) ? deepCopy(source[i]) : source[i];
}
return result;
}
function applyFilter(fns) {
for (var i = fns.length - 1; i >= 0; i--) {
if (Array.isArray(fns[i])) {
applyFilter(fns[i]);
if (fns[i].length === 0) {
fns.splice(i, 1);
}
}
else {
if (!filter(fns[i])) {
fns.splice(i, 1);
}
}
}
}
var newRoutes = deepCopy(routes);
newRoutes.matched = routes.matched;
newRoutes.captures = routes.captures;
newRoutes.after = routes.after.filter(filter);
applyFilter(newRoutes);
return newRoutes;
}
//
// Base Case #1:
// If we are dispatching from the root
// then only check if the method exists.
//
if (path === this.delimiter && routes[method]) {
next = [[routes.before, routes[method]].filter(Boolean)];
next.after = [routes.after].filter(Boolean);
next.matched = true;
next.captures = [];
return filterRoutes(next);
}
for (var r in routes) {
//
// We dont have an exact match, lets explore the tree
// in a depth-first, recursive, in-order manner where
// order is defined as:
//
// ['before', 'on', '<method>', 'after']
//
// Remember to ignore keys (i.e. values of `r`) which
// are actual methods (e.g. `on`, `before`, etc), but
// which are not actual nested route (i.e. JSON literals).
//
if (routes.hasOwnProperty(r) && (!this._methods[r] ||
this._methods[r] && typeof routes[r] === 'object' && !Array.isArray(routes[r]))) {
//
// Attempt to make an exact match for the current route
// which is built from the `regexp` that has been built
// through recursive iteration.
//
current = exact = regexp + this.delimiter + r;
if (!this.strict) {
exact += '[' + this.delimiter + ']?';
}
match = path.match(new RegExp('^' + exact));
if (!match) {
//
// If there isn't a `match` then continue. Here, the
// `match` is a partial match. e.g.
//
// '/foo/bar/buzz'.match(/^\/foo/) // ['/foo']
// '/no-match/route'.match(/^\/foo/) // null
//
continue;
}
if (match[0] && match[0] == path && routes[r][method]) {
//
// ### Base case 2:
// If we had a `match` and the capture is the path itself,
// then we have completed our recursion.
//
next = [[routes[r].before, routes[r][method]].filter(Boolean)];
next.after = [routes[r].after].filter(Boolean);
next.matched = true;
next.captures = match.slice(1);
if (this.recurse && routes === this.routes) {
next.push([routes.before, routes.on].filter(Boolean));
next.after = next.after.concat([routes.after].filter(Boolean));
}
return filterRoutes(next);
}
//
// ### Recursive case:
// If we had a match, but it is not yet an exact match then
// attempt to continue matching against the next portion of the
// routing table.
//
next = this.traverse(method, path, routes[r], current);
//
// `next.matched` will be true if the depth-first search of the routing
// table from this position was successful.
//
if (next.matched) {
//
// Build the in-place tree structure representing the function
// in the correct order.
//
if (next.length > 0) {
fns = fns.concat(next);
}
if (this.recurse) {
fns.push([routes[r].before, routes[r].on].filter(Boolean));
next.after = next.after.concat([routes[r].after].filter(Boolean));
if (routes === this.routes) {
fns.push([routes['before'], routes['on']].filter(Boolean));
next.after = next.after.concat([routes['after']].filter(Boolean));
}
}
fns.matched = true;
fns.captures = next.captures;
fns.after = next.after;
//
// ### Base case 2:
// Continue passing the partial tree structure back up the stack.
// The caller for `dispatch()` will decide what to do with the functions.
//
return filterRoutes(fns);
}
}
}
return false;
};
//
// ### function insert (method, path, route, context)
// #### @method {string} Method to insert the specific `route`.
// #### @path {Array} Parsed path to insert the `route` at.
// #### @route {Array|function} Route handlers to insert.
// #### @parent {Object} **Optional** Parent "routes" to insert into.
// Inserts the `route` for the `method` into the routing table for
// this instance at the specified `path` within the `context` provided.
// If no context is provided then `this.routes` will be used.
//
Router.prototype.insert = function (method, path, route, parent) {
var methodType,
parentType,
isArray,
nested,
part;
path = path.filter(function (p) {
return p && p.length > 0;
});
parent = parent || this.routes;
part = path.shift();
if (/\:|\*/.test(part) && !/\\d|\\w/.test(part)) {
part = regifyString(part, this.params);
}
if (path.length > 0) {
//
// If this is not the last part left in the `path`
// (e.g. `['cities', 'new-york']`) then recurse into that
// child
//
parent[part] = parent[part] || {};
return this.insert(method, path, route, parent[part]);
}
//
// If there is no part and the path has been exhausted
// and the parent is the root of the routing table,
// then we are inserting into the root and should
// only dive one level deep in the Routing Table.
//
if (!part && !path.length && parent === this.routes) {
methodType = typeof parent[method];
switch (methodType) {
case 'function':
parent[method] = [parent[method], route];
return;
case 'object':
parent[method].push(route);
return;
case 'undefined':
parent[method] = route;
return;
}
return;
}
//
// Otherwise, we are at the end of our insertion so we should
// insert the `route` based on the `method` after getting the
// `parent` of the last `part`.
//
parentType = typeof parent[part];
isArray = Array.isArray(parent[part]);
if (parent[part] && !isArray && parentType == 'object') {
methodType = typeof parent[part][method];
switch (methodType) {
case 'function':
parent[part][method] = [parent[part][method], route];
return;
case 'object':
parent[part][method].push(route);
return;
case 'undefined':
parent[part][method] = route;
return;
}
}
else if (parentType == 'undefined') {
nested = {};
nested[method] = route;
parent[part] = nested;
return;
}
throw new Error('Invalid route context: ' + parentType);
};
//
// ### function extend (methods)
// #### @methods {Array} List of method names to extend this instance with
// Extends this instance with simple helper methods to `this.on`
// for each of the specified `methods`
//
Router.prototype.extend = function(methods) {
var self = this,
len = methods.length,
i;
function extend(method) {
self._methods[method] = true;
self[method] = function () {
var extra = arguments.length === 1
? [method, '']
: [method];
self.on.apply(self, extra.concat(Array.prototype.slice.call(arguments)));
};
}
for (i = 0; i < len; i++) {
extend(methods[i]);
}
};
//
// ### function mount (routes, context)
// #### @routes {Object} Routes to mount onto this instance
// Mounts the sanitized `routes` onto the root context for this instance.
//
// e.g.
//
// new Router().mount({ '/foo': { '/bar': function foobar() {} } })
//
// yields
//
// { 'foo': 'bar': function foobar() {} } }
//
Router.prototype.mount = function(routes, path) {
if (!routes || typeof routes !== "object" || Array.isArray(routes)) {
return;
}
var self = this;
path = path || [];
if (!Array.isArray(path)) {
path = path.split(self.delimiter);
}
function insertOrMount(route, local) {
var rename = route,
parts = route.split(self.delimiter),
routeType = typeof routes[route],
isRoute = parts[0] === "" || !self._methods[parts[0]],
event = isRoute ? "on" : rename;
if (isRoute) {
rename = rename.slice((rename.match(new RegExp(self.delimiter)) || [''])[0].length);
parts.shift();
}
if (isRoute && routeType === 'object' && !Array.isArray(routes[route])) {
local = local.concat(parts);
self.mount(routes[route], local);
return;
}
if (isRoute) {
local = local.concat(rename.split(self.delimiter));
local = terminator(local, self.delimiter);
}
self.insert(event, local, routes[route]);
}
for (var route in routes) {
if (routes.hasOwnProperty(route)) {
insertOrMount(route, path.slice(0));
}
}
};
{
"name": "director",
"description": "A client Side/Server Side Router",
"author": "Nodejitsu Inc. <info@nodejitsu.com>",
"version": "1.2.0",
"maintainers": [
"hij1nx <paolo@nodejitsu.com>",
"indexzero <charlie@nodejitsu.com>"
],
"repository": {
"type": "git",
"url": "http://github.com/flatiron/director.git"
},
"keywords": [
"URL",
"router",
"http",
"cli",
"flatiron",
"client side",
"ender"
],
"devDependencies": {
"codesurgeon": "https://github.com/hij1nx/codesurgeon/tarball/master",
"colors": "0.5.x",
"api-easy": "0.3.x",
"uglify-js": "1.0.6",
"request": "2.9.x",
"qunitjs": "1.9.x",
"vows": "0.6.x"
},
"ender": "./build/ender.js",
"browserify": "./build/director",
"main": "./lib/director",
"engines": {
"node": ">= 0.4.0"
},
"scripts": {
"test": "vows test/server/*/*-test.js --spec"
}
}
var http = require('http'),
fs = require('fs'),
path = require('path'),
director = require('../../../lib/director'),
index;
fs.readFile(path.join(__dirname, '..', 'html5-routes-harness.html'), function (err, data) {
if (err) {
throw err;
}
index = data;
});
var CONTENT_TYPES = {
'.js' : 'text/javascript',
'.css' : 'text/css'
};
// Dummy file server
function fileServer(folder, file) {
var root = path.resolve(__dirname, '..');
if (folder === 'build' || folder === 'node_modules') {
root = path.resolve(root, '..', '..');
}
var filepath = path.resolve(root, folder, file);
var res = this.res;
(fs.exists || path.exists)(filepath, function (exists) {
if (exists) {
fs.readFile(filepath, function (err, data) {
if (err) {
res.writeHead(404);
res.end();
}
res.writeHead(200, {'Content-Type': CONTENT_TYPES[path.extname(filepath)]});
res.end(data);
});
} else {
res.writeHead(404);
res.end();
}
});
}
var router = new director.http.Router({
'/files': {
'/:folder': {
'/(.+)': {
get: fileServer
},
get: fileServer
}
}
});
var server = http.createServer(function (req, res) {
router.dispatch(req, res, function (err) {
if (err && req.url != '/favicon.ico') {
// By default just reply with the index page
this.res.writeHead(200, {'Content-Type': 'text/html'});
this.res.end(index);
}
});
});
server.listen(8080);
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Director Browserify Tests</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
</head>
<body>
<p>To run these tests first generate a Browserify bundle for director by running the command <kbd>browserify -r ../director -o test/browser/browserified-bundle.js</kbd> in the repo's root directory.</p>
<div id="qunit">
<div id="qunit-fixture"></div>
<script src="browserified-bundle.js"></script>
<script>
var HTML5TEST = false;
var RouterAlias = require('/director').Router;
</script>
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script src="helpers/api.js"></script>
<script src="routes-test.js"></script>
</body>
</html>
module("Director.js", {
setup: function() {
window.location.hash = "";
shared = {};
// Init needed keys earlier because of in HTML5 mode the route handler
// is executed upon Router.init() and due to that setting shared.fired
// in the param test of createTest is too late
if (HTML5TEST) {
shared.fired = [];
shared.fired_count = 0;
}
},
teardown: function() {
window.location.hash = "";
shared = {};
}
});
var shared;
function createTest(name, config, use, test, initialRoute) {
// We rename to `RouterAlias` for the browserify tests, since we want to be
// sure that no code is depending on `window.Router` being available.
var Router = window.Router || window.RouterAlias;
if (typeof use === 'function') {
test = use;
use = undefined;
}
if (HTML5TEST) {
if (use === undefined) {
use = {};
}
if (use.run_handler_in_init === undefined) {
use.run_handler_in_init = false;
}
use.html5history = true;
}
// Because of the use of setTimeout when defining onpopstate
var innerTimeout = HTML5TEST === true ? 500 : 0;
asyncTest(name, function() {
setTimeout(function() {
var router = new Router(config),
context;
if (use !== undefined) {
router.configure(use);
}
router.init(initialRoute);
setTimeout(function() {
test.call(context = {
router: router,
navigate: function(url, callback) {
if (HTML5TEST) {
router.setRoute(url);
} else {
window.location.hash = url;
}
setTimeout(function() {
callback.call(context);
}, 14);
},
finish: function() {
router.destroy();
start();
}
})
}, innerTimeout);
}, 14);
});
};
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Director HTML5 Tests</title>
<link rel="stylesheet" href="/files/node_modules/qunitjs/qunit/qunit.css">
</head>
<body>
<p>Note: in order to execute HTML5 mode test this file needs to be served with provided nodejs backend. Start the server from <kbd>director/test/browser/backend</kbd> and go to <kbd>http://localhost:8080/</kbd> to launch the tests.</p>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script>
var HTML5TEST = true;
</script>
<script src="/files/node_modules/qunitjs/qunit/qunit.js"></script>
<script src="/files/build/director.js"></script>
<script src="/files/helpers/api.js"></script>
<script src="/files/html5-routes-test.js"></script>
</body>
</html>
var browser_history_support = (window.history != null ? window.history.pushState : null) != null;
createTest('Nested route with the many children as a tokens, callbacks should yield historic params', {
'/a': {
'/:id': {
'/:id': function(a, b) {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''), a, b);
} else {
shared.fired.push(location.pathname, a, b);
}
}
}
}
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['/a/b/c', 'b', 'c']);
this.finish();
});
});
createTest('Nested route with the first child as a token, callback should yield a param', {
'/foo': {
'/:id': {
on: function(id) {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''), id);
} else {
shared.fired.push(location.pathname, id);
}
}
}
}
}, function() {
this.navigate('/foo/a', function() {
this.navigate('/foo/b/c', function() {
deepEqual(shared.fired, ['/foo/a', 'a']);
this.finish();
});
});
});
createTest('Nested route with the first child as a regexp, callback should yield a param', {
'/foo': {
'/(\\w+)': {
on: function(value) {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''), value);
} else {
shared.fired.push(location.pathname, value);
}
}
}
}
}, function() {
this.navigate('/foo/a', function() {
this.navigate('/foo/b/c', function() {
deepEqual(shared.fired, ['/foo/a', 'a']);
this.finish();
});
});
});
createTest('Nested route with the several regular expressions, callback should yield a param', {
'/a': {
'/(\\w+)': {
'/(\\w+)': function(a, b) {
shared.fired.push(a, b);
}
}
}
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['b', 'c']);
this.finish();
});
});
createTest('Single nested route with on member containing function value', {
'/a': {
'/b': {
on: function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}
}
}
}, function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['/a/b']);
this.finish();
});
});
createTest('Single non-nested route with on member containing function value', {
'/a/b': {
on: function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}
}
}, function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['/a/b']);
this.finish();
});
});
createTest('Single nested route with on member containing array of function values', {
'/a': {
'/b': {
on: [function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
},
function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}]
}
}
}, function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['/a/b', '/a/b']);
this.finish();
});
});
createTest('method should only fire once on the route.', {
'/a': {
'/b': {
once: function() {
shared.fired_count++;
}
}
}
}, function() {
this.navigate('/a/b', function() {
this.navigate('/a/b', function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired_count, 1);
this.finish();
});
});
});
});
createTest('method should only fire once on the route, multiple nesting.', {
'/a': {
on: function() { shared.fired_count++; },
once: function() { shared.fired_count++; }
},
'/b': {
on: function() { shared.fired_count++; },
once: function() { shared.fired_count++; }
}
}, function() {
this.navigate('/a', function() {
this.navigate('/b', function() {
this.navigate('/a', function() {
this.navigate('/b', function() {
deepEqual(shared.fired_count, 6);
this.finish();
});
});
});
});
});
createTest('overlapping routes with tokens.', {
'/a/:b/c' : function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
},
'/a/:b/c/:d' : function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}
}, function() {
this.navigate('/a/b/c', function() {
this.navigate('/a/b/c/d', function() {
deepEqual(shared.fired, ['/a/b/c', '/a/b/c/d']);
this.finish();
});
});
});
// // //
// // // Recursion features
// // // ----------------------------------------------------------
createTest('Nested routes with no recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['c']);
this.finish();
});
});
createTest('Nested routes with backward recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'backward'
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['c', 'b', 'a']);
this.finish();
});
});
createTest('Breaking out of nested routes with backward recursion', {
'/a': {
'/:b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
return false;
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'backward'
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['c', 'b']);
this.finish();
});
});
createTest('Nested routes with forward recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'forward'
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['a', 'b', 'c']);
this.finish();
});
});
createTest('Nested routes with forward recursion, single route with an after event.', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
},
after: function() {
shared.fired.push('c-after');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'forward'
}, function() {
this.navigate('/a/b/c', function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['a', 'b', 'c', 'c-after', 'a', 'b']);
this.finish();
});
});
});
createTest('Breaking out of nested routes with forward recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
return false;
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'forward'
}, function() {
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['a', 'b']);
this.finish();
});
});
//
// ABOVE IS WORKING
//
// //
// // Special Events
// // ----------------------------------------------------------
createTest('All global event should fire after every route', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
'/c': {
on: function a() {
shared.fired.push('a');
}
}
},
'/d': {
'/:e': {
on: function a() {
shared.fired.push('a');
}
}
}
}, {
after: function() {
shared.fired.push('b');
}
}, function() {
this.navigate('/a', function() {
this.navigate('/b/c', function() {
this.navigate('/d/e', function() {
deepEqual(shared.fired, ['a', 'b', 'a', 'b', 'a']);
this.finish();
});
});
});
});
createTest('Not found.', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
on: function a() {
shared.fired.push('b');
}
}
}, {
notfound: function() {
shared.fired.push('notfound');
}
}, function() {
this.navigate('/c', function() {
this.navigate('/d', function() {
deepEqual(shared.fired, ['notfound', 'notfound']);
this.finish();
});
});
});
createTest('On all.', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
on: function a() {
shared.fired.push('b');
}
}
}, {
on: function() {
shared.fired.push('c');
}
}, function() {
this.navigate('/a', function() {
this.navigate('/b', function() {
deepEqual(shared.fired, ['a', 'c', 'b', 'c']);
this.finish();
});
});
});
createTest('After all.', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
on: function a() {
shared.fired.push('b');
}
}
}, {
after: function() {
shared.fired.push('c');
}
}, function() {
this.navigate('/a', function() {
this.navigate('/b', function() {
deepEqual(shared.fired, ['a', 'c', 'b']);
this.finish();
});
});
});
createTest('resource object.', {
'/a': {
'/b/:c': {
on: 'f1'
},
on: 'f2'
},
'/d': {
on: ['f1', 'f2']
}
},
{
resource: {
f1: function (name){
shared.fired.push("f1-" + name);
},
f2: function (name){
shared.fired.push("f2");
}
}
}, function() {
this.navigate('/a/b/c', function() {
this.navigate('/d', function() {
deepEqual(shared.fired, ['f1-c', 'f1-undefined', 'f2']);
this.finish();
});
});
});
createTest('argument matching should be case agnostic', {
'/fooBar/:name': {
on: function(name) {
shared.fired.push("fooBar-" + name);
}
}
}, function() {
this.navigate('/fooBar/tesTing', function() {
deepEqual(shared.fired, ['fooBar-tesTing']);
this.finish();
});
});
createTest('sanity test', {
'/is/:this/:sane': {
on: function(a, b) {
shared.fired.push('yes ' + a + ' is ' + b);
}
},
'/': {
on: function() {
shared.fired.push('is there sanity?');
}
}
}, function() {
this.navigate('/is/there/sanity', function() {
deepEqual(shared.fired, ['yes there is sanity']);
this.finish();
});
});
createTest('`/` route should be navigable from the routing table', {
'/': {
on: function root() {
shared.fired.push('/');
}
},
'/:username': {
on: function afunc(username) {
shared.fired.push('/' + username);
}
}
}, function() {
this.navigate('/', function root() {
deepEqual(shared.fired, ['/']);
this.finish();
});
});
createTest('`/` route should not override a `/:token` route', {
'/': {
on: function root() {
shared.fired.push('/');
}
},
'/:username': {
on: function afunc(username) {
shared.fired.push('/' + username);
}
}
}, function() {
this.navigate('/a', function afunc() {
deepEqual(shared.fired, ['/a']);
this.finish();
});
});
createTest('should accept the root as a token.', {
'/:a': {
on: function root() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}
}
}, function() {
this.navigate('/a', function root() {
deepEqual(shared.fired, ['/a']);
this.finish();
});
});
createTest('routes should allow wildcards.', {
'/:a/b*d': {
on: function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}
}
}, function() {
this.navigate('/a/bcd', function root() {
deepEqual(shared.fired, ['/a/bcd']);
this.finish();
});
});
createTest('functions should have |this| context of the router instance.', {
'/': {
on: function root() {
shared.fired.push(!!this.routes);
}
}
}, function() {
this.navigate('/', function root() {
deepEqual(shared.fired, [true]);
this.finish();
});
});
createTest('setRoute with a single parameter should change location correctly', {
'/bonk': {
on: function() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(window.location.pathname);
}
}
}
}, function() {
var self = this;
this.router.setRoute('/bonk');
setTimeout(function() {
deepEqual(shared.fired, ['/bonk']);
self.finish();
}, 14)
});
createTest('route should accept _ and . within parameters', {
'/:a': {
on: function root() {
if (!browser_history_support) {
shared.fired.push(location.hash.replace(/^#/, ''));
} else {
shared.fired.push(location.pathname);
}
}
}
}, function() {
this.navigate('/a_complex_route.co.uk', function root() {
deepEqual(shared.fired, ['/a_complex_route.co.uk']);
this.finish();
});
});
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Director Tests</title>
<link rel="stylesheet" href="../../node_modules/qunitjs/qunit/qunit.css">
</head>
<body>
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script>
var HTML5TEST = false;
</script>
<script src="../../node_modules/qunitjs/qunit/qunit.js"></script>
<script src="../../build/director.js"></script>
<script src="helpers/api.js"></script>
<script src="routes-test.js"></script>
</body>
</html>
createTest('Nested route with the many children as a tokens, callbacks should yield historic params', {
'/a': {
'/:id': {
'/:id': function(a, b) {
shared.fired.push(location.hash, a, b);
}
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['#/a/b/c', 'b', 'c']);
this.finish();
});
});
createTest('Nested route with the first child as a token, callback should yield a param', {
'/foo': {
'/:id': {
on: function(id) {
shared.fired.push(location.hash, id);
}
}
}
}, function() {
shared.fired = [];
this.navigate('/foo/a', function() {
this.navigate('/foo/b/c', function() {
deepEqual(shared.fired, ['#/foo/a', 'a']);
this.finish();
});
});
});
createTest('Nested route with the first child as a regexp, callback should yield a param', {
'/foo': {
'/(\\w+)': {
on: function(value) {
shared.fired.push(location.hash, value);
}
}
}
}, function() {
shared.fired = [];
this.navigate('/foo/a', function() {
this.navigate('/foo/b/c', function() {
deepEqual(shared.fired, ['#/foo/a', 'a']);
this.finish();
});
});
});
createTest('Nested route with the several regular expressions, callback should yield a param', {
'/a': {
'/(\\w+)': {
'/(\\w+)': function(a, b) {
shared.fired.push(a, b);
}
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['b', 'c']);
this.finish();
});
});
createTest('Single nested route with on member containing function value', {
'/a': {
'/b': {
on: function() {
shared.fired.push(location.hash);
}
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['#/a/b']);
this.finish();
});
});
createTest('Single non-nested route with on member containing function value', {
'/a/b': {
on: function() {
shared.fired.push(location.hash);
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['#/a/b']);
this.finish();
});
});
createTest('Single nested route with on member containing array of function values', {
'/a': {
'/b': {
on: [function() { shared.fired.push(location.hash); },
function() { shared.fired.push(location.hash); }]
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['#/a/b', '#/a/b']);
this.finish();
});
});
createTest('method should only fire once on the route.', {
'/a': {
'/b': {
once: function() {
shared.fired++;
}
}
}
}, function() {
shared.fired = 0;
this.navigate('/a/b', function() {
this.navigate('/a/b', function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired, 1);
this.finish();
});
});
});
});
createTest('method should only fire once on the route, multiple nesting.', {
'/a': {
on: function() { shared.fired++; },
once: function() { shared.fired++; }
},
'/b': {
on: function() { shared.fired++; },
once: function() { shared.fired++; }
}
}, function() {
shared.fired = 0;
this.navigate('/a', function() {
this.navigate('/b', function() {
this.navigate('/a', function() {
this.navigate('/b', function() {
deepEqual(shared.fired, 6);
this.finish();
});
});
});
});
});
createTest('overlapping routes with tokens.', {
'/a/:b/c' : function() {
shared.fired.push(location.hash);
},
'/a/:b/c/:d' : function() {
shared.fired.push(location.hash);
}
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
this.navigate('/a/b/c/d', function() {
deepEqual(shared.fired, ['#/a/b/c', '#/a/b/c/d']);
this.finish();
});
});
});
// // //
// // // Recursion features
// // // ----------------------------------------------------------
createTest('Nested routes with no recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['c']);
this.finish();
});
});
createTest('Nested routes with backward recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'backward'
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['c', 'b', 'a']);
this.finish();
});
});
createTest('Breaking out of nested routes with backward recursion', {
'/a': {
'/:b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
return false;
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'backward'
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['c', 'b']);
this.finish();
});
});
createTest('Nested routes with forward recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'forward'
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['a', 'b', 'c']);
this.finish();
});
});
createTest('Nested routes with forward recursion, single route with an after event.', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
},
after: function() {
shared.fired.push('c-after');
}
},
on: function b() {
shared.fired.push('b');
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'forward'
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
this.navigate('/a/b', function() {
deepEqual(shared.fired, ['a', 'b', 'c', 'c-after', 'a', 'b']);
this.finish();
});
});
});
createTest('Breaking out of nested routes with forward recursion', {
'/a': {
'/b': {
'/c': {
on: function c() {
shared.fired.push('c');
}
},
on: function b() {
shared.fired.push('b');
return false;
}
},
on: function a() {
shared.fired.push('a');
}
}
}, {
recurse: 'forward'
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
deepEqual(shared.fired, ['a', 'b']);
this.finish();
});
});
//
// ABOVE IS WORKING
//
// //
// // Special Events
// // ----------------------------------------------------------
createTest('All global event should fire after every route', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
'/c': {
on: function a() {
shared.fired.push('a');
}
}
},
'/d': {
'/:e': {
on: function a() {
shared.fired.push('a');
}
}
}
}, {
after: function() {
shared.fired.push('b');
}
}, function() {
shared.fired = [];
this.navigate('/a', function() {
this.navigate('/b/c', function() {
this.navigate('/d/e', function() {
deepEqual(shared.fired, ['a', 'b', 'a', 'b', 'a']);
this.finish();
});
});
});
});
createTest('Not found.', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
on: function a() {
shared.fired.push('b');
}
}
}, {
notfound: function() {
shared.fired.push('notfound');
}
}, function() {
shared.fired = [];
this.navigate('/c', function() {
this.navigate('/d', function() {
deepEqual(shared.fired, ['notfound', 'notfound']);
this.finish();
});
});
});
createTest('On all.', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
on: function a() {
shared.fired.push('b');
}
}
}, {
on: function() {
shared.fired.push('c');
}
}, function() {
shared.fired = [];
this.navigate('/a', function() {
this.navigate('/b', function() {
deepEqual(shared.fired, ['a', 'c', 'b', 'c']);
this.finish();
});
});
});
createTest('After all.', {
'/a': {
on: function a() {
shared.fired.push('a');
}
},
'/b': {
on: function a() {
shared.fired.push('b');
}
}
}, {
after: function() {
shared.fired.push('c');
}
}, function() {
shared.fired = [];
this.navigate('/a', function() {
this.navigate('/b', function() {
deepEqual(shared.fired, ['a', 'c', 'b']);
this.finish();
});
});
});
createTest('resource object.', {
'/a': {
'/b/:c': {
on: 'f1'
},
on: 'f2'
},
'/d': {
on: ['f1', 'f2']
}
},
{
resource: {
f1: function (name){
shared.fired.push("f1-" + name);
},
f2: function (name){
shared.fired.push("f2");
}
}
}, function() {
shared.fired = [];
this.navigate('/a/b/c', function() {
this.navigate('/d', function() {
deepEqual(shared.fired, ['f1-c', 'f1-undefined', 'f2']);
this.finish();
});
});
});
createTest('argument matching should be case agnostic', {
'/fooBar/:name': {
on: function(name) {
shared.fired.push("fooBar-" + name);
}
}
}, function() {
shared.fired = [];
this.navigate('/fooBar/tesTing', function() {
deepEqual(shared.fired, ['fooBar-tesTing']);
this.finish();
});
});
createTest('sanity test', {
'/is/:this/:sane': {
on: function(a, b) {
shared.fired.push('yes ' + a + ' is ' + b);
}
},
'/': {
on: function() {
shared.fired.push('is there sanity?');
}
}
}, function() {
shared.fired = [];
this.navigate('/is/there/sanity', function() {
deepEqual(shared.fired, ['yes there is sanity']);
this.finish();
});
});
createTest('`/` route should be navigable from the routing table', {
'/': {
on: function root() {
shared.fired.push('/');
}
},
'/:username': {
on: function afunc(username) {
shared.fired.push('/' + username);
}
}
}, function() {
shared.fired = [];
this.navigate('/', function root() {
deepEqual(shared.fired, ['/']);
this.finish();
});
});
createTest('`/` route should not override a `/:token` route', {
'/': {
on: function root() {
shared.fired.push('/');
}
},
'/:username': {
on: function afunc(username) {
shared.fired.push('/' + username);
}
}
}, function() {
shared.fired = [];
this.navigate('/a', function afunc() {
deepEqual(shared.fired, ['/a']);
this.finish();
});
});
createTest('should accept the root as a token.', {
'/:a': {
on: function root() {
shared.fired.push(location.hash);
}
}
}, function() {
shared.fired = [];
this.navigate('/a', function root() {
deepEqual(shared.fired, ['#/a']);
this.finish();
});
});
createTest('routes should allow wildcards.', {
'/:a/b*d': {
on: function() {
shared.fired.push(location.hash);
}
}
}, function() {
shared.fired = [];
this.navigate('/a/bcd', function root() {
deepEqual(shared.fired, ['#/a/bcd']);
this.finish();
});
});
createTest('functions should have |this| context of the router instance.', {
'/': {
on: function root() {
shared.fired.push(!!this.routes);
}
}
}, function() {
shared.fired = [];
this.navigate('/', function root() {
deepEqual(shared.fired, [true]);
this.finish();
});
});
createTest('setRoute with a single parameter should change location correctly', {
'/bonk': {
on: function() {
shared.fired.push(window.location.hash);
}
}
}, function() {
var self = this;
shared.fired = [];
this.router.setRoute('/bonk');
setTimeout(function() {
deepEqual(shared.fired, ['#/bonk']);
self.finish();
}, 14)
});
createTest('route should accept _ and . within parameters', {
'/:a': {
on: function root() {
shared.fired.push(location.hash);
}
}
}, function() {
shared.fired = [];
this.navigate('/a_complex_route.co.uk', function root() {
deepEqual(shared.fired, ['#/a_complex_route.co.uk']);
this.finish();
});
});
createTest('initializing with a default route should only result in one route handling', {
'/': {
on: function root() {
if (!shared.init){
shared.init = 0;
}
shared.init++;
}
},
'/test': {
on: function root() {
if (!shared.test){
shared.test = 0;
}
shared.test++;
}
}
}, function() {
this.navigate('/test', function root() {
equal(shared.init, 1);
equal(shared.test, 1);
this.finish();
});
},
null,
'/');
createTest('changing the hash twice should call each route once', {
'/hash1': {
on: function root() {
shared.fired.push('hash1');
}
},
'/hash2': {
on: function root() {
shared.fired.push('hash2');
}
}
}, function() {
shared.fired = [];
this.navigate('/hash1', function(){});
this.navigate('/hash2', function(){
deepEqual(shared.fired, ['hash1', 'hash2']);
this.finish();
});
}
);
/*
* dispatch-test.js: Tests for the core dispatch method.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/cli/dispatch').addBatch({
"An instance of director.cli.Router": {
topic: function () {
var router = new director.cli.Router(),
that = this;
that.matched = {};
that.matched['users'] = [];
that.matched['apps'] = []
router.on('users create', function () {
that.matched['users'].push('on users create');
});
router.on(/apps (\w+\s\w+)/, function () {
assert.equal(arguments.length, 1);
that.matched['apps'].push('on apps (\\w+\\s\\w+)');
});
return router;
},
"should have the correct routing table": function (router) {
assert.isObject(router.routes.users);
assert.isObject(router.routes.users.create);
},
"the dispatch() method": {
"users create": function (router) {
assert.isTrue(router.dispatch('on', 'users create'));
assert.equal(this.matched.users[0], 'on users create');
},
"apps foo bar": function (router) {
assert.isTrue(router.dispatch('on', 'apps foo bar'));
assert.equal(this.matched['apps'][0], 'on apps (\\w+\\s\\w+)');
},
"not here": function (router) {
assert.isFalse(router.dispatch('on', 'not here'));
},
"still not here": function (router) {
assert.isFalse(router.dispatch('on', 'still not here'));
}
}
}
}).export(module);
/*
* mount-test.js: Tests for the core mount method.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/cli/path').addBatch({
"An instance of director.cli.Router with routes": {
topic: new director.cli.Router({
'apps': function () {
console.log('apps');
},
' users': function () {
console.log('users');
}
}),
"should create the correct nested routing table": function (router) {
assert.isObject(router.routes.apps);
assert.isFunction(router.routes.apps.on);
assert.isObject(router.routes.users);
assert.isFunction(router.routes.users.on);
}
}
}).export(module);
/*
* dispatch-test.js: Tests for the core dispatch method.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/cli/path').addBatch({
"An instance of director.cli.Router": {
topic: new director.cli.Router(),
"the path() method": {
"should create the correct nested routing table": function (router) {
function noop () {}
router.path(/apps/, function () {
router.path(/foo/, function () {
router.on(/bar/, noop);
});
router.on(/list/, noop);
});
router.on(/users/, noop);
assert.isObject(router.routes.apps);
assert.isFunction(router.routes.apps.list.on);
assert.isObject(router.routes.apps.foo);
assert.isFunction(router.routes.apps.foo.bar.on);
assert.isFunction(router.routes.users.on);
}
}
}
}).export(module);
/*
* dispatch-test.js: Tests for the core dispatch method.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/core/dispatch').addBatch({
"An instance of director.Router": {
topic: function () {
var that = this;
that.matched = {};
that.matched['/'] = [];
that.matched['foo'] = [];
that.matched['f*'] = []
var router = new director.Router({
'/': {
before: function () { that.matched['/'].push('before /') },
on: function () { that.matched['/'].push('on /') },
after: function () { that.matched['/'].push('after /') }
},
'/foo': {
before: function () { that.matched.foo.push('before foo') },
on: function () { that.matched.foo.push('on foo') },
after: function () { that.matched.foo.push('after foo') },
'/bar': {
before: function () { that.matched.foo.push('before foo bar') },
on: function () { that.matched.foo.push('foo bar') },
after: function () { that.matched.foo.push('after foo bar') },
'/buzz': function () { that.matched.foo.push('foo bar buzz') }
}
},
'/f*': {
'/barbie': function () { that.matched['f*'].push('f* barbie') }
}
});
router.configure({
recurse: 'backward'
});
return router;
},
"should have the correct routing table": function (router) {
assert.isObject(router.routes.foo);
assert.isObject(router.routes.foo.bar);
assert.isObject(router.routes.foo.bar.buzz);
assert.isFunction(router.routes.foo.bar.buzz.on);
},
"the dispatch() method": {
"/": function (router) {
assert.isTrue(router.dispatch('on', '/'));
assert.isTrue(router.dispatch('on', '/'));
assert.equal(this.matched['/'][0], 'before /');
assert.equal(this.matched['/'][1], 'on /');
assert.equal(this.matched['/'][2], 'after /');
},
"/foo/bar/buzz": function (router) {
assert.isTrue(router.dispatch('on', '/foo/bar/buzz'));
assert.equal(this.matched.foo[0], 'foo bar buzz');
assert.equal(this.matched.foo[1], 'before foo bar');
assert.equal(this.matched.foo[2], 'foo bar');
assert.equal(this.matched.foo[3], 'before foo');
assert.equal(this.matched.foo[4], 'on foo');
},
"/foo/barbie": function (router) {
assert.isTrue(router.dispatch('on', '/foo/barbie'));
assert.equal(this.matched['f*'][0], 'f* barbie');
},
"/foo/barbie/": function (router) {
assert.isFalse(router.dispatch('on', '/foo/barbie/'));
},
"/foo/BAD": function (router) {
assert.isFalse(router.dispatch('on', '/foo/BAD'));
},
"/bar/bar": function (router) {
assert.isFalse(router.dispatch('on', '/bar/bar'));
},
"with the strict option disabled": {
topic: function (router) {
return router.configure({
recurse: 'backward',
strict: false
});
},
"should have the proper configuration set": function (router) {
assert.isFalse(router.strict);
},
"/foo/barbie/": function (router) {
assert.isTrue(router.dispatch('on', '/foo/barbie/'));
assert.equal(this.matched['f*'][0], 'f* barbie');
},
"/foo/bar/buzz": function (router) {
assert.isTrue(router.dispatch('on', '/foo/bar/buzz'));
assert.equal(this.matched.foo[0], 'foo bar buzz');
assert.equal(this.matched.foo[1], 'before foo bar');
assert.equal(this.matched.foo[2], 'foo bar');
assert.equal(this.matched.foo[3], 'before foo');
assert.equal(this.matched.foo[4], 'on foo');
},
}
}
}
}).export(module);
/*
* insert-test.js: Tests for inserting routes into a normalized routing table.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/core/insert').addBatch({
"An instance of director.Router": {
topic: new director.Router(),
"the insert() method": {
"'on', ['foo', 'bar']": function (router) {
function route () { }
router.insert('on', ['foo', 'bar'], route);
assert.strictEqual(router.routes.foo.bar.on, route);
},
"'on', ['foo', 'bar'] again": function (router) {
function route () { }
router.insert('on', ['foo', 'bar'], route);
assert.isArray(router.routes.foo.bar.on);
assert.strictEqual(router.routes.foo.bar.on.length, 2);
},
"'on', ['foo', 'bar'] a third time": function (router) {
function route () { }
router.insert('on', ['foo', 'bar'], route);
assert.isArray(router.routes.foo.bar.on);
assert.strictEqual(router.routes.foo.bar.on.length, 3);
},
"'get', ['fizz', 'buzz']": function (router) {
function route () { }
router.insert('get', ['fizz', 'buzz'], route);
assert.strictEqual(router.routes.fizz.buzz.get, route);
}
}
}
}).export(module);
\ No newline at end of file
/*
* mount-test.js: Tests for mounting and normalizing routes into a Router instance.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
function assertRoute (fn, path, route) {
if (path.length === 1) {
return assert.strictEqual(route[path.shift()], fn);
}
route = route[path.shift()];
assert.isObject(route);
assertRoute(fn, path, route);
}
vows.describe('director/core/mount').addBatch({
"An instance of director.Router": {
"with no preconfigured params": {
topic: new director.Router(),
"the mount() method": {
"should sanitize the routes correctly": function (router) {
function foobar () { }
function foostar () { }
function foobazzbuzz () { }
function foodog () { }
function root () {}
var fnArray = [foobar, foostar];
router.mount({
'/': {
before: root,
on: root,
after: root,
'/nesting': {
on: foobar,
'/deep': foostar
}
},
'/foo': {
'/bar': foobar,
'/*': foostar,
'/jitsu/then': {
before: foobar
}
},
'/foo/bazz': {
'/buzz': foobazzbuzz
},
'/foo/jitsu': {
'/then': fnArray
},
'/foo/jitsu/then/now': foostar,
'/foo/:dog': foodog
});
assertRoute(root, ['on'], router.routes);
assertRoute(root, ['after'], router.routes);
assertRoute(root, ['before'], router.routes);
assertRoute(foobar, ['nesting', 'on'], router.routes);
assertRoute(foostar, ['nesting', 'deep', 'on'], router.routes);
assertRoute(foobar, [ 'foo', 'bar', 'on'], router.routes);
assertRoute(foostar, ['foo', '([_.()!\\ %@&a-zA-Z0-9-]+)', 'on'], router.routes);
assertRoute(fnArray, ['foo', 'jitsu', 'then', 'on'], router.routes);
assertRoute(foobar, ['foo', 'jitsu', 'then', 'before'], router.routes);
assertRoute(foobazzbuzz, ['foo', 'bazz', 'buzz', 'on'], router.routes);
assertRoute(foostar, ['foo', 'jitsu', 'then', 'now', 'on'], router.routes);
assertRoute(foodog, ['foo', '([._a-zA-Z0-9-]+)', 'on'], router.routes);
},
"should accept string path": function(router) {
function dogs () { }
router.mount({
'/dogs': {
on: dogs
}
},
'/api');
assertRoute(dogs, ['api', 'dogs', 'on'], router.routes);
}
}
},
"with preconfigured params": {
topic: function () {
var router = new director.Router();
router.param('city', '([\\w\\-]+)');
router.param(':country', /([A-Z][A-Za-z]+)/);
router.param(':zip', /([\d]{5})/);
return router;
},
"should sanitize the routes correctly": function (router) {
function usaCityZip () { }
function countryCityZip () { }
router.mount({
'/usa/:city/:zip': usaCityZip,
'/world': {
'/:country': {
'/:city/:zip': countryCityZip
}
}
});
assertRoute(usaCityZip, ['usa', '([\\w\\-]+)', '([\\d]{5})', 'on'], router.routes);
assertRoute(countryCityZip, ['world', '([A-Z][A-Za-z]+)', '([\\w\\-]+)', '([\\d]{5})', 'on'], router.routes);
}
}
}
}).export(module);
/*
* on-test.js: Tests for the on/route method.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/core/insert').addBatch({
"An instance of director.Router": {
topic: new director.Router(),
"the on() method": {
"['foo', 'bar']": function (router) {
function noop () { }
router.on(['foo', 'bar'], noop);
assert.strictEqual(router.routes.foo.on, noop);
assert.strictEqual(router.routes.bar.on, noop);
},
"'baz'": function (router) {
function noop () { }
router.on('baz', noop);
assert.strictEqual(router.routes.baz.on, noop);
},
"'after', 'baz'": function (router) {
function noop () { }
router.on('after', 'boo', noop);
assert.strictEqual(router.routes.boo.after, noop);
}
}
}
}).export(module);
\ No newline at end of file
/*
* path-test.js: Tests for the core `.path()` method.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/core/path').addBatch({
"An instance of director.Router": {
topic: function () {
var that = this;
that.matched = {};
that.matched['foo'] = [];
that.matched['newyork'] = [];
var router = new director.Router({
'/foo': function () { that.matched['foo'].push('foo'); }
});
return router;
},
"the path() method": {
"should create the correct nested routing table": function (router) {
var that = this;
router.path('/regions', function () {
this.on('/:state', function(country) {
that.matched['newyork'].push('new york');
});
});
assert.isFunction(router.routes.foo.on);
assert.isObject(router.routes.regions);
assert.isFunction(router.routes.regions['([._a-zA-Z0-9-]+)'].on);
},
"should dispatch the function correctly": function (router) {
router.dispatch('on', '/regions/newyork')
router.dispatch('on', '/foo');
assert.equal(this.matched['foo'].length, 1);
assert.equal(this.matched['newyork'].length, 1);
assert.equal(this.matched['foo'][0], 'foo');
assert.equal(this.matched['newyork'][0], 'new york');
}
}
}
}).export(module);
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
var callback = function() {
return true;
};
var testRoute = function(route, callback) {
var router = new director.Router();
router.on(route, callback);
return function(value) {
return router.dispatch('on', value);
};
};
vows.describe('director/core/regifyString').addBatch({
'When using "/home(.*)"': {
topic: function() {
return testRoute('/home(.*)', callback);
},
'Should match "/homepage"': function(result) {
assert.isTrue(result('/homepage'));
},
'Should match "/home/page"': function(result) {
assert.isTrue(result('/home/page'));
},
'Should not match "/foo-bar"': function(result) {
assert.isFalse(result('/foo-bar'));
}
},
'When using "/home.*"': {
topic: function() {
return testRoute('/home.*', callback);
},
'Should match "/homepage"': function(result) {
assert.isTrue(result('/homepage'));
},
'Should match "/home/page"': function(result) {
assert.isTrue(result('/home/page'));
},
'Should not match "/foo-bar"': function(result) {
assert.isFalse(result('/foo-bar'));
}
},
'When using "/home(page[0-9])*"': {
topic: function() {
return testRoute('/home(page[0-9])*', callback);
},
'Should match "/home"': function(result) {
assert.isTrue(result('/home'));
},
'Should match "/homepage0", "/homepage1", etc.': function(result) {
for (i = 0; i < 10; i++) {
assert.isTrue(result('/homepage' + i));
}
},
'Should not match "/home_page"': function(result) {
assert.isFalse(result('/home_page'));
},
'Should not match "/home/page"': function(result) {
assert.isFalse(result('/home/page'));
}
},
'When using "/home*"': {
topic: function() {
return testRoute('/home*', callback);
},
'Should match "/homepage"': function(result) {
assert.isTrue(result('/homepage'));
},
'Should match "/home_page"': function(result) {
assert.isTrue(result('/home_page'));
},
'Should match "/home-page"': function(result) {
assert.isTrue(result('/home-page'));
},
'Should not match "/home/page"': function(result) {
assert.isFalse(result('/home/page'));
}
}
}).export(module);
/*
* index.js: Test helpers for director.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var http = require('http');
exports.createServer = function (router) {
return http.createServer(function (req, res) {
router.dispatch(req, res, function (err) {
if (err) {
res.writeHead(404);
res.end();
}
});
});
};
exports.handlers = {
respondWithId: function (id) {
this.res.writeHead(200, { 'Content-Type': 'text/plain' })
this.res.end('hello from (' + id + ')');
},
respondWithData: function () {
this.res.writeHead(200, { 'Content-Type': 'application/json' })
this.res.end(JSON.stringify(this.data));
},
respondWithOk: function () {
return function () {
this.res.writeHead(200);
this.res.end('ok');
};
},
streamBody: function () {
var body = '',
res = this.res;
this.req.on('data', function (chunk) {
body += chunk;
});
this.req.on('end', function () {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(body);
});
}
};
exports.macros = require('./macros');
/*
* macros.js: Test macros for director tests.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
request = require('request');
exports.assertGet = function(port, uri, expected) {
var context = {
topic: function () {
request({ uri: 'http://localhost:' + port + '/' + uri }, this.callback);
}
};
context['should respond with `' + expected + '`'] = function (err, res, body) {
assert.isNull(err);
assert.equal(res.statusCode, 200);
assert.equal(body, expected);
};
return context;
};
exports.assert404 = function (port, uri) {
return {
topic: function () {
request({ uri: 'http://localhost:' + port + '/' + uri }, this.callback);
},
"should respond with 404": function (err, res, body) {
assert.isNull(err);
assert.equal(res.statusCode, 404);
}
};
};
exports.assertPost = function(port, uri, expected) {
return {
topic: function () {
request({
method: 'POST',
uri: 'http://localhost:' + port + '/' + uri,
body: JSON.stringify(expected)
}, this.callback);
},
"should respond with the POST body": function (err, res, body) {
assert.isNull(err);
assert.equal(res.statusCode, 200);
assert.deepEqual(JSON.parse(body), expected);
}
};
};
/*
* accept-test.js: Tests for `content-type`-based routing
*
* (C) 2012, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
apiEasy = require('api-easy'),
director = require('../../../lib/director'),
helpers = require('../helpers'),
macros = helpers.macros,
handlers = helpers.handlers;
var PORT = 9067;
apiEasy.describe('director/http/accept')
.addBatch({
"An instance of `director.http.Router`": {
"with routes set up": {
topic: function () {
var router = new director.http.Router();
router.get('/json', { accept: 'application/json' }, handlers.respondWithOk());
router.get('/txt', { accept: 'text/plain' }, handlers.respondWithOk());
router.get('/both', { accept: ['text/plain', 'application/json'] }, handlers.respondWithOk());
router.get('/regex', { accept: /.+\/x\-.+/ }, handlers.respondWithOk());
router.get('/weird', { accept: 'application/json' }, function () {
this.res.writeHead(400);
this.res.end();
});
router.get('/weird', handlers.respondWithOk());
helpers.createServer(router).listen(PORT, this.callback);
},
"should be created": function (err) {
assert(!err);
}
}
}
})
.use('localhost', PORT)
.discuss('with `content-type: application/json`')
.setHeader('content-type', 'application/json')
.get('/json')
.expect(200)
.get('/txt')
.expect(404)
.get('/both')
.expect(200)
.get('/regex')
.expect(404)
.get('/weird')
.expect(400)
.undiscuss()
.next()
.discuss('with `content-type: text/plain`')
.setHeader('content-type', 'text/plain')
.get('/json')
.expect(404)
.get('/txt')
.expect(200)
.get('/both')
.expect(200)
.get('/regex')
.expect(404)
.get('/weird')
.expect(200)
.undiscuss()
.next()
.discuss('with `content-type: application/x-tar-gz`')
.setHeader('content-type', 'application/x-tar-gz`')
.get('/json')
.get('/json')
.expect(404)
.get('/txt')
.expect(404)
.get('/both')
.expect(404)
.get('/regex')
.expect(200)
.get('/weird')
.expect(200)
.undiscuss()
.export(module);
/*
* attach-test.js: Tests 'router.attach' functionality.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
http = require('http'),
vows = require('vows'),
request = require('request'),
director = require('../../../lib/director'),
helpers = require('../helpers'),
handlers = helpers.handlers,
macros = helpers.macros;
function assertData(uri) {
return macros.assertGet(
9091,
uri,
JSON.stringify([1,2,3])
);
}
vows.describe('director/http/attach').addBatch({
"An instance of director.http.Router": {
"instantiated with a Routing table": {
topic: new director.http.Router({
'/hello': {
get: handlers.respondWithData
}
}),
"should have the correct routes defined": function (router) {
assert.isObject(router.routes.hello);
assert.isFunction(router.routes.hello.get);
},
"when passed to an http.Server instance": {
topic: function (router) {
router.attach(function () {
this.data = [1,2,3];
});
helpers.createServer(router)
.listen(9091, this.callback);
},
"a request to hello": assertData('hello'),
}
}
}
}).export(module);
/*
* before-test.js: Tests for running before methods on HTTP server(s).
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
http = require('http'),
vows = require('vows'),
request = require('request'),
director = require('../../../lib/director'),
helpers = require('../helpers'),
handlers = helpers.handlers,
macros = helpers.macros;
vows.describe('director/http/before').addBatch({
"An instance of director.http.Router": {
"with ad-hoc routes including .before()": {
topic: function () {
var router = new director.http.Router();
router.before('/hello', function () { });
router.after('/hello', function () { });
router.get('/hello', handlers.respondWithId);
return router;
},
"should have the correct routes defined": function (router) {
assert.isObject(router.routes.hello);
assert.isFunction(router.routes.hello.before);
assert.isFunction(router.routes.hello.after);
assert.isFunction(router.routes.hello.get);
}
}
}
}).export(module);
\ No newline at end of file
/*
* http-test.js: Tests for basic HTTP server(s).
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
http = require('http'),
vows = require('vows'),
request = require('request'),
director = require('../../../lib/director'),
helpers = require('../helpers'),
handlers = helpers.handlers,
macros = helpers.macros;
function assertBark(uri) {
return macros.assertGet(
9090,
uri,
'hello from (bark)'
);
}
vows.describe('director/http').addBatch({
"An instance of director.http.Router": {
"instantiated with a Routing table": {
topic: new director.http.Router({
'/hello': {
get: handlers.respondWithId
}
}),
"should have the correct routes defined": function (router) {
assert.isObject(router.routes.hello);
assert.isFunction(router.routes.hello.get);
},
"when passed to an http.Server instance": {
topic: function (router) {
router.get(/foo\/bar\/(\w+)/, handlers.respondWithId);
router.get(/foo\/update\/(\w+)/, handlers.respondWithId);
router.path(/bar\/bazz\//, function () {
this.get(/(\w+)/, handlers.respondWithId);
});
router.get(/\/foo\/wild\/(.*)/, handlers.respondWithId);
router.get(/(\/v2)?\/somepath/, handlers.respondWithId);
helpers.createServer(router)
.listen(9090, this.callback);
},
"a request to foo/bar/bark": assertBark('foo/bar/bark'),
"a request to foo/update/bark": assertBark('foo/update/bark'),
"a request to bar/bazz/bark": assertBark('bar/bazz/bark'),
"a request to foo/bar/bark?test=test": assertBark('foo/bar/bark?test=test'),
"a request to foo/wild/bark": assertBark('foo/wild/bark'),
"a request to foo/%RT": macros.assert404(9090, 'foo/%RT'),
"a request to /v2/somepath": macros.assertGet(
9090,
'/v2/somepath',
'hello from (/v2)'
)
}
}
}
}).export(module);
\ No newline at end of file
/*
* methods-test.js: Tests for HTTP methods.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/http/methods').addBatch({
"When using director": {
"an instance of director.http.Router should have all relevant RFC methods": function () {
var router = new director.http.Router();
director.http.methods.forEach(function (method) {
assert.isFunction(router[method.toLowerCase()]);
});
},
"the path() method": {
topic: new director.http.Router(),
"/resource": {
"should insert nested routes correct": function (router) {
function getResource() {}
function modifyResource() {}
router.path(/\/resource/, function () {
this.get(getResource);
this.put(/\/update/, modifyResource);
this.post(/create/, modifyResource);
});
assert.equal(router.routes.resource.get, getResource);
assert.equal(router.routes.resource.update.put, modifyResource);
assert.equal(router.routes.resource.create.post, modifyResource);
}
}
}
}
}).export(module);
\ No newline at end of file
/*
* responses-test.js: Tests for HTTP responses.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
vows = require('vows'),
director = require('../../../lib/director');
vows.describe('director/http/responses').addBatch({
"When using director.http": {
"it should have the relevant responses defined": function () {
Object.keys(require('../../../lib/director/http/responses')).forEach(function (name) {
assert.isFunction(director.http[name]);
});
}
}
}).export(module);
\ No newline at end of file
/*
* stream-test.js: Tests for streaming HTTP in director.
*
* (C) 2011, Nodejitsu Inc.
* MIT LICENSE
*
*/
var assert = require('assert'),
http = require('http'),
vows = require('vows'),
request = require('request'),
director = require('../../../lib/director'),
helpers = require('../helpers'),
macros = helpers.macros,
handlers = helpers.handlers
vows.describe('director/http/stream').addBatch({
"An instance of director.http.Router": {
"with streaming routes": {
topic: function () {
var router = new director.http.Router();
router.post(/foo\/bar/, { stream: true }, handlers.streamBody);
router.path('/a-path', function () {
this.post({ stream: true }, handlers.streamBody);
});
return router;
},
"when passed to an http.Server instance": {
topic: function (router) {
helpers.createServer(router)
.listen(9092, this.callback);
},
"a POST request to /foo/bar": macros.assertPost(9092, 'foo/bar', {
foo: 'foo',
bar: 'bar'
}),
"a POST request to /a-path": macros.assertPost(9092, 'a-path', {
foo: 'foo',
bar: 'bar'
})
}
}
}
}).export(module);
\ No newline at end of file
// Copyright (c) 2012 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers.
For Docs, License, Tests, and pre-packed downloads, see:
http://www.polymer-project.org/
Many thanks to our contributors:
https://github.com/Polymer/platform/contributors
{
"name": "polymer",
"description": "Leverage the future of the web platform today.",
"homepage": "http://www.polymer-project.org/",
"keywords": [
"util",
"client",
"browser"
],
"author": {
"name": "Polymer Authors",
"email": "polymer-dev@googlegroups.com"
},
"version": "0.0.0",
"main": [
"polymer.min.js"
],
"gitHead": "d40e679c70baa0693fe59433fe928af913338544",
"readme": "Polymer is a new type of library for the web, built on top of Web Components, and designed to leverage the evolving web platform on modern browsers.\n\nFor Docs, License, Tests, and pre-packed downloads, see:\nhttp://www.polymer-project.org/\n\nMany thanks to our contributors:\nhttps://github.com/Polymer/platform/contributors\n",
"readmeFilename": "README.md",
"_id": "polymer@0.0.1",
"commit": "d40e679c70baa0693fe59433fe928af913338544",
"repository": {
"type": "git",
"url": "git://github.com/components/polymer.git"
}
}
\ No newline at end of file
BUILD LOG
---------
Build Time: 2013-06-17T12:21:13
NODEJS INFORMATION
==================
nodejs: v0.10.4
chai: 1.6.1
grunt: 0.4.1
grunt-audit: 0.0.0
grunt-contrib-uglify: 0.2.2
grunt-contrib-yuidoc: 0.4.0
grunt-karma-0.9.1: 0.4.3
karma: 0.9.2
karma-chrome-launcher: 0.0.1
karma-coffee-preprocessor: 0.0.1
karma-crbot-reporter: 0.0.3
karma-firefox-launcher: 0.0.2
karma-jasmine: 0.0.1
karma-mocha: 0.0.1
karma-phantomjs-launcher: 0.0.2
karma-requirejs: 0.0.1
karma-script-launcher: 0.0.1
mocha: 1.10.0
REPO REVISIONS
==============
polymer: 24689e70b7ebb122909d4ed17b53a2b2a455a2e5
platform: 569a80ae565a0ecbe01d3a79128021fdf1caaff7
ShadowDOM: 7a50b20542a559a0ffccb9e3a9330246a1f9fb5f
HTMLImports: 7960a892cd4461333809605c3806ba4da699b1f0
CustomElements: 76bfa07e8bfd5ed75c0c14682a66c40600e6baa0
PointerEvents: 9bab6b80e74fcbbdb4145286aae264ae54175419
PointerGestures: d6328b1e65daf111720f76b70f6d469a91a96335
mdv: aaa18dc0069764ec9f25654c949e4a1071f551f2
BUILD HASHES
============
polymer.min.js: 434cd0b3e8d81699760c75043ab6f7c5676db006
polymer.native.min.js: 4f966c0f59a909dd3ac28068f1164016551ec179
polymer.sandbox.min.js: 7444aba9341f62aeb0d89538ee033c77c16cd0e0
{
"name": "platform",
"repo": "components/platform",
"description": "Leverage the future of the web platform today.",
"homepage": "http://www.polymer-project.org/",
"keywords": [
"util",
"client",
"browser"
],
"author": "Polymer Authors <polymer-dev@googlegroups.com>",
"version": "0.0.1",
"main": "polymer.min.js",
"scripts": [
"polymer.min.js"
]
}
{
"name": "components/platform",
"description": "Leverage the future of the web platform today.",
"keywords": ["util", "client", "browser"],
"type": "component",
"homepage": "http://www.polymer-project.org/",
"support": {
"issues": "https://github.com/Polymer/platform/issues",
"source": "https://github.com/Polymer/platform"
},
"authors": [
{
"name": "Polymer Authors",
"email": "polymer-dev@googlegroups.com"
}
],
"extra": {
"component": {
"name": "polymer",
"scripts": [
"polymer.min.js"
],
"files": [
"src/**",
"platform/**",
"polymer.min.js",
"polymer.min.js.map",
"polymer.native.min.js",
"polymer.native.min.js.map"
],
"shim": {
"exports": "Platform"
}
}
}
}
{
"name": "components-polymer",
"description": "Leverage the future of the web platform today.",
"homepage": "http://www.polymer-project.org/",
"keywords": [
"util",
"client",
"browser"
],
"author": "Polymer Authors <polymer-dev@googlegroups.com>",
"repository": {
"type": "git",
"url": "git://github.com/Polymer/platform.git"
},
"main": "polymer.min.js",
"version": "0.0.1",
"devDependencies": {
"mocha": "*",
"chai": "*",
"grunt": "*",
"grunt-contrib-concat": "*",
"grunt-contrib-uglify": "*",
"grunt-contrib-yuidoc": "~0.4.0",
"grunt-karma-0.9.1": "~0.4.3",
"karma-mocha": "*",
"karma-script-launcher": "*",
"karma-crbot-reporter": "*"
}
}
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "todomvc-common",
"version": "0.1.7",
"gitHead": "42348a8146fe0be847b93cd98664813fbae62be9",
"_id": "todomvc-common@0.1.7",
"readme": "ERROR: No README.md file found!",
"description": "ERROR: No README.md file found!",
"repository": {
"type": "git",
"url": "git://github.com/tastejs/todomvc-common.git"
}
}
\ No newline at end of file
<!doctype html>
<html lang="en" data-framework="polymer">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Polymer • TodoMVC</title>
<link rel="stylesheet" href="app/app.css">
</head>
<body>
<section>
<header>
<h1>todos</h1>
</header>
<td-todos></td-todos>
<footer id="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="http://www.polymer-project.org">The Polymer Authors</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</section>
<!-- Scripts here. Don't remove this ↓ -->
<script src="components/todomvc-common/base.js"></script>
<script src="components/polymer/polymer.min.js"></script>
<link rel="import" href="elements/td-todos.html">
</body>
</html>
<head>
<meta charset="utf-8">
<title>Polymer has graduated</title>
<meta http-equiv="refresh" content="0; url=../../../architecture-examples/polymer/">
</head>
<h1>Polymer has moved out of labs</h1>
<a href="../../../architecture-examples/polymer/">Get to the app</a>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment