diff --git a/examples/route-constraints/app.js b/examples/route-constraints/app.js
new file mode 100644
index 0000000000..758a71df47
--- /dev/null
+++ b/examples/route-constraints/app.js
@@ -0,0 +1,78 @@
+/** @jsx React.DOM */
+var React = require('react');
+var Router = require('../../modules/main');
+var Route = Router.Route;
+var Link = Router.Link;
+
+var NotFound = React.createClass({
+ render : function() { return
{'404 - Not Found'}
; }
+});
+
+var App = React.createClass({
+ mixins : [Router.Constrainable],
+
+ statics : {
+ redirectTo : '404',
+ paramConstraints : {
+ userId : /^\d+$/
+ }
+ },
+
+ render: function() {
+ return (
+
+
+ {this.props.activeRoute}
+
+ );
+ }
+});
+
+var User = React.createClass({
+ mixins : [Router.Constrainable],
+
+ statics : {
+ redirectTo : '404',
+ paramConstraints : {
+ userId : /^\d+$/
+ }
+ },
+
+ render: function() {
+ return (
+
+
User id: {this.props.params.userId}
+
+ {this.props.activeRoute}
+
+ );
+ }
+});
+
+var Task = React.createClass({
+ render: function() {
+ return (
+
+
User id: {this.props.params.userId}
+ Task id: {this.props.params.taskId}
+
+ );
+ }
+});
+
+var routes = (
+
+
+
+
+
+
+);
+
+React.renderComponent(routes, document.body);
diff --git a/examples/route-constraints/index.html b/examples/route-constraints/index.html
new file mode 100644
index 0000000000..1c018b491d
--- /dev/null
+++ b/examples/route-constraints/index.html
@@ -0,0 +1,5 @@
+
+Route Constraints Example
+
+
+
diff --git a/modules/main.js b/modules/main.js
index d8b4c3587b..8935c787c2 100644
--- a/modules/main.js
+++ b/modules/main.js
@@ -1,5 +1,6 @@
exports.Link = require('./components/Link');
exports.Route = require('./components/Route');
+exports.Constrainable = require('./mixins/constrainable');
exports.goBack = require('./helpers/goBack');
exports.replaceWith = require('./helpers/replaceWith');
diff --git a/modules/mixins/constrainable.js b/modules/mixins/constrainable.js
new file mode 100644
index 0000000000..5f69a9462e
--- /dev/null
+++ b/modules/mixins/constrainable.js
@@ -0,0 +1,67 @@
+function isObject(val) {
+ return typeof val === 'object';
+}
+
+function isRegExp(val) {
+ return val instanceof RegExp;
+}
+
+var Constrainable = {
+ statics: {
+ willTransitionTo : function(transition, params) {
+ if (!this.validatePath(transition.path) || !this.validateParams(params)) {
+ transition.redirect(this.redirectTo);
+ }
+ },
+
+ /**
+ * Uses this.pathConstraint (defined in the component's statics) to validate
+ * the current matched path. If this.pathConstraint is not defined, or it is
+ * not a RegExp, then this method will return true (permissive by default).
+ *
+ * @param {string} path The path to validate against this.pathConstraint
+ * @return {bool} Whether the path matches the given constraint
+ */
+ validatePath : function(path) {
+ if (! isRegExp(this.pathConstraint)) {
+ return true;
+ }
+
+ return this.pathConstraint.test(path);
+ },
+
+ /**
+ * Uses this.paramConstraints (defined in the component's statics) to
+ * validate the current path's parameters. If this.paramConstraints is not
+ * defined or is not an object, then this method will return true. If a
+ * constraint is not provided for a particular parameter, it will assume
+ * that anything should match.
+ *
+ * @param {string} params The matched params to validate
+ * @return {bool} Whether the params matche the given constraints
+ */
+ validateParams : function(params) {
+ if (! isObject(this.paramConstraints)) {
+ return true;
+ }
+
+ for (var param in params) {
+ if (! params.hasOwnProperty(param)) {
+ continue;
+ }
+
+ if (! isRegExp(this.paramConstraints[param])) {
+ continue;
+ }
+
+ if (! this.paramConstraints[param].test(params[param])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+};
+
+module.exports = Constrainable;
diff --git a/specs/constrainable.spec.js b/specs/constrainable.spec.js
new file mode 100644
index 0000000000..596253e4f9
--- /dev/null
+++ b/specs/constrainable.spec.js
@@ -0,0 +1,123 @@
+require('./helper');
+var Constrainable = require('../modules/mixins/constrainable');
+
+describe('Constrainable', function() {
+ describe('validatePath', function() {
+ it('returns true when pathConstraint is not set', function () {
+ var constrained = Object.create(Constrainable);
+
+ expect(constrained.statics.validatePath('/abc')).toBe(true);
+ });
+
+ it('returns true when pathConstraint is set and matches', function () {
+ var constrained = Object.create(Constrainable);
+ constrained.statics.pathConstraint = /^\/[A-Za-z]+$/;
+ expect(constrained.statics.validatePath('/abc')).toBe(true);
+ });
+
+ it('returns false when pathConstraint is set and does not match', function () {
+ var constrained = Object.create(Constrainable);
+ constrained.statics.pathConstraint = /^\/[A-Za-z]+$/;
+ expect(constrained.statics.validatePath('/123')).toBe(false);
+ });
+ });
+
+ describe('validateParams', function() {
+ it('returns true when paramConstraints is not set', function () {
+ var constrained = Object.create(Constrainable);
+
+ expect(constrained.statics.validateParams({
+ alpha : 'abc',
+ numeric : '123'
+ })).toBe(true);
+ });
+
+ it('returns true when paramConstraints is set and params match', function () {
+ var constrained = Object.create(Constrainable);
+
+ constrained.statics.paramConstraints = {
+ alpha : /^[A-Za-z]+$/,
+ numeric : /^\d+$/
+ };
+
+ expect(constrained.statics.validateParams({
+ alpha : 'abc',
+ numeric : '123'
+ })).toBe(true);
+ });
+
+ it('returns true when paramConstraints is set and params do not match', function () {
+ var constrained = Object.create(Constrainable);
+
+ constrained.statics.paramConstraints = {
+ alpha : /^[A-Za-z]+$/,
+ numeric : /^\d+$/
+ };
+
+ expect(constrained.statics.validateParams({
+ alpha : '123',
+ numeric : 'abc'
+ })).toBe(false);
+ });
+
+ it('returns correct value when not all params have constraints', function () {
+ var constrained = Object.create(Constrainable);
+
+ constrained.statics.paramConstraints = {
+ alpha : /^[A-Za-z]+$/
+ };
+
+ expect(constrained.statics.validateParams({
+ alpha : 'abc',
+ noConstraint : 'abc123'
+ })).toBe(true);
+ });
+ });
+});
+
+// describe('Path.testConstraints', function () {
+// it('returns false when one or more constraints fail', function () {
+// var params = {
+// id : 123,
+// name : 'Abc'
+// };
+
+// var constraints = {
+// id : /^\d+$/,
+// name : /^[a-z]+$/
+// };
+
+// expect(Path.testConstraints(params, constraints)).toBe(false);
+// });
+
+// it('returns true when constraints pass', function () {
+// var params = {
+// id : 123,
+// name : 'Abc'
+// };
+
+// var constraints = {
+// id : /^\d+$/,
+// name : /^[A-Za-z]+$/
+// };
+
+// expect(Path.testConstraints(params, constraints)).toBe(true);
+// });
+// });
+
+// describe('when a pattern has dynamic segments with constraints', function() {
+// var pattern = '/comments/:id/edit',
+// constraints = {
+// id : /\d+/
+// };
+
+// describe('and the constraints match', function() {
+// expect(Path.extractParams(pattern, '/comments/123/edit', constraints))
+// .toEqual({ id : 123 });
+// });
+
+// describe('and the constraints do not match', function() {
+// expect(Path.extractParams(pattern, '/comments/abc/edit', constraints))
+// .toBe(null);
+// });
+// });
diff --git a/specs/main.js b/specs/main.js
index f84f98c5f9..51033eedb6 100644
--- a/specs/main.js
+++ b/specs/main.js
@@ -5,4 +5,4 @@ require('./Path.spec.js');
require('./Route.spec.js');
require('./RouteStore.spec.js');
require('./URLStore.spec.js');
-
+require('./constrainable.spec.js');