├── .npmignore ├── .gitignore ├── browser.js ├── .travis.yml ├── docs ├── public │ ├── fonts │ │ ├── aller-bold.eot │ │ ├── aller-bold.ttf │ │ ├── aller-bold.woff │ │ ├── aller-light.eot │ │ ├── aller-light.ttf │ │ ├── aller-light.woff │ │ ├── roboto-black.eot │ │ ├── roboto-black.ttf │ │ ├── novecento-bold.eot │ │ ├── novecento-bold.ttf │ │ ├── roboto-black.woff │ │ └── novecento-bold.woff │ └── stylesheets │ │ └── normalize.css ├── helpers.html ├── text.html ├── resource.html ├── docco.css ├── key.html └── router.html ├── index.js ├── Makefile ├── bower.json ├── lib ├── helpers.coffee ├── text.coffee ├── resource.coffee ├── key.coffee ├── router.coffee └── route.coffee ├── LICENSE.txt ├── package.json ├── tests ├── mocha.coffee └── barista.test.coffee └── Readme.md /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | lib/*.js 4 | -------------------------------------------------------------------------------- /browser.js: -------------------------------------------------------------------------------- 1 | window.Barista = require('./lib/router').Router; 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.6 4 | - 0.8 5 | - 0.10 6 | - 0.12 7 | -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/aller-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/aller-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/aller-bold.woff -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/aller-light.eot -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/aller-light.ttf -------------------------------------------------------------------------------- /docs/public/fonts/aller-light.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/aller-light.woff -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/roboto-black.eot -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/roboto-black.ttf -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/novecento-bold.eot -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/novecento-bold.ttf -------------------------------------------------------------------------------- /docs/public/fonts/roboto-black.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/roboto-black.woff -------------------------------------------------------------------------------- /docs/public/fonts/novecento-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kieran/barista/HEAD/docs/public/fonts/novecento-bold.woff -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | try { require('coffee-script/register'); } 2 | catch (e) {} 3 | exports.Router = require('./lib/router').Router; 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | @mocha --reporter list --growl --compilers coffee:coffee-script/register tests/mocha.coffee 3 | 4 | oldtest: 5 | @coffee tests/barista.test.coffee 6 | 7 | debug_test: 8 | @coffee --nodejs --debug-brk tests/barista.test.coffee 9 | 10 | autotest: 11 | @mocha --watch lib/ --reporter list --growl --compilers coffee:coffee-script/register tests/mocha.coffee 12 | 13 | browser: 14 | @browserify -t coffeeify --extension=".coffee" browser.js -o dist/barista.js 15 | 16 | docs: 17 | @docco lib/*.coffee 18 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barista", 3 | "main": "dist/barista.js", 4 | "version": "0.5.3", 5 | "homepage": "https://kieran.github.io/barista", 6 | "authors": [ 7 | "Kieran Huggins " 8 | ], 9 | "description": "URL router & generator, similar to Rails / merb", 10 | "moduleType": [ 11 | "globals" 12 | ], 13 | "keywords": [ 14 | "router" 15 | ], 16 | "license": "MIT", 17 | "ignore": [ 18 | "**/.*", 19 | "node_modules", 20 | "bower_components", 21 | "test", 22 | "tests" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /lib/helpers.coffee: -------------------------------------------------------------------------------- 1 | # Helper methods 2 | # ============================================= 3 | 4 | 5 | # deep object mixer 6 | mixin = ( ret, mixins... )-> 7 | for obj in mixins 8 | for own key, val of obj 9 | if kindof(val) == 'object' 10 | ret[key] = mixin {}, val 11 | else 12 | ret[key] = val 13 | ret 14 | 15 | # better than typeof 16 | kindof = ( o )-> 17 | switch 18 | when typeof o != "object" then typeof o 19 | when o == null then "null" 20 | when o.constructor == Array then "array" 21 | when o.constructor == Date then "date" 22 | when o.constructor == RegExp then "regex" 23 | else "object" 24 | 25 | 26 | module.exports = 27 | kindof: kindof 28 | mixin: mixin 29 | 30 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011 Kieran Huggins 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "barista", 3 | "description": "URL router & generator, similar to Rails / merb", 4 | "version": "0.5.3", 5 | "homepage": "https://kieran.github.io/barista", 6 | "license": "MIT", 7 | "bugs": { 8 | "url": "https://github.com/kieran/barista/issues" 9 | }, 10 | "author": { 11 | "name": "Kieran Huggins", 12 | "email": "kieran@kieran.ca", 13 | "url": "http://kieran.ca" 14 | }, 15 | "main": "./index.js", 16 | "scripts": { 17 | "prepublish": "./node_modules/.bin/coffee -c ./lib/", 18 | "test": "./node_modules/.bin/mocha --reporter list --growl --compilers coffee:coffee-script/register tests/mocha.coffee", 19 | "autotest": "cd lib; ../node_modules/.bin/mocha -w --reporter list --growl --compilers coffee:coffee-script/register ../tests/mocha.coffee" 20 | }, 21 | "engines": { 22 | "node": ">= 0.4.0" 23 | }, 24 | "dependencies": { 25 | "inflection": "*", 26 | "querystring": "0.2.0" 27 | }, 28 | "devDependencies": { 29 | "coffee-script": "1.9.0", 30 | "mocha": "*" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "https://github.com/kieran/barista.git" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/text.coffee: -------------------------------------------------------------------------------- 1 | # new Text( name, optional ) 2 | # ========================== 3 | # those variable thingies 4 | # 5 | # text = new Static('text') 6 | # 7 | exports.Text = 8 | class Text 9 | constructor: ( @text )-> 10 | # no-op 11 | 12 | # text.regexString() 13 | # -------------------- 14 | # makes a regex string of the text path - used by text.test() 15 | # 16 | # returns a string of this path's regex 17 | # 18 | regexString: -> 19 | @text.replace /[.*+?^${}()|[\]\\]/g, "\\$&" 20 | 21 | 22 | # text.test( string ) 23 | # --------------------- 24 | # validates a string using the key's regex pattern 25 | # 26 | # returns true/false if the string matches 27 | # 28 | test: ( string )-> 29 | @text == string 30 | 31 | 32 | # text.url( string ) 33 | # -------------------- 34 | # returns a string for building the url 35 | # if it matches the key conditions 36 | # 37 | url: ( string )-> 38 | if @test(string) then string else false 39 | 40 | 41 | # text.toString() 42 | # ----------------- 43 | # returns a unique id that can be compared to other parts 44 | # 45 | toString: -> 46 | @text 47 | 48 | 49 | @regex = /[\w\-_\\\/\.]+/ 50 | 51 | @parse = ( string )-> 52 | pattern = new RegExp "^#{ @regex.source }" 53 | 54 | name = pattern.exec(string)[0] 55 | 56 | new @ name 57 | -------------------------------------------------------------------------------- /lib/resource.coffee: -------------------------------------------------------------------------------- 1 | { kindof, mixin } = require './helpers' 2 | inflection = require 'inflection' 3 | 4 | exports.Resource = 5 | class Resource 6 | constructor: ( base, controller )-> 7 | 8 | plural = inflection.underscore inflection.pluralize controller 9 | singular = inflection.underscore inflection.singularize controller 10 | 11 | # set up the actual routes for the resource 12 | @routes = [ 13 | base.get("/#{plural}(.:format)") 14 | .to("#{controller}.index") 15 | .as( 16 | if base.collection || base.member 17 | nomenclate base.route_name, plural 18 | else 19 | nomenclate plural 20 | ) 21 | 22 | base.post("/#{plural}(.:format)") 23 | .to("#{controller}.create") 24 | 25 | base.get("/#{plural}/add(.:format)") 26 | .to("#{controller}.add") 27 | .as( 28 | if base.collection || base.member 29 | nomenclate 'add', base.route_name, singular 30 | else 31 | nomenclate 'add', singular 32 | ) 33 | 34 | base.get("/#{plural}/:id(.:format)") 35 | .to("#{controller}.show") 36 | .as( 37 | if base.collection || base.member 38 | nomenclate base.route_name, singular 39 | else 40 | nomenclate singular 41 | ) 42 | 43 | base.get("/#{plural}/:id/edit(.:format)") 44 | .to("#{controller}.edit") 45 | .as( 46 | if base.collection || base.member 47 | nomenclate 'edit', base.route_name, singular 48 | else 49 | nomenclate 'edit', singular 50 | ) 51 | 52 | base.put("/#{plural}/:id(.:format)") 53 | .to("#{controller}.update") 54 | 55 | base.del("/#{plural}/:id(.:format)") 56 | .to("#{controller}.destroy") 57 | ] 58 | 59 | @collection_route = @routes[0] 60 | @member_route = @routes[3] 61 | 62 | @collection_route.collection = true 63 | @member_route.member = true 64 | this 65 | 66 | where: ( conditions )-> 67 | if kindof(conditions) != 'object' 68 | throw new Error 'conditions must be an object' 69 | # recursively apply all conditions to sub-parts 70 | route.where? conditions for route in @routes 71 | this # chainable 72 | 73 | collection: ( cb )-> 74 | @collection_route.nest cb 75 | this # for chaining 76 | 77 | member: ( cb )-> 78 | @member_route.nest cb 79 | this # for chaining 80 | 81 | nest: ( cb )-> 82 | @member_route.nest cb 83 | @collection_route.nest cb 84 | this # for chaining 85 | 86 | 87 | # Helper methods 88 | # ============================================= 89 | 90 | # builds route names 91 | # TODO: clean this up 92 | nomenclate = -> 93 | args = Array::slice.call arguments 94 | args.join('_') unless args.filter((a)->!a?).length 95 | -------------------------------------------------------------------------------- /lib/key.coffee: -------------------------------------------------------------------------------- 1 | { kindof, mixin } = require './helpers' 2 | 3 | # new Key( name, optional ) 4 | # ================================= 5 | # those variable thingies 6 | # 7 | # key = new Key('name') 8 | # key = new Key('name', true) 9 | # 10 | exports.Key = 11 | class Key 12 | regex: /[\w\-\s]+/ 13 | constructor: ( @name, @optional )-> 14 | 15 | # special defaults for controllers & actions, which will always be function-name-safe 16 | if @name == 'controller' || @name == 'action' 17 | @regex = /[a-zA-Z_][\w\-]*/ 18 | 19 | 20 | # key.regexString() 21 | # ----------------- 22 | # makes a regex string of the key - used by key.test() 23 | # 24 | # returns a string of this keys regex 25 | # 26 | regexString: -> 27 | ret = ['('] 28 | ret.push @regex.source 29 | ret.push ')' 30 | ret.push '?' if @optional 31 | ret.join '' 32 | 33 | 34 | # key.test( string ) 35 | # ----------------- 36 | # validates a string using the key's regex pattern 37 | # 38 | # returns true/false if the string matches 39 | # 40 | test: ( string )-> # this regex test passes for null & undefined :-( 41 | new RegExp("^#{@regexString()}$").test string 42 | 43 | 44 | # key.url( string ) 45 | # ----------------- 46 | # returns a string for building the url 47 | # if it matches the key conditions 48 | # 49 | url: ( string )-> 50 | if @test(string) then string else false 51 | 52 | 53 | # key.where( conditions ) 54 | # ----------------------- 55 | # adds conditions that the key must match 56 | # 57 | # returns the key... because it can? 58 | # 59 | where: ( conditions )-> 60 | 61 | condition = conditions[@name] 62 | 63 | if condition instanceof RegExp 64 | @regex = condition # e.g. /\d+/ 65 | 66 | if condition instanceof String 67 | @regex = new RegExp condition # e.g. "\d+" 68 | 69 | # an array of allowed values, e.g. ['stop','play','pause'] 70 | if condition instanceof Array 71 | ret = [] 72 | for c in condition 73 | ret.push c.source if 'regex' == kindof c 74 | ret.push c if 'string' == kindof c 75 | @regex = new RegExp ret.join '|' 76 | 77 | this # chainable 78 | 79 | 80 | # key.toString() 81 | # -------------- 82 | # returns the original key definition 83 | # 84 | toString: -> 85 | ":#{@name}" 86 | 87 | 88 | @regex = /:([a-zA-Z_][\w\-]*)/ 89 | @parse = ( string, optional=false )-> 90 | [ definition, name ] = @regex.exec string 91 | new @ name, optional 92 | 93 | # new Glob( name, optional ) 94 | # ================================= 95 | # globs are just greedy keys 96 | # 97 | # glob = new Glob('name') 98 | # glob = new Glob('name', true) 99 | # 100 | exports.Glob = 101 | class Glob extends Key 102 | regex: /[\w\-\/\s]+?/ # default url-friendly regex 103 | constructor: ( @name, @optional )-> 104 | # special defaults for controllers & actions, which will always be function-name-safe 105 | if @name == 'controller' || @name == 'action' 106 | @regex = /[a-zA-Z_][\w\-]*/ 107 | 108 | # glob.toString() 109 | # --------------- 110 | # returns the original glob definition 111 | # 112 | toString: -> 113 | "*#{@name}" 114 | 115 | @regex = /\*([a-zA-Z_][\w\-]*)/ 116 | -------------------------------------------------------------------------------- /docs/helpers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Helper methods 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 56 | 57 | 135 |
136 | 137 | 138 | -------------------------------------------------------------------------------- /tests/mocha.coffee: -------------------------------------------------------------------------------- 1 | assert = require 'assert' 2 | { Router } = require '../index' 3 | 4 | router = null 5 | 6 | verbs = 7 | get: 'GET' 8 | post: 'POST' 9 | put: 'PUT' 10 | patch: 'PATCH' 11 | del: 'DELETE' 12 | options: 'OPTIONS' 13 | 14 | describe 'Barista', -> 15 | 16 | describe 'new Router', -> 17 | it 'should return a blank Router object', -> 18 | router = new Router 19 | assert.ok router instanceof Router 20 | assert.equal [].length, router.routes.length 21 | 22 | describe 'calling #match', -> 23 | 24 | beforeEach -> 25 | router = new Router 26 | 27 | describe 'with a static route', -> 28 | it 'should not throw an exception', -> 29 | route = router.match '/path/to/thing' 30 | assert.ok route, @fail 31 | 32 | describe 'with a keyed route', -> 33 | it 'should not throw an exception', -> 34 | route = router.match '/:controller/:action/:id' 35 | assert.ok route, @fail 36 | 37 | describe 'with a static route and an optional segment', -> 38 | it 'should not throw an exception', -> 39 | route = router.match '/:controller/:action/:id(.:format)' 40 | assert.ok route, @fail 41 | 42 | describe 'with a static route and multiple optional segments', -> 43 | it 'should not throw an exception', -> 44 | route = router.match '/:controller/:id(/:action)(.:format)' 45 | assert.ok route, @fail 46 | 47 | 48 | badPaths = 49 | number: 5 50 | regexp: /bob/ 51 | object: {} 52 | array: [] 53 | date: new Date 54 | # string: 'sadsa' 55 | 56 | for key, val of badPaths # BAD BAD BAD, callbacks are referring to the last item set in the outer scope 57 | do (key,val)-> 58 | it "should throw an error when called with a #{key} for the URL", -> 59 | assert.throws -> 60 | router.match val 61 | , /path must be a string/ 62 | , @fail 63 | 64 | 65 | describe '#match with HTTP verbs', -> 66 | 67 | beforeEach -> 68 | router = new Router 69 | 70 | for method, verb of verbs 71 | do (method, verb)-> 72 | describe verb, -> 73 | it 'should not throw an exception', -> 74 | route = router.match '/:controller/:action', verb 75 | assert.ok route, @fail 76 | 77 | describe 'WTF', -> 78 | it 'should throw an exception', -> 79 | assert.throws -> 80 | route = router.match '/:controller/:action', 'WTF' 81 | 82 | 83 | describe 'convenience methods', -> 84 | 85 | beforeEach -> 86 | router = new Router 87 | 88 | for method, verb of verbs 89 | do (method, verb)-> 90 | 91 | describe "##{method}", -> 92 | it "should not throw an exception", -> 93 | route = router[method] '/path/to/thing' 94 | assert.ok route 95 | 96 | it "should be equivalent to #match with #{verb}", -> 97 | one = router[method] '/path/to/thing' 98 | two = router.match '/path/to/thing', verb 99 | assert.equal JSON.stringify(one), JSON.stringify two 100 | 101 | it 'should NOT be equivalent to a generic match', -> 102 | one = router[method] '/path/to/thing' 103 | two = router.match '/path/to/thing' 104 | assert.notEqual JSON.stringify(one), JSON.stringify two 105 | 106 | it 'should NOT be equivalent to a false match', -> 107 | one = router[method] '/path/to/thing' 108 | two = router.match '/path/to/thing', 'HEAD' 109 | assert.notEqual JSON.stringify(one), JSON.stringify two 110 | 111 | describe '#first', -> 112 | 113 | beforeEach -> 114 | router = new Router 115 | 116 | describe 'with a static route', -> 117 | 118 | beforeEach -> 119 | router.match('/path/to/thing','GET').to('someController.someAction') 120 | 121 | it 'params should match', -> 122 | paramsIn = 123 | method: 'GET' 124 | controller: 'someController' 125 | action: 'someAction' 126 | 127 | paramsOut = router.first '/path/to/thing', 'GET' 128 | 129 | for key, val of paramsIn 130 | assert.equal paramsIn[key], paramsOut[key] 131 | 132 | 133 | 134 | describe 'a where clause with a period', -> 135 | 136 | beforeEach -> 137 | router.match('/sites/:id/edit','GET').to('sites.edit').where(id:/[\w.]+/) 138 | 139 | it 'params should match', -> 140 | paramsIn = 141 | method: 'GET' 142 | controller: 'sites' 143 | action: 'edit' 144 | id: 'site.ru' 145 | 146 | paramsOut = router.first '/sites/site.ru/edit', 'GET' 147 | 148 | for key, val of paramsIn 149 | assert.equal paramsIn[key], paramsOut[key] 150 | 151 | # describe 'a keyed route', -> 152 | # it 'should not throw an exception', -> 153 | # route = router.match '/:controller/:action/:id' 154 | # assert.ok route, @fail 155 | 156 | # describe 'a static route with an optional segment', -> 157 | # it 'should not throw an exception', -> 158 | # route = router.match '/:controller/:action/:id(.:format)' 159 | # assert.ok route, @fail 160 | 161 | # describe 'a static route with multiple optional segments', -> 162 | # it 'should not throw an exception', -> 163 | # route = router.match '/:controller/:id(/:action)(.:format)' 164 | # assert.ok route, @fail 165 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![npm](https://badge.fury.io/js/barista.svg)](http://badge.fury.io/js/barista) [![Bower](https://badge.fury.io/bo/barista.svg)](http://badge.fury.io/bo/barista) [![Build Status](https://travis-ci.org/kieran/barista.svg?branch=master)](https://travis-ci.org/kieran/barista) 2 | 3 | [![NPM](https://nodei.co/npm/barista.png?downloads=true&downloadRank=true)](https://nodei.co/npm/barista/) 4 | 5 | 6 | Barista is a simple URL router for NodeJS. 7 | 8 | In a nutshell 9 | ============= 10 | 11 | ```javascript 12 | router.get( '/:beverage/near/:location(.:format)' ) 13 | .to( 'beverage.byLocation' ) 14 | 15 | router.first( '/coffee/near/90210', 'GET' ) 16 | // -> { controller:'beverage', action:'byLocation', beverage:'coffee', location:90210 } 17 | 18 | router.url({ 19 | controller: 'beverage', 20 | action: 'byLocation', 21 | beverage: 'coffee', 22 | location: 90210, 23 | format: 'json' 24 | }) 25 | // -> '/coffee/near/90210.json' 26 | ``` 27 | 28 | 29 | Getting Barista 30 | =============== 31 | 32 | Install via npm, thusly: 33 | 34 | ```javascript 35 | npm install --save barista 36 | ``` 37 | 38 | Running barista in the browser? Try bower: 39 | 40 | ```javascript 41 | bower install --save barista 42 | ``` 43 | 44 | Using Barista 45 | ------------- 46 | 47 | via node: 48 | ```javascript 49 | var Router = require('barista').Router; 50 | 51 | var router = new Router; 52 | ``` 53 | 54 | in the browser: 55 | ```javascript 56 | var router = new Barista 57 | ``` 58 | 59 | 60 | Adding routes 61 | ------------- 62 | 63 | ### A simple example 64 | 65 | ```javascript 66 | router.match( '/products', 'GET' ) 67 | .to( 'products.index' ) 68 | ``` 69 | 70 | ### Rails-esque variable names 71 | 72 | ```javascript 73 | router.match( '/products/:id', 'GET' ) 74 | .to( 'products.show' ) 75 | 76 | router.match( '/profiles/:username', 'GET' ) 77 | .to( 'users.show' ) 78 | 79 | router.match( '/products/:id(.:format)', 'GET' ) 80 | .to( 'products.show' ) 81 | ``` 82 | 83 | ### Globs (they also capture slashes) 84 | 85 | ```javascript 86 | router.get('/timezones/*tzname') 87 | .to( 'timezones.select' ) 88 | 89 | router.first( '/timezones/America/Toronto', 'GET' ) 90 | // -> { controller:'timezones', action:'select', tzname:'America/Toronto' } 91 | 92 | 93 | router.match( '/*path(.:format)' ) // a "catch-all" route: 94 | .to( 'errors.notFound' ) 95 | 96 | router.first( '/somewhere/that/four-oh-fours.json', 'GET' ) 97 | // -> { controller:'errors', action:'notFound', path:'somewhere/that/four-oh-fours', format:'json' } 98 | ``` 99 | 100 | ### Match conditions 101 | 102 | ```javascript 103 | router.match( '/:beverage/near/:zipcode', 'GET' ) 104 | .to( 'beverage.byZipCode' ) 105 | .where({ 106 | // an array of options 107 | beverage: [ 'coffee', 'tea', 'beer', 'warm_sake' ], 108 | // a regex pattern 109 | zipcode: /^\d{5}(-\d{4})?$/ 110 | }) 111 | 112 | router.match( '/:beverage/near/:location', 'GET' ) 113 | .to( 'beverage.byLocation' ) 114 | .where({ 115 | // could be a postal code 116 | // OR a zip code 117 | // OR the word 'me' (geolocation FTW) 118 | location: [ /^\d{5}(-\d{4})?$/, /^[ABCEGHJKLMNPRSTVXY]{1}\d{1}[A-Z]{1} *\d{1}[A-Z]{1}\d{1}$/, 'me' ] 119 | }) 120 | ``` 121 | 122 | ### Convenience methods 123 | 124 | ```javascript 125 | router.get( '/products/:id(.:format)' ) 126 | .to( 'products.show' ) 127 | 128 | router.put( '/products/:id(.:format)' ) 129 | .to( 'products.update' ) 130 | 131 | router.post( '/products' ) 132 | .to( 'products.create' ) 133 | 134 | router.del( '/products' ) 135 | .to( 'products.destroy' ) 136 | 137 | router.options( '/products' ) 138 | .to( 'products.options' ) 139 | ``` 140 | 141 | ### REST Resources 142 | 143 | ```javascript 144 | router.resource( 'products' ) 145 | ``` 146 | 147 | is equivalent to: 148 | 149 | ```javascript 150 | router.get( '/products(.:format)' ) 151 | .to( 'products.index' ) 152 | 153 | router.get( '/products/add(.:format)' ) 154 | .to( 'products.add' ) 155 | 156 | router.get( '/products/:id(.:format)' ) 157 | .to('products.show' ) 158 | 159 | router.get('/products/:id/edit(.:format)' ) 160 | .to( 'products.edit' ) 161 | 162 | router.post('/products(.:format)' ) 163 | .to( 'products.create' ) 164 | 165 | router.put('/products/:id(.:format)' ) 166 | .to( 'products.update' ) 167 | 168 | router.del('/products/:id(.:format)' ) 169 | .to( 'products.destroy' ) 170 | ``` 171 | 172 | Removing Routes 173 | ------------------------ 174 | 175 | In some cases, you will need to remove routes on a running router. The `router.remove( name )` method will work for this, but requires 176 | use of the otherwise unused `route.name( name )` method. 177 | 178 | ### Adding a name (currently only used with this functionality) 179 | 180 | ```javascript 181 | router.match( '/products/:id', 'GET' ) 182 | .to( 'products.show' ) 183 | .name('products_show') 184 | ``` 185 | 186 | ### Removing a named route 187 | 188 | ```javascript 189 | 190 | router.remove('products_show') 191 | 192 | ``` 193 | 194 | Resolution & dispatching 195 | ------------------------ 196 | 197 | The `router.first( url, method [, callback] )` method can be used in two ways: 198 | 199 | ```javascript 200 | var params = router.first( '/products/15', 'GET' ) 201 | ``` 202 | 203 | OR 204 | 205 | ```javascript 206 | router.first( '/products/15', 'GET', function( err, params ){ 207 | if (err) { 208 | // couldn't find match 209 | } 210 | // dispatch the request or something 211 | }) 212 | ``` 213 | 214 | You can get all the matching routes like so: 215 | 216 | ```javascript 217 | var params = router.all( '/products/15', 'GET' ) 218 | 219 | //=> [params, params, params....] 220 | ``` 221 | 222 | Route generation 223 | ---------------- 224 | 225 | Pass in a params hash, get back a tasty string: 226 | 227 | ```javascript 228 | router.url( { 229 | controller: 'products', 230 | action: 'show', 231 | id: 5 232 | } ) 233 | //=> '/products/5' 234 | 235 | router.url( { 236 | controller: 'products', 237 | action: 'show', 238 | id: 5, 239 | format: 'json' 240 | } ) 241 | //=> '/products/5.json' 242 | ``` 243 | 244 | Set the optional second parameter to `true` if you want 245 | extra params appended as a query string: 246 | 247 | ```javascript 248 | router.url({ 249 | controller: 'products', 250 | action: 'show', 251 | id: 5, 252 | format: 'json', 253 | love: 'cheese' 254 | }, true ) 255 | //=> '/products/5.json?love=cheese' 256 | ``` 257 | 258 | 259 | TODOs 260 | ----- 261 | - Add namespace support 262 | - Better support for named routes 263 | - Customizable resources 264 | 265 | 266 | Things I forgot... 267 | ------------------ 268 | ...might be in the `/docs` folder... 269 | 270 | ...or might not exist at all. 271 | 272 | 273 | It's broken! 274 | ------------ 275 | Shit happens. 276 | 277 | Write a test that fails and add it to the tests folder, 278 | then create an issue! 279 | 280 | Patches welcome :-) 281 | 282 | 283 | Who are you? 284 | ------------ 285 | I'm [Kieran Huggins](mailto:kieran@kieran.ca) in Toronto, Canada. 286 | -------------------------------------------------------------------------------- /lib/router.coffee: -------------------------------------------------------------------------------- 1 | { Route } = require './route' 2 | { Resource } = require './resource' 3 | qstring = require 'querystring' 4 | 5 | exports.Router = 6 | class Router 7 | constructor: -> 8 | @methods = [ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS' ] 9 | @routes = [] 10 | 11 | # router.match( path [, method] ) 12 | # ------------------------------- 13 | # 14 | # router.match('/:controller/:action(/:id)(.:format)', 'GET') 15 | # .to(......) 16 | # 17 | # path is mandatory (duh) 18 | # method is optional, routes without a method will apply in all cases 19 | # 20 | # returns the route (for chaining) 21 | # 22 | match: ( path, method )-> 23 | # upcase the method 24 | if typeof(method) == 'string' 25 | method = method.toUpperCase() 26 | 27 | # upcase the method 28 | if method? && method not in @methods 29 | throw new Error "method must be one of: #{ @methods.join ', ' }" 30 | 31 | route = new Route this, path, method 32 | 33 | # ### router.get( path ) 34 | # equivalent to 35 | # 36 | # router.match( path, 'GET' ) 37 | # 38 | get: ( path )-> 39 | @match path, 'GET' 40 | 41 | # ### router.options( path ) 42 | # equivalent to 43 | # 44 | # router.match( path, 'OPTIONS' ) 45 | # 46 | options: ( path )-> 47 | @match path, 'OPTIONS' 48 | 49 | # ### router.put( path ) 50 | # equivalent to 51 | # 52 | # router.match( path, 'PUT' ) 53 | # 54 | put: ( path )-> 55 | @match path, 'PUT' 56 | 57 | # ### router.post( path ) 58 | # equivalent to 59 | # 60 | # router.match( path, 'POST' ) 61 | # 62 | post: ( path )-> 63 | @match path, 'POST' 64 | 65 | # ### router.patch( path ) 66 | # equivalent to 67 | # 68 | # router.match( path, 'PATCH' ) 69 | # 70 | patch: ( path )-> 71 | @match path, 'PATCH' 72 | 73 | # ### router.del( path ) 74 | # equivalent to 75 | # 76 | # router.match( path, 'DEL' ) 77 | # 78 | del: ( path )-> 79 | @match path, 'DELETE' 80 | 81 | # router.resource( controller ) 82 | # ----------------------------- 83 | # generates standard resource routes for a controller name 84 | # 85 | # router.resource('products') 86 | # 87 | # returns a Resource object 88 | # 89 | resource: ( controller )-> 90 | new Resource this, controller 91 | 92 | # // router.first( path, method, callback ) 93 | # ---------------------------- 94 | # find the first route that match the path & method 95 | # 96 | # router.first('/products/5', 'GET') 97 | # => { controller: 'products', action: 'show', id: 5, method: 'GET' } 98 | # 99 | # find & return a params hash from the first route that matches. If there's no match, this will return false 100 | # 101 | # If the options callback function is provided, it will be fired like so: 102 | # 103 | # callback( error, params ) 104 | # 105 | first: ( path, method, cb )-> 106 | params = false 107 | 108 | for route in @routes 109 | # attempt the parse 110 | params = route.parse path, method 111 | 112 | if params 113 | # fire the callback if given 114 | if typeof cb == 'function' 115 | cb undefined, params 116 | # may as well return this 117 | return params 118 | 119 | if typeof cb == 'function' 120 | cb 'No matching routes found' 121 | false 122 | 123 | 124 | # router.all( path [, method] ) 125 | # -------------------------- 126 | # find & return a params hash from ALL routes that match 127 | # 128 | # router.all( '/products/5' ) 129 | # 130 | # => [ 131 | # { controller: 'products', action: 'show', id: 5, method: 'GET' }, 132 | # { controller: 'products', action: 'update', id: 5, method: 'PUT' }, 133 | # { controller: 'products', action: 'destroy', id: 5, method: 'DELETE' }, 134 | # ] 135 | # 136 | # if there ares no matches, returns an empty array 137 | # 138 | all: ( path, method )-> 139 | ret = [] 140 | params = false 141 | 142 | for route in @routes 143 | params = route.parse.apply route, arguments 144 | if params 145 | ret.push params 146 | ret 147 | 148 | 149 | 150 | # // router.url( params[, add_querystring=false] ) 151 | # -------------------------------------------- 152 | # generates a URL from a params hash 153 | # 154 | # router.url( { 155 | # controller: 'products', 156 | # action: 'show', 157 | # id: 5 158 | # } ) 159 | # => '/products/5' 160 | # 161 | # router.url( { 162 | # controller: 'products', 163 | # action: 'show', 164 | # id: 5, 165 | # format: 'json' 166 | # } ) 167 | # => '/products/5.json' 168 | # 169 | # router.url({ 170 | # controller: 'products', 171 | # action: 'show', 172 | # id: 5, 173 | # format: 'json', 174 | # love: 'cheese' 175 | # }, true ) 176 | # => '/products/5.json?love=cheese' 177 | # 178 | # returns false if there are no suitable routes 179 | # 180 | url: ( params, add_querystring )-> 181 | url = false 182 | 183 | # iterate through the existing routes until a suitable match is found 184 | for route in @routes 185 | # do the controller & acton match? 186 | continue if route.params.controller? && route.params.controller != params.controller 187 | continue if route.params.action? && route.params.action != params.action 188 | 189 | break if url = route.stringify params 190 | 191 | return false unless url # no love? return false 192 | qs = qstring.stringify url[1] # build the possibly empty query string 193 | 194 | if add_querystring && qs.length > 0 195 | return url[0] + '?' + qs # if there is a query string... 196 | 197 | url[0] # just return the url 198 | 199 | 200 | # router.remove( name ) 201 | # ------------------------ 202 | # 203 | # Removes previously created routes by name 204 | # 205 | # The route must be a named route, and the name is passed in. 206 | # 207 | # returns: Nothing 208 | # 209 | remove: ( name )-> 210 | @routes = (route for route in @routes when route.route_name != name) 211 | 212 | # router.defer( testfn() ) 213 | # ------------------------ 214 | # 215 | # router.defer( test( path, method ) ) 216 | # 217 | # test should be a function that examines non-standard URLs 218 | # 219 | # path and method will be passed in - expects a params hash back OR false on a non-match 220 | # 221 | # returns: DeferredRoute (for... reference? I dunno.) 222 | # 223 | # **THIS IS CURRENTLY COMPLETELY UNTESTED. IT MIGHT NOT EVEN WORK. SERIOUSLY.** 224 | # 225 | defer: ( fn )-> 226 | if typeof(fn) != 'function' 227 | throw new Error 'Router.defer requires a function as the only argument' 228 | 229 | route = new Route this, 'deferred' 230 | route.parse = fn # add the custom parser 231 | delete route.test # = function(){return false}; 232 | delete route.stringify # = function(){ throw new Error('Deferred routes are NOT generatable')}; 233 | @routes.push route 234 | route 235 | 236 | # router.toString 237 | # --------------- 238 | # 239 | # renders a textual description of the router for inpection 240 | # 241 | toString: -> 242 | ( route.toString() for route in @routes ).join '\n' 243 | -------------------------------------------------------------------------------- /docs/public/stylesheets/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v2.0.1 | MIT License | git.io/normalize */ 2 | 3 | /* ========================================================================== 4 | HTML5 display definitions 5 | ========================================================================== */ 6 | 7 | /* 8 | * Corrects `block` display not defined in IE 8/9. 9 | */ 10 | 11 | article, 12 | aside, 13 | details, 14 | figcaption, 15 | figure, 16 | footer, 17 | header, 18 | hgroup, 19 | nav, 20 | section, 21 | summary { 22 | display: block; 23 | } 24 | 25 | /* 26 | * Corrects `inline-block` display not defined in IE 8/9. 27 | */ 28 | 29 | audio, 30 | canvas, 31 | video { 32 | display: inline-block; 33 | } 34 | 35 | /* 36 | * Prevents modern browsers from displaying `audio` without controls. 37 | * Remove excess height in iOS 5 devices. 38 | */ 39 | 40 | audio:not([controls]) { 41 | display: none; 42 | height: 0; 43 | } 44 | 45 | /* 46 | * Addresses styling for `hidden` attribute not present in IE 8/9. 47 | */ 48 | 49 | [hidden] { 50 | display: none; 51 | } 52 | 53 | /* ========================================================================== 54 | Base 55 | ========================================================================== */ 56 | 57 | /* 58 | * 1. Sets default font family to sans-serif. 59 | * 2. Prevents iOS text size adjust after orientation change, without disabling 60 | * user zoom. 61 | */ 62 | 63 | html { 64 | font-family: sans-serif; /* 1 */ 65 | -webkit-text-size-adjust: 100%; /* 2 */ 66 | -ms-text-size-adjust: 100%; /* 2 */ 67 | } 68 | 69 | /* 70 | * Removes default margin. 71 | */ 72 | 73 | body { 74 | margin: 0; 75 | } 76 | 77 | /* ========================================================================== 78 | Links 79 | ========================================================================== */ 80 | 81 | /* 82 | * Addresses `outline` inconsistency between Chrome and other browsers. 83 | */ 84 | 85 | a:focus { 86 | outline: thin dotted; 87 | } 88 | 89 | /* 90 | * Improves readability when focused and also mouse hovered in all browsers. 91 | */ 92 | 93 | a:active, 94 | a:hover { 95 | outline: 0; 96 | } 97 | 98 | /* ========================================================================== 99 | Typography 100 | ========================================================================== */ 101 | 102 | /* 103 | * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, 104 | * Safari 5, and Chrome. 105 | */ 106 | 107 | h1 { 108 | font-size: 2em; 109 | } 110 | 111 | /* 112 | * Addresses styling not present in IE 8/9, Safari 5, and Chrome. 113 | */ 114 | 115 | abbr[title] { 116 | border-bottom: 1px dotted; 117 | } 118 | 119 | /* 120 | * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. 121 | */ 122 | 123 | b, 124 | strong { 125 | font-weight: bold; 126 | } 127 | 128 | /* 129 | * Addresses styling not present in Safari 5 and Chrome. 130 | */ 131 | 132 | dfn { 133 | font-style: italic; 134 | } 135 | 136 | /* 137 | * Addresses styling not present in IE 8/9. 138 | */ 139 | 140 | mark { 141 | background: #ff0; 142 | color: #000; 143 | } 144 | 145 | 146 | /* 147 | * Corrects font family set oddly in Safari 5 and Chrome. 148 | */ 149 | 150 | code, 151 | kbd, 152 | pre, 153 | samp { 154 | font-family: monospace, serif; 155 | font-size: 1em; 156 | } 157 | 158 | /* 159 | * Improves readability of pre-formatted text in all browsers. 160 | */ 161 | 162 | pre { 163 | white-space: pre; 164 | white-space: pre-wrap; 165 | word-wrap: break-word; 166 | } 167 | 168 | /* 169 | * Sets consistent quote types. 170 | */ 171 | 172 | q { 173 | quotes: "\201C" "\201D" "\2018" "\2019"; 174 | } 175 | 176 | /* 177 | * Addresses inconsistent and variable font size in all browsers. 178 | */ 179 | 180 | small { 181 | font-size: 80%; 182 | } 183 | 184 | /* 185 | * Prevents `sub` and `sup` affecting `line-height` in all browsers. 186 | */ 187 | 188 | sub, 189 | sup { 190 | font-size: 75%; 191 | line-height: 0; 192 | position: relative; 193 | vertical-align: baseline; 194 | } 195 | 196 | sup { 197 | top: -0.5em; 198 | } 199 | 200 | sub { 201 | bottom: -0.25em; 202 | } 203 | 204 | /* ========================================================================== 205 | Embedded content 206 | ========================================================================== */ 207 | 208 | /* 209 | * Removes border when inside `a` element in IE 8/9. 210 | */ 211 | 212 | img { 213 | border: 0; 214 | } 215 | 216 | /* 217 | * Corrects overflow displayed oddly in IE 9. 218 | */ 219 | 220 | svg:not(:root) { 221 | overflow: hidden; 222 | } 223 | 224 | /* ========================================================================== 225 | Figures 226 | ========================================================================== */ 227 | 228 | /* 229 | * Addresses margin not present in IE 8/9 and Safari 5. 230 | */ 231 | 232 | figure { 233 | margin: 0; 234 | } 235 | 236 | /* ========================================================================== 237 | Forms 238 | ========================================================================== */ 239 | 240 | /* 241 | * Define consistent border, margin, and padding. 242 | */ 243 | 244 | fieldset { 245 | border: 1px solid #c0c0c0; 246 | margin: 0 2px; 247 | padding: 0.35em 0.625em 0.75em; 248 | } 249 | 250 | /* 251 | * 1. Corrects color not being inherited in IE 8/9. 252 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 253 | */ 254 | 255 | legend { 256 | border: 0; /* 1 */ 257 | padding: 0; /* 2 */ 258 | } 259 | 260 | /* 261 | * 1. Corrects font family not being inherited in all browsers. 262 | * 2. Corrects font size not being inherited in all browsers. 263 | * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome 264 | */ 265 | 266 | button, 267 | input, 268 | select, 269 | textarea { 270 | font-family: inherit; /* 1 */ 271 | font-size: 100%; /* 2 */ 272 | margin: 0; /* 3 */ 273 | } 274 | 275 | /* 276 | * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in 277 | * the UA stylesheet. 278 | */ 279 | 280 | button, 281 | input { 282 | line-height: normal; 283 | } 284 | 285 | /* 286 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 287 | * and `video` controls. 288 | * 2. Corrects inability to style clickable `input` types in iOS. 289 | * 3. Improves usability and consistency of cursor style between image-type 290 | * `input` and others. 291 | */ 292 | 293 | button, 294 | html input[type="button"], /* 1 */ 295 | input[type="reset"], 296 | input[type="submit"] { 297 | -webkit-appearance: button; /* 2 */ 298 | cursor: pointer; /* 3 */ 299 | } 300 | 301 | /* 302 | * Re-set default cursor for disabled elements. 303 | */ 304 | 305 | button[disabled], 306 | input[disabled] { 307 | cursor: default; 308 | } 309 | 310 | /* 311 | * 1. Addresses box sizing set to `content-box` in IE 8/9. 312 | * 2. Removes excess padding in IE 8/9. 313 | */ 314 | 315 | input[type="checkbox"], 316 | input[type="radio"] { 317 | box-sizing: border-box; /* 1 */ 318 | padding: 0; /* 2 */ 319 | } 320 | 321 | /* 322 | * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. 323 | * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome 324 | * (include `-moz` to future-proof). 325 | */ 326 | 327 | input[type="search"] { 328 | -webkit-appearance: textfield; /* 1 */ 329 | -moz-box-sizing: content-box; 330 | -webkit-box-sizing: content-box; /* 2 */ 331 | box-sizing: content-box; 332 | } 333 | 334 | /* 335 | * Removes inner padding and search cancel button in Safari 5 and Chrome 336 | * on OS X. 337 | */ 338 | 339 | input[type="search"]::-webkit-search-cancel-button, 340 | input[type="search"]::-webkit-search-decoration { 341 | -webkit-appearance: none; 342 | } 343 | 344 | /* 345 | * Removes inner padding and border in Firefox 4+. 346 | */ 347 | 348 | button::-moz-focus-inner, 349 | input::-moz-focus-inner { 350 | border: 0; 351 | padding: 0; 352 | } 353 | 354 | /* 355 | * 1. Removes default vertical scrollbar in IE 8/9. 356 | * 2. Improves readability and alignment in all browsers. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; /* 1 */ 361 | vertical-align: top; /* 2 */ 362 | } 363 | 364 | /* ========================================================================== 365 | Tables 366 | ========================================================================== */ 367 | 368 | /* 369 | * Remove most spacing between table cells. 370 | */ 371 | 372 | table { 373 | border-collapse: collapse; 374 | border-spacing: 0; 375 | } -------------------------------------------------------------------------------- /docs/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | new Text( name, optional ) 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 56 | 57 | 234 |
235 | 236 | 237 | -------------------------------------------------------------------------------- /docs/resource.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | resource.coffee 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 56 | 57 | 254 |
255 | 256 | 257 | -------------------------------------------------------------------------------- /docs/docco.css: -------------------------------------------------------------------------------- 1 | /*--------------------- Typography ----------------------------*/ 2 | 3 | @font-face { 4 | font-family: 'aller-light'; 5 | src: url('public/fonts/aller-light.eot'); 6 | src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), 7 | url('public/fonts/aller-light.woff') format('woff'), 8 | url('public/fonts/aller-light.ttf') format('truetype'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'aller-bold'; 15 | src: url('public/fonts/aller-bold.eot'); 16 | src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), 17 | url('public/fonts/aller-bold.woff') format('woff'), 18 | url('public/fonts/aller-bold.ttf') format('truetype'); 19 | font-weight: normal; 20 | font-style: normal; 21 | } 22 | 23 | @font-face { 24 | font-family: 'roboto-black'; 25 | src: url('public/fonts/roboto-black.eot'); 26 | src: url('public/fonts/roboto-black.eot?#iefix') format('embedded-opentype'), 27 | url('public/fonts/roboto-black.woff') format('woff'), 28 | url('public/fonts/roboto-black.ttf') format('truetype'); 29 | font-weight: normal; 30 | font-style: normal; 31 | } 32 | 33 | /*--------------------- Layout ----------------------------*/ 34 | html { height: 100%; } 35 | body { 36 | font-family: "aller-light"; 37 | font-size: 14px; 38 | line-height: 18px; 39 | color: #30404f; 40 | margin: 0; padding: 0; 41 | height:100%; 42 | } 43 | #container { min-height: 100%; } 44 | 45 | a { 46 | color: #000; 47 | } 48 | 49 | b, strong { 50 | font-weight: normal; 51 | font-family: "aller-bold"; 52 | } 53 | 54 | p { 55 | margin: 15px 0 0px; 56 | } 57 | .annotation ul, .annotation ol { 58 | margin: 25px 0; 59 | } 60 | .annotation ul li, .annotation ol li { 61 | font-size: 14px; 62 | line-height: 18px; 63 | margin: 10px 0; 64 | } 65 | 66 | h1, h2, h3, h4, h5, h6 { 67 | color: #112233; 68 | line-height: 1em; 69 | font-weight: normal; 70 | font-family: "roboto-black"; 71 | text-transform: uppercase; 72 | margin: 30px 0 15px 0; 73 | } 74 | 75 | h1 { 76 | margin-top: 40px; 77 | } 78 | h2 { 79 | font-size: 1.26em; 80 | } 81 | 82 | hr { 83 | border: 0; 84 | background: 1px #ddd; 85 | height: 1px; 86 | margin: 20px 0; 87 | } 88 | 89 | pre, tt, code { 90 | font-size: 12px; line-height: 16px; 91 | font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; 92 | margin: 0; padding: 0; 93 | } 94 | .annotation pre { 95 | display: block; 96 | margin: 0; 97 | padding: 7px 10px; 98 | background: #fcfcfc; 99 | -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 100 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 101 | box-shadow: inset 0 0 10px rgba(0,0,0,0.1); 102 | overflow-x: auto; 103 | } 104 | .annotation pre code { 105 | border: 0; 106 | padding: 0; 107 | background: transparent; 108 | } 109 | 110 | 111 | blockquote { 112 | border-left: 5px solid #ccc; 113 | margin: 0; 114 | padding: 1px 0 1px 1em; 115 | } 116 | .sections blockquote p { 117 | font-family: Menlo, Consolas, Monaco, monospace; 118 | font-size: 12px; line-height: 16px; 119 | color: #999; 120 | margin: 10px 0 0; 121 | white-space: pre-wrap; 122 | } 123 | 124 | ul.sections { 125 | list-style: none; 126 | padding:0 0 5px 0;; 127 | margin:0; 128 | } 129 | 130 | /* 131 | Force border-box so that % widths fit the parent 132 | container without overlap because of margin/padding. 133 | 134 | More Info : http://www.quirksmode.org/css/box.html 135 | */ 136 | ul.sections > li > div { 137 | -moz-box-sizing: border-box; /* firefox */ 138 | -ms-box-sizing: border-box; /* ie */ 139 | -webkit-box-sizing: border-box; /* webkit */ 140 | -khtml-box-sizing: border-box; /* konqueror */ 141 | box-sizing: border-box; /* css3 */ 142 | } 143 | 144 | 145 | /*---------------------- Jump Page -----------------------------*/ 146 | #jump_to, #jump_page { 147 | margin: 0; 148 | background: white; 149 | -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; 150 | -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; 151 | font: 16px Arial; 152 | cursor: pointer; 153 | text-align: right; 154 | list-style: none; 155 | } 156 | 157 | #jump_to a { 158 | text-decoration: none; 159 | } 160 | 161 | #jump_to a.large { 162 | display: none; 163 | } 164 | #jump_to a.small { 165 | font-size: 22px; 166 | font-weight: bold; 167 | color: #676767; 168 | } 169 | 170 | #jump_to, #jump_wrapper { 171 | position: fixed; 172 | right: 0; top: 0; 173 | padding: 10px 15px; 174 | margin:0; 175 | } 176 | 177 | #jump_wrapper { 178 | display: none; 179 | padding:0; 180 | } 181 | 182 | #jump_to:hover #jump_wrapper { 183 | display: block; 184 | } 185 | 186 | #jump_page_wrapper{ 187 | position: fixed; 188 | right: 0; 189 | top: 0; 190 | bottom: 0; 191 | } 192 | 193 | #jump_page { 194 | padding: 5px 0 3px; 195 | margin: 0 0 25px 25px; 196 | max-height: 100%; 197 | overflow: auto; 198 | } 199 | 200 | #jump_page .source { 201 | display: block; 202 | padding: 15px; 203 | text-decoration: none; 204 | border-top: 1px solid #eee; 205 | } 206 | 207 | #jump_page .source:hover { 208 | background: #f5f5ff; 209 | } 210 | 211 | #jump_page .source:first-child { 212 | } 213 | 214 | /*---------------------- Low resolutions (> 320px) ---------------------*/ 215 | @media only screen and (min-width: 320px) { 216 | .pilwrap { display: none; } 217 | 218 | ul.sections > li > div { 219 | display: block; 220 | padding:5px 10px 0 10px; 221 | } 222 | 223 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 224 | padding-left: 30px; 225 | } 226 | 227 | ul.sections > li > div.content { 228 | overflow-x:auto; 229 | -webkit-box-shadow: inset 0 0 5px #e5e5ee; 230 | box-shadow: inset 0 0 5px #e5e5ee; 231 | border: 1px solid #dedede; 232 | margin:5px 10px 5px 10px; 233 | padding-bottom: 5px; 234 | } 235 | 236 | ul.sections > li > div.annotation pre { 237 | margin: 7px 0 7px; 238 | padding-left: 15px; 239 | } 240 | 241 | ul.sections > li > div.annotation p tt, .annotation code { 242 | background: #f8f8ff; 243 | border: 1px solid #dedede; 244 | font-size: 12px; 245 | padding: 0 0.2em; 246 | } 247 | } 248 | 249 | /*---------------------- (> 481px) ---------------------*/ 250 | @media only screen and (min-width: 481px) { 251 | #container { 252 | position: relative; 253 | } 254 | body { 255 | background-color: #F5F5FF; 256 | font-size: 15px; 257 | line-height: 21px; 258 | } 259 | pre, tt, code { 260 | line-height: 18px; 261 | } 262 | p, ul, ol { 263 | margin: 0 0 15px; 264 | } 265 | 266 | 267 | #jump_to { 268 | padding: 5px 10px; 269 | } 270 | #jump_wrapper { 271 | padding: 0; 272 | } 273 | #jump_to, #jump_page { 274 | font: 10px Arial; 275 | text-transform: uppercase; 276 | } 277 | #jump_page .source { 278 | padding: 5px 10px; 279 | } 280 | #jump_to a.large { 281 | display: inline-block; 282 | } 283 | #jump_to a.small { 284 | display: none; 285 | } 286 | 287 | 288 | 289 | #background { 290 | position: absolute; 291 | top: 0; bottom: 0; 292 | width: 350px; 293 | background: #fff; 294 | border-right: 1px solid #e5e5ee; 295 | z-index: -1; 296 | } 297 | 298 | ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { 299 | padding-left: 40px; 300 | } 301 | 302 | ul.sections > li { 303 | white-space: nowrap; 304 | } 305 | 306 | ul.sections > li > div { 307 | display: inline-block; 308 | } 309 | 310 | ul.sections > li > div.annotation { 311 | max-width: 350px; 312 | min-width: 350px; 313 | min-height: 5px; 314 | padding: 13px; 315 | overflow-x: hidden; 316 | white-space: normal; 317 | vertical-align: top; 318 | text-align: left; 319 | } 320 | ul.sections > li > div.annotation pre { 321 | margin: 15px 0 15px; 322 | padding-left: 15px; 323 | } 324 | 325 | ul.sections > li > div.content { 326 | padding: 13px; 327 | vertical-align: top; 328 | border: none; 329 | -webkit-box-shadow: none; 330 | box-shadow: none; 331 | } 332 | 333 | .pilwrap { 334 | position: relative; 335 | display: inline; 336 | } 337 | 338 | .pilcrow { 339 | font: 12px Arial; 340 | text-decoration: none; 341 | color: #454545; 342 | position: absolute; 343 | top: 3px; left: -20px; 344 | padding: 1px 2px; 345 | opacity: 0; 346 | -webkit-transition: opacity 0.2s linear; 347 | } 348 | .for-h1 .pilcrow { 349 | top: 47px; 350 | } 351 | .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { 352 | top: 35px; 353 | } 354 | 355 | ul.sections > li > div.annotation:hover .pilcrow { 356 | opacity: 1; 357 | } 358 | } 359 | 360 | /*---------------------- (> 1025px) ---------------------*/ 361 | @media only screen and (min-width: 1025px) { 362 | 363 | body { 364 | font-size: 16px; 365 | line-height: 24px; 366 | } 367 | 368 | #background { 369 | width: 525px; 370 | } 371 | ul.sections > li > div.annotation { 372 | max-width: 525px; 373 | min-width: 525px; 374 | padding: 10px 25px 1px 50px; 375 | } 376 | ul.sections > li > div.content { 377 | padding: 9px 15px 16px 25px; 378 | } 379 | } 380 | 381 | /*---------------------- Syntax Highlighting -----------------------------*/ 382 | 383 | td.linenos { background-color: #f0f0f0; padding-right: 10px; } 384 | span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } 385 | /* 386 | 387 | github.com style (c) Vasily Polovnyov 388 | 389 | */ 390 | 391 | pre code { 392 | display: block; padding: 0.5em; 393 | color: #000; 394 | background: #f8f8ff 395 | } 396 | 397 | pre .hljs-comment, 398 | pre .hljs-template_comment, 399 | pre .hljs-diff .hljs-header, 400 | pre .hljs-javadoc { 401 | color: #408080; 402 | font-style: italic 403 | } 404 | 405 | pre .hljs-keyword, 406 | pre .hljs-assignment, 407 | pre .hljs-literal, 408 | pre .hljs-css .hljs-rule .hljs-keyword, 409 | pre .hljs-winutils, 410 | pre .hljs-javascript .hljs-title, 411 | pre .hljs-lisp .hljs-title, 412 | pre .hljs-subst { 413 | color: #954121; 414 | /*font-weight: bold*/ 415 | } 416 | 417 | pre .hljs-number, 418 | pre .hljs-hexcolor { 419 | color: #40a070 420 | } 421 | 422 | pre .hljs-string, 423 | pre .hljs-tag .hljs-value, 424 | pre .hljs-phpdoc, 425 | pre .hljs-tex .hljs-formula { 426 | color: #219161; 427 | } 428 | 429 | pre .hljs-title, 430 | pre .hljs-id { 431 | color: #19469D; 432 | } 433 | pre .hljs-params { 434 | color: #00F; 435 | } 436 | 437 | pre .hljs-javascript .hljs-title, 438 | pre .hljs-lisp .hljs-title, 439 | pre .hljs-subst { 440 | font-weight: normal 441 | } 442 | 443 | pre .hljs-class .hljs-title, 444 | pre .hljs-haskell .hljs-label, 445 | pre .hljs-tex .hljs-command { 446 | color: #458; 447 | font-weight: bold 448 | } 449 | 450 | pre .hljs-tag, 451 | pre .hljs-tag .hljs-title, 452 | pre .hljs-rules .hljs-property, 453 | pre .hljs-django .hljs-tag .hljs-keyword { 454 | color: #000080; 455 | font-weight: normal 456 | } 457 | 458 | pre .hljs-attribute, 459 | pre .hljs-variable, 460 | pre .hljs-instancevar, 461 | pre .hljs-lisp .hljs-body { 462 | color: #008080 463 | } 464 | 465 | pre .hljs-regexp { 466 | color: #B68 467 | } 468 | 469 | pre .hljs-class { 470 | color: #458; 471 | font-weight: bold 472 | } 473 | 474 | pre .hljs-symbol, 475 | pre .hljs-ruby .hljs-symbol .hljs-string, 476 | pre .hljs-ruby .hljs-symbol .hljs-keyword, 477 | pre .hljs-ruby .hljs-symbol .hljs-keymethods, 478 | pre .hljs-lisp .hljs-keyword, 479 | pre .hljs-tex .hljs-special, 480 | pre .hljs-input_number { 481 | color: #990073 482 | } 483 | 484 | pre .hljs-builtin, 485 | pre .hljs-constructor, 486 | pre .hljs-built_in, 487 | pre .hljs-lisp .hljs-title { 488 | color: #0086b3 489 | } 490 | 491 | pre .hljs-preprocessor, 492 | pre .hljs-pi, 493 | pre .hljs-doctype, 494 | pre .hljs-shebang, 495 | pre .hljs-cdata { 496 | color: #999; 497 | font-weight: bold 498 | } 499 | 500 | pre .hljs-deletion { 501 | background: #fdd 502 | } 503 | 504 | pre .hljs-addition { 505 | background: #dfd 506 | } 507 | 508 | pre .hljs-diff .hljs-change { 509 | background: #0086b3 510 | } 511 | 512 | pre .hljs-chunk { 513 | color: #aaa 514 | } 515 | 516 | pre .hljs-tex .hljs-formula { 517 | opacity: 0.5; 518 | } 519 | -------------------------------------------------------------------------------- /lib/route.coffee: -------------------------------------------------------------------------------- 1 | { Key, Glob } = require './key' 2 | { Text } = require './text' 3 | { Resource } = require './resource' 4 | { kindof, mixin } = require './helpers' 5 | inflection = require 'inflection' 6 | 7 | # new Route( router, path [, method] ) 8 | # ==================================== 9 | # turns strings into magical ponies that come when you call them 10 | # 11 | # route = new Route(router, '/:controller/:action/:id(.:format)') 12 | # route = new Route(router, '/:controller/:action(/:id)(.:format)', 'GET') 13 | # route = new Route(router, '/:controller/:action(/:id)(.:format)', 14 | # route = new Route(router, '/:controller/:action/:id(.:format)', 'GET') 15 | # 16 | # Pretty familiar to anyone who's used Merb/Rails - called by Router.match() 17 | # 18 | exports.Route = 19 | class Route 20 | constructor: ( router, path, method, @optional=false )-> 21 | if router && path 22 | @match.apply this, arguments 23 | 24 | 25 | # route.match() 26 | # ------------------- 27 | # 28 | # the actual route builder function, mostly called by `new Route` 29 | # 30 | match: ( router, path, method, @optional=false )-> 31 | 32 | if typeof path != 'string' 33 | throw new Error 'path must be a string' 34 | 35 | # is this a nested path? 36 | if @path? 37 | prefix = @path 38 | 39 | # get a list of key names in the new segment TODO: globs 40 | new_keys = [] 41 | new_keys.push ':id' if @collection || @member # force id to be renamed for resources 42 | new_keys.push *path.match RegExp Key.regex.source, 'g' # find ALL 43 | 44 | # rename earlier keys 45 | for key in new_keys 46 | replKey = new RegExp ":(#{key.substring(1)}/?)" 47 | prefix = prefix.replace replKey, ":#{inflection.underscore inflection.singularize @params.controller}_$1" 48 | 49 | # create a new route instance 50 | nested_route = new Route router, prefix+path, method, @optional 51 | # add local default params 52 | nested_route.default_params = @default_params 53 | # apply local conditions 54 | nested_route.where @conditions if @conditions 55 | # return the new awesomeness 56 | return nested_route 57 | 58 | # uppercase the method name 59 | if typeof(method) == 'string' 60 | @method = method.toUpperCase() 61 | 62 | # base properties 63 | @params = {} 64 | @default_params = {} 65 | @parts = [] 66 | @route_name = null 67 | @path = path 68 | # @router = router 69 | Object.defineProperty this, 'router', # exclude in enumerables 70 | enumerable: false 71 | configurable: false 72 | writable: false 73 | value: router 74 | 75 | @parts = Route.parse router, path, method, @optional 76 | 77 | # reset the path to the generated one (chop off any extra )'s ) 78 | @path = @toString() 79 | 80 | unless @optional 81 | @router.routes.push this 82 | 83 | this 84 | 85 | 86 | # convenience methods 87 | # ------------------- 88 | 89 | get: ( path )-> @match @router, path, 'GET' 90 | put: ( path )-> @match @router, path, 'PUT' 91 | post: ( path )-> @match @router, path, 'POST' 92 | patch: ( path )-> @match @router, path, 'PATCH' 93 | del: ( path )-> @match @router, path, 'DELETE' 94 | 95 | 96 | # route.resource( controller ) 97 | # ----------------------------- 98 | # generates standard resource routes for a controller name 99 | # 100 | # route.resource('products') 101 | # 102 | # returns a Resource object 103 | # 104 | resource: ( controller )-> 105 | new Resource this, controller 106 | 107 | 108 | # route.regexString() 109 | # ------------------- 110 | # 111 | # returns a composite regex string of all route parts 112 | # 113 | regexString: -> 114 | # a route regex is a composite of its parts' regexe(s|n) 115 | ret = ['('] 116 | ret.push part.regexString() for part in @parts 117 | ret.push ')' 118 | ret.push '?' if @optional 119 | ret.join '' 120 | 121 | # route.test( string ) 122 | # ----------- 123 | # builds & tests on a full regex of the entire path 124 | # 125 | # route.test( '/products/19/edit' ) 126 | # => true 127 | # 128 | # returns true/false depending on whether the url matches 129 | # 130 | test: ( string )-> 131 | # cache the regex string 132 | @regex ?= RegExp "^#{@regexString()}(\\\?.*)?$" 133 | 134 | @regex.test string 135 | 136 | 137 | 138 | # route.to( endpoint [, default_params ] ) 139 | # -------------------------------------- 140 | # defines the endpoint & mixes in optional params 141 | # 142 | # route.to( 'controller.action' ) 143 | # 144 | # route.to( 'controller.action', {lang:'en'} ) 145 | # 146 | # returns the route for chaining 147 | # 148 | to: ( endpoint, default_params )-> 149 | 150 | if !default_params && typeof endpoint != 'string' 151 | [ default_params, endpoint ] = [ endpoint, undefined ] 152 | 153 | mixin @default_params, default_params 154 | 155 | # TODO: make endpoint optional, since you can have the 156 | # controller & action in the URL utself, 157 | # even though that's a terrible idea... 158 | 159 | if endpoint 160 | unless 0 < endpoint.indexOf '.' 161 | throw new Error 'syntax should be in the form: controller.action' 162 | [ @params.controller, @params.action ] = endpoint.split '.' 163 | 164 | mixin @params, @default_params 165 | 166 | this # chainable 167 | 168 | 169 | # route.as( name ) 170 | # ------------------ 171 | # sets the route name - NAMED ROUTES ARE NOT CURRENTLY USED 172 | # 173 | # route.as( 'login' ) 174 | # route.as( 'homepage' ) # etc... 175 | # 176 | # returns: the route for chaining 177 | # 178 | as: ( @route_name )-> 179 | this # chainable 180 | 181 | # alias for as 182 | name: ( name )-> 183 | console.log ''' 184 | DEPRECATION NOTICE: this method has been renamed "as" 185 | and will be removed in a future version of Barista 186 | ''' 187 | @as.apply this, arguments 188 | 189 | 190 | # route.where( conditions ) 191 | # ------------------------- 192 | # sets conditions that each url variable must match for the URL to be valid 193 | # 194 | # route.where( { id:/\d+/, username:/\w+/ } ) 195 | # 196 | # returns: the route for chaining 197 | # 198 | where: ( @conditions )-> 199 | if kindof(@conditions) != 'object' 200 | throw new Error 'conditions must be an object' 201 | # recursively apply all conditions to sub-parts 202 | part.where? @conditions for part in @parts 203 | this # chainable 204 | 205 | 206 | # route.stringify( params ) 207 | # ------------------------- 208 | # builds a string url for this Route from a params object 209 | # 210 | # returns: [ "url", [leftover params] ] 211 | # 212 | # **this is meant to be called & modified by router.url()** 213 | # 214 | stringify: ( params )-> 215 | url = [] # urls start life as an array to enable a second pass 216 | for part in @parts 217 | if part instanceof Key 218 | if params[part.name]? && part.regex.test params[part.name] 219 | # there's a param named this && the param matches the key's regex 220 | url.push part.url params[part.name] # push it onto the stack 221 | delete params[part.name] 222 | # `delete params[part.name]` # and remove from list of params 223 | else if @optional 224 | # (sub)route doesn't match, move on 225 | return false 226 | else if part instanceof Route 227 | # sub-routes must be handled in the next pass 228 | # to avoid leftover param duplication 229 | url.push part 230 | else # string 231 | url.push part 232 | 233 | # second pass, resolve optional parts (backwards, so later optionals are resolved first) 234 | for part, i in url by -1 235 | if part instanceof Route 236 | part = part.stringify params # recursion is your friend 237 | # it resolved to a url fragment! 238 | if part 239 | params = part[1] # replace leftover params hash with the new, smaller leftover params hash 240 | url[i] = part = part[0] # leave only the string for joining 241 | else 242 | delete url[i] # get rid of these shits 243 | 244 | for key, val of @params 245 | # remove from leftovers, they're implied in the to() portion of the route 246 | delete params[key] 247 | 248 | [ url.join(''), params ] 249 | 250 | 251 | # route.keysAndRoutes() 252 | # --------------------- 253 | # just the parts that aren't strings. basically 254 | # 255 | # returns an array of Key and Route objects 256 | # 257 | keysAndRoutes: -> 258 | part for part in @parts when part instanceof Key || part instanceof Route 259 | 260 | # route.keys() 261 | # --------------------- 262 | # just the parts that are Keys (or globs) 263 | # 264 | # returns an array of aforementioned Keys 265 | # 266 | keys: -> 267 | part for part in @parts when part instanceof Key 268 | 269 | 270 | # route.parse( url, method ) 271 | # -------------------------- 272 | # parses a URL into a params object 273 | # 274 | # route.parse( '/products/15/edit', 'GET' ) 275 | # => { controller:'products', action:'edit', id:15 } 276 | # 277 | # returns: a params hash || false (if the route doesn't match) 278 | # 279 | # **this is meant to be called by Router.first() && Router.all()** 280 | # 281 | parse: ( urlParam, method )-> 282 | 283 | # parse the URL with the regex & step along with the parts, 284 | # assigning the vals from the url to the names of the keys as we go (potentially stoopid) 285 | 286 | # let's chop off the QS to make life easier 287 | url = require('url').parse urlParam # TODO: fix this 288 | path = decodeURI url.pathname 289 | params = method: method 290 | 291 | mixin params, @params 292 | 293 | # route HEAD requests to GET endpoints 294 | if is_head_req = params.method == 'HEAD' 295 | params.method = 'GET' # we'll put it back when we're done 296 | 297 | # if the method doesn't match, gtfo immediately 298 | return false if @method? && params.method? && @method != params.method 299 | 300 | # assign the route's method if there isn't one 301 | params.method ?= @method 302 | 303 | # TODO: implement substring checks for possible performance boost 304 | 305 | # if the route doesn't match the regex, gtfo 306 | return false unless @test path 307 | 308 | # parse the URL with the regex 309 | # first 2 elements are shit, chop 'em 310 | parts = new RegExp("^#{@regexString()}$").exec(path)[2..] 311 | pairings = [] 312 | 313 | # loop 1 (forwards) - collect the key/route -> part pairings for loop 2 314 | j = 0 315 | for segm in @keysAndRoutes() 316 | part = parts[j] 317 | 318 | # stash the pairings for loop 2 if this part matches segment 319 | pairings.push [ segm, part ] if segm.test part 320 | 321 | # routes must advance the part iterator by the number of parts matched in the segment 322 | j+= segm.regex.exec(part||'')[2..].length || 1 323 | 324 | 325 | # loop 2 (backwards) - parse the key/route -> part pairings (backards so later optional matches are preferred) 326 | # i.e. /path(.:format)/something(.:format) would keep the last format segment, while discarding the first 327 | # this makes life easier for nesting route definitions 328 | for [ segm, part ] in pairings by -1 329 | # actually mixin the params 330 | if segm instanceof Key 331 | params[segm.name] = part 332 | else if segm instanceof Route 333 | mixin params, segm.parse part, method 334 | 335 | # replace HEAD method? 336 | params.method = 'HEAD' if is_head_req 337 | 338 | params 339 | 340 | 341 | 342 | nest: ( cb )-> 343 | unless typeof cb == 'function' 344 | throw new Error 'route.nest() requires a callback function' 345 | cb.call this 346 | this # for chaining 347 | 348 | 349 | 350 | # route.toString() 351 | # ---------------- 352 | # returns the original route definition 353 | # 354 | toString: -> 355 | defn = (part.toString() for part in @parts).join '' 356 | return "(#{ defn })" if @optional 357 | defn 358 | 359 | # Route.parse( router, string, method, optional=false ) 360 | # ===================================================== 361 | # parses a route definition into a swiss army bazooka 362 | # 363 | # route = Route.parse(router, '/:controller/:action/:id(.:format)') 364 | # route = Route.parse(router, '/:controller/:action(/:id)(.:format)', 'GET') 365 | # route = Route.parse(router, '/:controller/:action(/:id)(.:format)', 366 | # route = Route.parse(router, '/:controller/:action/:id(.:format)', 'GET') 367 | # 368 | # Pretty familiar to anyone who's used Merb/Rails - called by Router.match() 369 | # 370 | 371 | @parse = ( router, string, method, optional=false )-> 372 | parts = [] 373 | 374 | # parse it char by char, baby 375 | i = 0 376 | len = string.length 377 | 378 | # for char, i in string 379 | while i < len 380 | char = string[i] 381 | rest = string[i..] 382 | 383 | # special consideration for Ogrp starts 384 | if char == '(' && i == 0 385 | i++ 386 | continue # skip this char, since it's not strictly part of the route definition 387 | 388 | # the route definition is over, return what we have 389 | if char == ')' 390 | return parts 391 | 392 | if char == ':' && string[i+1] != ':' 393 | parts.push part = Key.parse rest 394 | else if char == '*' 395 | parts.push part = Glob.parse rest 396 | else if char == '(' 397 | parts.push part = new Route router, rest, method, true 398 | else 399 | parts.push part = Text.parse rest 400 | 401 | i += part.toString().length 402 | 403 | parts 404 | -------------------------------------------------------------------------------- /docs/key.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | new Key( name, optional ) 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 56 | 57 |
    58 | 59 | 60 | 61 |
  • 62 |
    63 | 64 |
    65 | 66 |
    67 | 68 |
    69 | 70 |
    { kindof, mixin } = require './helpers'
    71 | 72 |
  • 73 | 74 | 75 |
  • 76 |
    77 | 78 |
    79 | 80 |
    81 |

    new Key( name, optional )

    82 | 83 |
    84 | 85 |
  • 86 | 87 | 88 |
  • 89 |
    90 | 91 |
    92 | 93 |
    94 |

    those variable thingies

    95 |
    key = new Key('name')
     96 | key = new Key('name', true)
     97 | 
    98 |
    99 | 100 |
    exports.Key =
    101 | class Key
    102 |   regex: /[\w\-\s]+/
    103 |   constructor: ( @name, @optional )->
    104 | 105 |
  • 106 | 107 | 108 |
  • 109 |
    110 | 111 |
    112 | 113 |
    114 |

    special defaults for controllers & actions, which will always be function-name-safe

    115 | 116 |
    117 | 118 |
        if @name == 'controller' || @name == 'action'
    119 |       @regex = /[a-zA-Z_][\w\-]*/
    120 | 121 |
  • 122 | 123 | 124 |
  • 125 |
    126 | 127 |
    128 | 129 |
    130 |

    key.regexString()

    131 | 132 |
    133 | 134 |
  • 135 | 136 | 137 |
  • 138 |
    139 | 140 |
    141 | 142 |
    143 |

    makes a regex string of the key - used by key.test()

    144 |

    returns a string of this keys regex

    145 | 146 |
    147 | 148 |
      regexString: ->
    149 |     ret = ['(']
    150 |     ret.push @regex.source
    151 |     ret.push ')'
    152 |     ret.push '?' if @optional
    153 |     ret.join ''
    154 | 155 |
  • 156 | 157 | 158 |
  • 159 |
    160 | 161 |
    162 | 163 |
    164 |

    key.test( string )

    165 | 166 |
    167 | 168 |
  • 169 | 170 | 171 |
  • 172 |
    173 | 174 |
    175 | 176 |
    177 |

    validates a string using the key’s regex pattern

    178 |

    returns true/false if the string matches

    179 | 180 |
    181 | 182 |
      test: ( string )-> # this regex test passes for null & undefined :-(
    183 |     new RegExp("^#{@regexString()}$").test string
    184 | 185 |
  • 186 | 187 | 188 |
  • 189 |
    190 | 191 |
    192 | 193 |
    194 |

    key.url( string )

    195 | 196 |
    197 | 198 |
  • 199 | 200 | 201 |
  • 202 |
    203 | 204 |
    205 | 206 |
    207 |

    returns a string for building the url 208 | if it matches the key conditions

    209 | 210 |
    211 | 212 |
      url: ( string )->
    213 |     if @test(string) then string else false
    214 | 215 |
  • 216 | 217 | 218 |
  • 219 |
    220 | 221 |
    222 | 223 |
    224 |

    key.where( conditions )

    225 | 226 |
    227 | 228 |
  • 229 | 230 | 231 |
  • 232 |
    233 | 234 |
    235 | 236 |
    237 |

    adds conditions that the key must match

    238 |

    returns the key… because it can?

    239 | 240 |
    241 | 242 |
      where: ( conditions )->
    243 | 
    244 |     condition = conditions[@name]
    245 | 
    246 |     if condition instanceof RegExp
    247 |       @regex = condition #  e.g. /\d+/
    248 | 
    249 |     if condition instanceof String
    250 |       @regex = new RegExp condition #  e.g. "\d+"
    251 | 252 |
  • 253 | 254 | 255 |
  • 256 |
    257 | 258 |
    259 | 260 |
    261 |

    an array of allowed values, e.g. [‘stop’,’play’,’pause’]

    262 | 263 |
    264 | 265 |
        if condition instanceof Array
    266 |       ret = []
    267 |       for c in condition
    268 |         ret.push c.source if 'regex' == kindof c
    269 |         ret.push c if 'string' == kindof c
    270 |       @regex = new RegExp ret.join '|'
    271 | 
    272 |     this # chainable
    273 | 274 |
  • 275 | 276 | 277 |
  • 278 |
    279 | 280 |
    281 | 282 |
    283 |

    key.toString()

    284 | 285 |
    286 | 287 |
  • 288 | 289 | 290 |
  • 291 |
    292 | 293 |
    294 | 295 |
    296 |

    returns the original key definition

    297 | 298 |
    299 | 300 |
      toString: ->
    301 |     ":#{@name}"
    302 | 
    303 | 
    304 |   @regex = /:([a-zA-Z_][\w\-]*)/
    305 |   @parse = ( string, optional=false )->
    306 |     [ definition, name ] = @regex.exec string
    307 |     new @ name, optional
    308 | 309 |
  • 310 | 311 | 312 |
  • 313 |
    314 | 315 |
    316 | 317 |
    318 |

    new Glob( name, optional )

    319 | 320 |
    321 | 322 |
  • 323 | 324 | 325 |
  • 326 |
    327 | 328 |
    329 | 330 |
    331 |

    globs are just greedy keys

    332 |
    glob = new Glob('name')
    333 | glob = new Glob('name', true)
    334 | 
    335 |
    336 | 337 |
    exports.Glob =
    338 | class Glob extends Key
    339 |   regex: /[\w\-\/\s]+?/ # default url-friendly regex
    340 |   constructor: ( @name, @optional )->
    341 | 342 |
  • 343 | 344 | 345 |
  • 346 |
    347 | 348 |
    349 | 350 |
    351 |

    special defaults for controllers & actions, which will always be function-name-safe

    352 | 353 |
    354 | 355 |
        if @name == 'controller' || @name == 'action'
    356 |       @regex = /[a-zA-Z_][\w\-]*/
    357 | 358 |
  • 359 | 360 | 361 |
  • 362 |
    363 | 364 |
    365 | 366 |
    367 |

    glob.toString()

    368 | 369 |
    370 | 371 |
  • 372 | 373 | 374 |
  • 375 |
    376 | 377 |
    378 | 379 |
    380 |

    returns the original glob definition

    381 | 382 |
    383 | 384 |
      toString: ->
    385 |     "*#{@name}"
    386 | 
    387 |   @regex = /\*([a-zA-Z_][\w\-]*)/
    388 | 389 |
  • 390 | 391 |
392 |
393 | 394 | 395 | -------------------------------------------------------------------------------- /docs/router.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | router.coffee 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 56 | 57 |
    58 | 59 |
  • 60 |
    61 |

    router.coffee

    62 |
    63 |
  • 64 | 65 | 66 | 67 |
  • 68 |
    69 | 70 |
    71 | 72 |
    73 | 74 |
    75 | 76 |
    { Route }         = require './route'
     77 | { Resource }      = require './resource'
     78 | qstring           = require 'querystring'
     79 | 
     80 | exports.Router =
     81 | class Router
     82 |   constructor: ->
     83 |     @methods =  [ 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS' ]
     84 |     @routes =   []
    85 | 86 |
  • 87 | 88 | 89 |
  • 90 |
    91 | 92 |
    93 | 94 |
    95 |

    router.match( path [, method] )

    96 | 97 |
    98 | 99 |
  • 100 | 101 | 102 |
  • 103 |
    104 | 105 |
    106 | 107 |
    108 |
    router.match('/:controller/:action(/:id)(.:format)', 'GET')
    109 |  .to(......)
    110 | 

    path is mandatory (duh) 111 | method is optional, routes without a method will apply in all cases

    112 |

    returns the route (for chaining)

    113 | 114 |
    115 | 116 |
      match: ( path, method )->
    117 | 118 |
  • 119 | 120 | 121 |
  • 122 |
    123 | 124 |
    125 | 126 |
    127 |

    upcase the method

    128 | 129 |
    130 | 131 |
        if typeof(method) == 'string'
    132 |       method = method.toUpperCase()
    133 | 134 |
  • 135 | 136 | 137 |
  • 138 |
    139 | 140 |
    141 | 142 |
    143 |

    upcase the method

    144 | 145 |
    146 | 147 |
        if method? && method not in @methods
    148 |       throw new Error "method must be one of: #{ @methods.join ', ' }"
    149 | 
    150 |     route = new Route this, path, method
    151 | 152 |
  • 153 | 154 | 155 |
  • 156 |
    157 | 158 |
    159 | 160 |
    161 |

    router.get( path )

    162 |

    equivalent to

    163 |
    router.match( path, 'GET' )
    164 | 
    165 |
    166 | 167 |
      get: ( path )->
    168 |     @match path, 'GET'
    169 | 170 |
  • 171 | 172 | 173 |
  • 174 |
    175 | 176 |
    177 | 178 |
    179 |

    router.options( path )

    180 |

    equivalent to

    181 |
    router.match( path, 'OPTIONS' )
    182 | 
    183 |
    184 | 185 |
      options: ( path )->
    186 |     @match path, 'OPTIONS'
    187 | 188 |
  • 189 | 190 | 191 |
  • 192 |
    193 | 194 |
    195 | 196 |
    197 |

    router.put( path )

    198 |

    equivalent to

    199 |
    router.match( path, 'PUT' )
    200 | 
    201 |
    202 | 203 |
      put: ( path )->
    204 |     @match path, 'PUT'
    205 | 206 |
  • 207 | 208 | 209 |
  • 210 |
    211 | 212 |
    213 | 214 |
    215 |

    router.post( path )

    216 |

    equivalent to

    217 |
    router.match( path, 'POST' )
    218 | 
    219 |
    220 | 221 |
      post: ( path )->
    222 |     @match path, 'POST'
    223 | 224 |
  • 225 | 226 | 227 |
  • 228 |
    229 | 230 |
    231 | 232 |
    233 |

    router.patch( path )

    234 |

    equivalent to

    235 |
    router.match( path, 'PATCH' )
    236 | 
    237 |
    238 | 239 |
      patch: ( path )->
    240 |     @match path, 'PATCH'
    241 | 242 |
  • 243 | 244 | 245 |
  • 246 |
    247 | 248 |
    249 | 250 |
    251 |

    router.del( path )

    252 |

    equivalent to

    253 |
    router.match( path, 'DEL' )
    254 | 
    255 |
    256 | 257 |
      del: ( path )->
    258 |     @match path, 'DELETE'
    259 | 260 |
  • 261 | 262 | 263 |
  • 264 |
    265 | 266 |
    267 | 268 |
    269 |

    router.resource( controller )

    270 | 271 |
    272 | 273 |
  • 274 | 275 | 276 |
  • 277 |
    278 | 279 |
    280 | 281 |
    282 |

    generates standard resource routes for a controller name

    283 |
    router.resource('products')
    284 | 

    returns a Resource object

    285 | 286 |
    287 | 288 |
      resource: ( controller )->
    289 |     new Resource this, controller
    290 | 291 |
  • 292 | 293 | 294 |
  • 295 |
    296 | 297 |
    298 | 299 |
    300 |

    // router.first( path, method, callback )

    301 | 302 |
    303 | 304 |
  • 305 | 306 | 307 |
  • 308 |
    309 | 310 |
    311 | 312 |
    313 |

    find the first route that match the path & method

    314 |
    router.first('/products/5', 'GET')
    315 | => { controller: 'products', action: 'show', id: 5, method: 'GET' }
    316 | 

    find & return a params hash from the first route that matches. If there’s no match, this will return false

    317 |

    If the options callback function is provided, it will be fired like so:

    318 |
    callback( error, params )
    319 | 
    320 |
    321 | 322 |
      first: ( path, method, cb )->
    323 |     params = false
    324 | 
    325 |     for route in @routes
    326 | 327 |
  • 328 | 329 | 330 |
  • 331 |
    332 | 333 |
    334 | 335 |
    336 |

    attempt the parse

    337 | 338 |
    339 | 340 |
          params = route.parse path, method
    341 | 
    342 |       if params
    343 | 344 |
  • 345 | 346 | 347 |
  • 348 |
    349 | 350 |
    351 | 352 |
    353 |

    fire the callback if given

    354 | 355 |
    356 | 357 |
            if typeof cb == 'function'
    358 |           cb undefined, params
    359 | 360 |
  • 361 | 362 | 363 |
  • 364 |
    365 | 366 |
    367 | 368 |
    369 |

    may as well return this

    370 | 371 |
    372 | 373 |
            return params
    374 | 
    375 |     if typeof cb == 'function'
    376 |       cb 'No matching routes found'
    377 |     false
    378 | 379 |
  • 380 | 381 | 382 |
  • 383 |
    384 | 385 |
    386 | 387 |
    388 |

    router.all( path [, method] )

    389 | 390 |
    391 | 392 |
  • 393 | 394 | 395 |
  • 396 |
    397 | 398 |
    399 | 400 |
    401 |

    find & return a params hash from ALL routes that match

    402 |
    router.all( '/products/5' )
    403 | 
    404 |   => [
    405 |     { controller: 'products', action: 'show', id: 5, method: 'GET' },
    406 |     { controller: 'products', action: 'update', id: 5, method: 'PUT' },
    407 |     { controller: 'products', action: 'destroy', id: 5, method: 'DELETE' },
    408 |   ]
    409 | 

    if there ares no matches, returns an empty array

    410 | 411 |
    412 | 413 |
      all: ( path, method )->
    414 |     ret = []
    415 |     params = false
    416 | 
    417 |     for route in @routes
    418 |       params = route.parse.apply route, arguments
    419 |       if params
    420 |         ret.push params
    421 |     ret
    422 | 423 |
  • 424 | 425 | 426 |
  • 427 |
    428 | 429 |
    430 | 431 |
    432 |

    // router.url( params[, add_querystring=false] )

    433 | 434 |
    435 | 436 |
  • 437 | 438 | 439 |
  • 440 |
    441 | 442 |
    443 | 444 |
    445 |

    generates a URL from a params hash

    446 |
    router.url( {
    447 |   controller: 'products',
    448 |   action: 'show',
    449 |   id: 5
    450 | } )
    451 | => '/products/5'
    452 | 
    453 | router.url( {
    454 |   controller: 'products',
    455 |   action: 'show',
    456 |   id: 5,
    457 |   format: 'json'
    458 | } )
    459 | => '/products/5.json'
    460 | 
    461 | router.url({
    462 |   controller: 'products',
    463 |   action: 'show',
    464 |   id: 5,
    465 |   format: 'json',
    466 |   love: 'cheese'
    467 | }, true )
    468 | => '/products/5.json?love=cheese'
    469 | 

    returns false if there are no suitable routes

    470 | 471 |
    472 | 473 |
      url: ( params, add_querystring )->
    474 |     url = false
    475 | 476 |
  • 477 | 478 | 479 |
  • 480 |
    481 | 482 |
    483 | 484 |
    485 |

    iterate through the existing routes until a suitable match is found

    486 | 487 |
    488 | 489 |
        for route in @routes
    490 | 491 |
  • 492 | 493 | 494 |
  • 495 |
    496 | 497 |
    498 | 499 |
    500 |

    do the controller & acton match?

    501 | 502 |
    503 | 504 |
          continue if route.params.controller? && route.params.controller != params.controller
    505 |       continue if route.params.action? && route.params.action != params.action
    506 | 
    507 |       break if url = route.stringify params
    508 | 
    509 |     return false unless url # no love? return false
    510 |     qs = qstring.stringify url[1] # build the possibly empty query string
    511 | 
    512 |     if add_querystring && qs.length > 0
    513 |       return url[0] + '?' + qs # if there is a query string...
    514 | 
    515 |     url[0] # just return the url
    516 | 517 |
  • 518 | 519 | 520 |
  • 521 |
    522 | 523 |
    524 | 525 |
    526 |

    router.remove( name )

    527 | 528 |
    529 | 530 |
  • 531 | 532 | 533 |
  • 534 |
    535 | 536 |
    537 | 538 |
    539 |

    Removes previously created routes by name

    540 |

    The route must be a named route, and the name is passed in.

    541 |

    returns: Nothing

    542 | 543 |
    544 | 545 |
      remove: ( name )->
    546 |     @routes = (route for route in @routes when route.route_name != name)
    547 | 548 |
  • 549 | 550 | 551 |
  • 552 |
    553 | 554 |
    555 | 556 |
    557 |

    router.defer( testfn() )

    558 | 559 |
    560 | 561 |
  • 562 | 563 | 564 |
  • 565 |
    566 | 567 |
    568 | 569 |
    570 |
    router.defer( test( path, method ) )
    571 | 

    test should be a function that examines non-standard URLs

    572 |

    path and method will be passed in - expects a params hash back OR false on a non-match

    573 |

    returns: DeferredRoute (for… reference? I dunno.)

    574 |

    THIS IS CURRENTLY COMPLETELY UNTESTED. IT MIGHT NOT EVEN WORK. SERIOUSLY.

    575 | 576 |
    577 | 578 |
      defer: ( fn )->
    579 |     if typeof(fn) != 'function'
    580 |       throw new Error 'Router.defer requires a function as the only argument'
    581 | 
    582 |     route = new Route this, 'deferred'
    583 |     route.parse = fn # add the custom parser
    584 |     delete route.test # = function(){return false};
    585 |     delete route.stringify # = function(){ throw new Error('Deferred routes are NOT generatable')};
    586 |     @routes.push route
    587 |     route
    588 | 589 |
  • 590 | 591 | 592 |
  • 593 |
    594 | 595 |
    596 | 597 |
    598 |

    router.toString

    599 | 600 |
    601 | 602 |
  • 603 | 604 | 605 |
  • 606 |
    607 | 608 |
    609 | 610 |
    611 |

    renders a textual description of the router for inpection

    612 | 613 |
    614 | 615 |
      toString: ->
    616 |     ( route.toString() for route in @routes ).join '\n'
    617 | 618 |
  • 619 | 620 |
621 |
622 | 623 | 624 | -------------------------------------------------------------------------------- /tests/barista.test.coffee: -------------------------------------------------------------------------------- 1 | assert = require 'assert' 2 | { Router } = require '../index' 3 | 4 | bench = (fn) -> 5 | return true 6 | start = (new Date).getTime() 7 | fn() for i in [1..1000] 8 | console.log "\navg time: #{ ((new Date).getTime() - start) / 1000 }ms for the following test:" 9 | 10 | router = null 11 | 12 | RouterTests = 13 | 14 | setup: (opts) -> 15 | router = new Router 16 | 17 | teardown: (test) -> 18 | console.log "PASSED :: #{test}" 19 | 20 | 'test Create Router': -> 21 | assert.ok router, @fail 22 | 23 | 'test Create Static Route': -> 24 | route = router.match('/path/to/thing') 25 | assert.ok route, @fail 26 | bench -> 27 | router.match '/path/to/thing' 28 | 29 | 30 | 'test Create Simple Route': -> 31 | route = router.match('/:controller/:action/:id') 32 | assert.ok route, @fail 33 | bench -> 34 | router.match '/:controller/:action/:id' 35 | 36 | 37 | 'test Create Optional Route': -> 38 | route = router.match('/:controller/:action/:id(.:format)') 39 | assert.ok route, @fail 40 | bench -> 41 | router.match '/:controller/:action/:id(.:format)' 42 | 43 | 44 | 'test Create Multiple Optional Route': -> 45 | route = router.match('/:controller/:id(/:action)(.:format)') 46 | assert.ok route, @fail 47 | bench -> 48 | router.match '/:controller/:id(/:action)(.:format)' 49 | 50 | 51 | 'test Create Resource': -> 52 | resource = router.resource('snow_dogs') 53 | assert.ok resource.routes.length == 7, @fail 54 | for i of resource.routes 55 | assert.ok resource.routes[i], @fail 56 | bench -> 57 | router.resource 'snow_dogs' 58 | 59 | 60 | 'test Route With Params': -> 61 | route = router.match('/hello/there').to('applicaton.index') 62 | assert.ok route, @fail 63 | bench -> 64 | router.match('/hello/there').to 'applicaton.index' 65 | 66 | 67 | 'test Route With Extra Params': -> 68 | route = router.match('/hello/there').to('applicaton.index', language: 'english') 69 | assert.ok route, @fail 70 | 71 | 'test Route With Extra Params And Route-Implied Endpoint': -> 72 | route = router.match('/:controller/:action').to(language: 'english') 73 | assert.ok route, @fail 74 | 75 | 'test Route With Method': -> 76 | route = router.match('/:controller/:action', 'GET') 77 | assert.ok route, @fail 78 | 79 | 'test Route With Regex Reqs': -> 80 | route = router.match('/:controller/:action/:id').where(id: /\d+/) 81 | assert.ok route, @fail 82 | 83 | 'test Route With String Regex Reqs': -> 84 | route = router.match('/:controller/:action/:id').where(id: '\\d+') 85 | assert.ok route, @fail 86 | 87 | 'test Route With An Array of String Reqs': -> 88 | route = router.match('/:controller/:action/:id').where(id: [ 89 | 'bob' 90 | 'frank' 91 | 'ted' 92 | ]) 93 | assert.ok route, @fail 94 | 95 | 'test Route With An Array of Mixed Reqs': -> 96 | route = router.match('/:controller/:action/:id').where(id: [ 97 | /\d{1}/ 98 | '\\d\\d' 99 | '123' 100 | ]) 101 | assert.ok route, @fail 102 | 103 | 'test Route With Reqs And Method': -> 104 | route = router.match('/:controller/:action/:id', 'GET').where(id: /\d+/) 105 | assert.ok route, @fail 106 | 107 | 'test Route With Name': -> 108 | route = router.match('/:controller/:action/:id', 'GET').where(id: /\d+/).as('awesome') 109 | assert.ok route, @fail 110 | 111 | 'test Simple Route Parses': -> 112 | route = router.match('/:controller/:action/:id') 113 | params = router.first('/products/show/1', 'GET') 114 | assert.ok params, @fail 115 | assert.equal params.controller, 'products', @fail 116 | assert.equal params.action, 'show', @fail 117 | assert.equal params.id, 1, @fail 118 | assert.equal params.method, 'GET', @fail 119 | bench -> 120 | router.first '/products/show/1', 'GET' 121 | 122 | 123 | 'test Simple Route Parses with regex condition': -> 124 | route = router.match('/:controller/:action/:id').where(id: /\d+/) 125 | params = router.first('/products/show/1', 'GET') 126 | assert.ok params, @fail 127 | assert.equal params.controller, 'products', @fail 128 | assert.equal params.action, 'show', @fail 129 | assert.equal params.id, 1, @fail 130 | assert.equal params.method, 'GET', @fail 131 | bench -> 132 | router.first '/products/show/1', 'GET' 133 | 134 | 135 | 'test Simple Route Parses with string regex condition': -> 136 | route = router.match('/:controller/:action/:id').where(id: '\\d+') 137 | params = router.first('/products/show/1', 'GET') 138 | assert.ok params, @fail 139 | assert.equal params.controller, 'products', @fail 140 | assert.equal params.action, 'show', @fail 141 | assert.equal params.id, 1, @fail 142 | assert.equal params.method, 'GET', @fail 143 | bench -> 144 | router.first '/products/show/1', 'GET' 145 | 146 | 147 | 'test Simple Route Parses with string condition': -> 148 | route = router.match('/:controller/:action/:id').where(id: '1') 149 | params = router.first('/products/show/1', 'GET') 150 | assert.ok params, @fail 151 | assert.equal params.controller, 'products', @fail 152 | assert.equal params.action, 'show', @fail 153 | assert.equal params.id, 1, @fail 154 | assert.equal params.method, 'GET', @fail 155 | bench -> 156 | router.first '/products/show/1', 'GET' 157 | 158 | 159 | 'test Simple Route Parses with an array of mixed conditions': -> 160 | route = router.match('/:controller/:action/:id').where(id: [ 161 | '\\d\\d' 162 | /\d{1}/ 163 | '123' 164 | ]) 165 | params = router.first('/products/show/1', 'GET') 166 | assert.ok params, @fail 167 | assert.equal params.controller, 'products', @fail 168 | assert.equal params.action, 'show', @fail 169 | assert.equal params.id, 1, @fail 170 | assert.equal params.method, 'GET', @fail 171 | bench -> 172 | router.first '/products/show/1', 'GET' 173 | 174 | 'test Simple Route resolves with an array of mixed conditions': -> 175 | route = router.match('/:controller/:action/:id').where(id: [ 176 | 'one' 177 | 'two' 178 | /\d{1}/ 179 | ]) 180 | params = router.first('/products/show/1', 'GET') 181 | assert.ok params, @fail 182 | assert.equal params.controller, 'products', @fail 183 | assert.equal params.action, 'show', @fail 184 | assert.equal params.id, 1, @fail 185 | assert.equal params.method, 'GET', @fail 186 | 187 | params = router.first('/products/show/one', 'GET') 188 | assert.ok params, @fail 189 | assert.equal params.id, 'one', @fail 190 | 191 | bench -> 192 | router.first '/products/show/1', 'GET' 193 | 194 | 195 | 'test Simple Route fails to Parse with bad conditions': -> 196 | route = router.match('/:controller/:action/:id').where(id: /\d+/) 197 | params = router.first('/products/show/bob', 'GET') 198 | assert.equal params, false, @fail 199 | bench -> 200 | router.first '/products/show/1', 'GET' 201 | 202 | 203 | 'test Callback Fires With Params': -> 204 | route = router.match('/:controller/:action/:id') 205 | router.first '/products/show/1', 'GET', (err, params) -> 206 | assert.ok params, @fail 207 | assert.equal params.controller, 'products', @fail 208 | assert.equal params.action, 'show', @fail 209 | assert.equal params.id, 1, @fail 210 | assert.equal params.method, 'GET', @fail 211 | 212 | 213 | 'test Route With Extra Params And Route-Implied Endpoint Parses': -> 214 | route = router.match('/:controller/:action').to(language: 'english') 215 | params = router.first('/products/show', 'GET') 216 | assert.ok params, @fail 217 | assert.equal params.controller, 'products', @fail 218 | assert.equal params.action, 'show', @fail 219 | assert.equal params.method, 'GET', @fail 220 | assert.equal params.language, 'english', @fail 221 | 222 | 'test Simple Route Parses With Optional Segment': -> 223 | route = router.match('/:controller/:action/:id(.:format)') 224 | params = router.first('/products/show/1.html', 'GET') 225 | assert.ok params, @fail 226 | assert.equal params.controller, 'products', @fail 227 | assert.equal params.action, 'show', @fail 228 | assert.equal params.id, 1, @fail 229 | assert.equal params.method, 'GET', @fail 230 | assert.equal params.format, 'html', @fail 231 | bench -> 232 | router.first '/products/show/1.html', 'GET' 233 | 234 | 235 | 'test Simple Route Parses With Optional Segment Missing': -> 236 | route = router.match('/:controller/:action/:id(.:format)', 'GET') 237 | params = router.first('/products/show/1', 'GET') 238 | assert.ok params, @fail 239 | assert.equal params.controller, 'products', @fail 240 | assert.equal params.action, 'show', @fail 241 | assert.equal params.id, 1, @fail 242 | assert.equal params.method, 'GET', @fail 243 | assert.equal typeof params.format, 'undefined', @fail 244 | bench -> 245 | router.first '/products/show/1', 'GET' 246 | 247 | 248 | 'test Simple Route Failing Due To Bad Method': -> 249 | route = router.match('/:controller/:action/:id(.:format)', 'GET') 250 | params = router.first('/products/show/1', 'POST') 251 | assert.equal params, false, @fail 252 | bench -> 253 | router.first '/products/show/1', 'POST' 254 | 255 | 256 | 'test Simple Route With Two Optional Segments': -> 257 | route = router.match('/:controller/:action(/:id)(.:format)', 'GET') 258 | params = router.first('/products/show', 'GET') 259 | assert.ok params, @fail 260 | assert.equal params.controller, 'products', @fail 261 | assert.equal params.action, 'show', @fail 262 | assert.equal typeof params.id, 'undefined', @fail 263 | assert.equal typeof params.format, 'undefined', @fail 264 | assert.equal params.method, 'GET', @fail 265 | bench -> 266 | router.first '/products/show', 'GET' 267 | 268 | 269 | 'test Simple Route With Two Optional Segments With First Used': -> 270 | route = router.match('/:controller/:action(/:id)(.:format)', 'GET') 271 | params = router.first('/products/show/1', 'GET') 272 | assert.ok params, @fail 273 | assert.equal params.controller, 'products', @fail 274 | assert.equal params.action, 'show', @fail 275 | assert.equal params.id, 1, @fail 276 | assert.equal typeof params.format, 'undefined', @fail 277 | assert.equal params.method, 'GET', @fail 278 | bench -> 279 | router.first '/products/show/1', 'GET' 280 | 281 | 282 | 'test Simple Route With Two Optional Segments With Second Used': -> 283 | route = router.match('/:controller/:action(/:id)(.:format)', 'GET') 284 | params = router.first('/products/show.html', 'GET') 285 | assert.ok params, @fail 286 | assert.equal params.controller, 'products', @fail 287 | assert.equal params.action, 'show', @fail 288 | assert.equal typeof params.id, 'undefined', @fail 289 | assert.equal params.format, 'html', @fail 290 | assert.equal params.method, 'GET', @fail 291 | bench -> 292 | router.first '/products/show.html', 'GET' 293 | 294 | 295 | 'test Simple Route With Two Optional Segments With Both Used': -> 296 | route = router.match('/:controller/:action(/:id)(.:format)', 'GET') 297 | params = router.first('/products/show/1.html', 'GET') 298 | assert.ok params, @fail 299 | assert.equal params.controller, 'products', @fail 300 | assert.equal params.action, 'show', @fail 301 | assert.equal params.id, 1, @fail 302 | assert.equal params.format, 'html', @fail 303 | assert.equal params.method, 'GET', @fail 304 | bench -> 305 | router.first '/products/show/1.html', 'GET' 306 | 307 | 308 | 'test GET': -> 309 | route = router.match('/:controller/:action(/:id)(.:format)', 'GET') 310 | params = router.first('/products/show/1.html', 'GET') 311 | assert.ok params, @fail 312 | assert.equal params.method, 'GET', @fail 313 | 314 | 'test POST': -> 315 | route = router.match('/:controller/:action(/:id)(.:format)', 'POST') 316 | params = router.first('/products/show/1.html', 'POST') 317 | assert.ok params, @fail 318 | assert.equal params.method, 'POST', @fail 319 | 320 | 'test PUT': -> 321 | route = router.match('/:controller/:action(/:id)(.:format)', 'PUT') 322 | params = router.first('/products/show/1.html', 'PUT') 323 | assert.ok params, @fail 324 | assert.equal params.method, 'PUT', @fail 325 | 326 | 'test PATCH': -> 327 | route = router.match('/:controller/:action(/:id)(.:format)', 'PATCH') 328 | params = router.first('/products/show/1.html', 'PATCH') 329 | assert.ok params, @fail 330 | assert.equal params.method, 'PATCH', @fail 331 | 332 | 'test DELETE': -> 333 | route = router.match('/:controller/:action(/:id)(.:format)', 'DELETE') 334 | params = router.first('/products/show/1.html', 'DELETE') 335 | assert.ok params, @fail 336 | assert.equal params.method, 'DELETE', @fail 337 | 338 | 'test OPTIONS': -> 339 | route = router.match('/:controller/:action(/:id)(.:format)', 'OPTIONS') 340 | params = router.first('/products/show/1.html', 'OPTIONS') 341 | assert.ok params, @fail 342 | assert.equal params.method, 'OPTIONS', @fail 343 | 344 | 'test GET Shorthand': -> 345 | route = router.get('/:controller/:action(/:id)(.:format)') 346 | params = router.first('/products/show/1.html', 'GET') 347 | assert.ok params, @fail 348 | assert.equal params.method, 'GET', @fail 349 | 350 | 'test POST Shorthand': -> 351 | route = router.post('/:controller/:action(/:id)(.:format)') 352 | params = router.first('/products/show/1.html', 'POST') 353 | assert.ok params, @fail 354 | assert.equal params.method, 'POST', @fail 355 | 356 | 'test PUT Shorthand': -> 357 | route = router.put('/:controller/:action(/:id)(.:format)') 358 | params = router.first('/products/show/1.html', 'PUT') 359 | assert.ok params, @fail 360 | assert.equal params.method, 'PUT', @fail 361 | 362 | 'test PATCH Shorthand': -> 363 | route = router.patch('/:controller/:action(/:id)(.:format)') 364 | params = router.first('/products/show/1.html', 'PATCH') 365 | assert.ok params, @fail 366 | assert.equal params.method, 'PATCH', @fail 367 | 368 | 'test DELETE Shorthand': -> 369 | route = router.del('/:controller/:action(/:id)(.:format)') 370 | params = router.first('/products/show/1.html', 'DELETE') 371 | assert.ok params, @fail 372 | assert.equal params.method, 'DELETE', @fail 373 | assert.equal params.action, 'show', @fail 374 | 375 | 'test OPTIONS Shorthand': -> 376 | route = router.options('/:controller/:action(/:id)(.:format)') 377 | params = router.first('/products/show/1.html', 'OPTIONS') 378 | assert.ok params, @fail 379 | assert.equal params.method, 'OPTIONS', @fail 380 | assert.equal params.action, 'show', @fail 381 | 382 | 'test Resource Matches': -> 383 | routes = router.resource('snow_dogs') 384 | # index 385 | assert.ok router.first('/snow_dogs', 'GET'), @fail 386 | assert.ok router.first('/snow_dogs.html', 'GET'), @fail 387 | assert.equal router.first('/snow_dogs', 'GET').action, 'index', @fail 388 | # show 389 | assert.ok router.first('/snow_dogs/1', 'GET'), @fail 390 | assert.ok router.first('/snow_dogs/1.html', 'GET'), @fail 391 | assert.equal router.first('/snow_dogs/1', 'GET').action, 'show', @fail 392 | # add form 393 | assert.ok router.first('/snow_dogs/add', 'GET'), @fail 394 | assert.ok router.first('/snow_dogs/add.html', 'GET'), @fail 395 | assert.equal router.first('/snow_dogs/add', 'GET').action, 'add', @fail 396 | # edit form 397 | assert.ok router.first('/snow_dogs/1/edit', 'GET'), @fail 398 | assert.ok router.first('/snow_dogs/1/edit.html', 'GET'), @fail 399 | assert.equal router.first('/snow_dogs/1/edit', 'GET').action, 'edit', @fail 400 | # create 401 | assert.ok router.first('/snow_dogs', 'POST'), @fail 402 | assert.ok router.first('/snow_dogs.html', 'POST'), @fail 403 | assert.equal router.first('/snow_dogs', 'POST').action, 'create', @fail 404 | # update 405 | assert.ok router.first('/snow_dogs/1', 'PUT'), @fail 406 | assert.ok router.first('/snow_dogs/1.html', 'PUT'), @fail 407 | assert.equal router.first('/snow_dogs/1', 'PUT').action, 'update', @fail 408 | # delete 409 | assert.ok router.first('/snow_dogs/1', 'DELETE'), @fail 410 | assert.ok router.first('/snow_dogs/1.html', 'DELETE'), @fail 411 | assert.equal router.first('/snow_dogs/1', 'DELETE').action, 'destroy', @fail 412 | 413 | 'test Resource Url Generation': -> 414 | routes = router.resource('snow_dogs').routes 415 | # index 416 | assert.equal router.url( 417 | controller: 'snow_dogs' 418 | action: 'index'), '/snow_dogs', @fail 419 | assert.equal router.url( 420 | controller: 'snow_dogs' 421 | action: 'index' 422 | format: 'html'), '/snow_dogs.html', @fail 423 | assert.equal router.url( 424 | controller: 'snow_dogs' 425 | action: 'index' 426 | format: 'json'), '/snow_dogs.json', @fail 427 | # show 428 | assert.equal router.url( 429 | controller: 'snow_dogs' 430 | action: 'show' 431 | id: 1), '/snow_dogs/1', @fail 432 | assert.equal router.url( 433 | controller: 'snow_dogs' 434 | action: 'show' 435 | id: 1 436 | format: 'html'), '/snow_dogs/1.html', @fail 437 | assert.equal router.url( 438 | controller: 'snow_dogs' 439 | action: 'show' 440 | id: 1 441 | format: 'json'), '/snow_dogs/1.json', @fail 442 | # add form 443 | assert.equal router.url( 444 | controller: 'snow_dogs' 445 | action: 'add'), '/snow_dogs/add', @fail 446 | assert.equal router.url( 447 | controller: 'snow_dogs' 448 | action: 'add' 449 | format: 'html'), '/snow_dogs/add.html', @fail 450 | assert.equal router.url( 451 | controller: 'snow_dogs' 452 | action: 'add' 453 | format: 'json'), '/snow_dogs/add.json', @fail 454 | # edit form 455 | assert.equal router.url( 456 | controller: 'snow_dogs' 457 | action: 'edit' 458 | id: 1), '/snow_dogs/1/edit', @fail 459 | assert.equal router.url( 460 | controller: 'snow_dogs' 461 | action: 'edit' 462 | id: 1 463 | format: 'html'), '/snow_dogs/1/edit.html', @fail 464 | assert.equal router.url( 465 | controller: 'snow_dogs' 466 | action: 'edit' 467 | id: 1 468 | format: 'json'), '/snow_dogs/1/edit.json', @fail 469 | # create 470 | assert.equal router.url( 471 | controller: 'snow_dogs' 472 | action: 'create'), '/snow_dogs', @fail 473 | assert.equal router.url( 474 | controller: 'snow_dogs' 475 | action: 'create' 476 | format: 'html'), '/snow_dogs.html', @fail 477 | assert.equal router.url( 478 | controller: 'snow_dogs' 479 | action: 'create' 480 | format: 'json'), '/snow_dogs.json', @fail 481 | # update 482 | assert.equal router.url( 483 | controller: 'snow_dogs' 484 | action: 'update' 485 | id: 1), '/snow_dogs/1', @fail 486 | assert.equal router.url( 487 | controller: 'snow_dogs' 488 | action: 'update' 489 | id: 1 490 | format: 'html'), '/snow_dogs/1.html', @fail 491 | assert.equal router.url( 492 | controller: 'snow_dogs' 493 | action: 'update' 494 | id: 1 495 | format: 'json'), '/snow_dogs/1.json', @fail 496 | # delete 497 | assert.equal router.url( 498 | controller: 'snow_dogs' 499 | action: 'destroy' 500 | id: 1), '/snow_dogs/1', @fail 501 | assert.equal router.url( 502 | controller: 'snow_dogs' 503 | action: 'destroy' 504 | id: 1 505 | format: 'html'), '/snow_dogs/1.html', @fail 506 | assert.equal router.url( 507 | controller: 'snow_dogs' 508 | action: 'destroy' 509 | id: 1 510 | format: 'json'), '/snow_dogs/1.json', @fail 511 | bench -> 512 | router.url 513 | controller: 'snow_dogs' 514 | action: 'destroy' 515 | id: 1 516 | format: 'json' 517 | 518 | 519 | 'test Route Url Generation': -> 520 | route = router.match('/:controller/:action(/:id)(.:format)') 521 | assert.equal router.url( 522 | controller: 'snow_dogs' 523 | action: 'pet'), '/snow_dogs/pet', @fail 524 | assert.equal router.url( 525 | controller: 'snow_dogs' 526 | action: 'pet' 527 | id: 5), '/snow_dogs/pet/5', @fail 528 | assert.equal router.url( 529 | controller: 'snow_dogs' 530 | action: 'pet' 531 | id: 5 532 | format: 'html'), '/snow_dogs/pet/5.html', @fail 533 | assert.equal router.url( 534 | controller: 'snow_dogs' 535 | action: 'pet' 536 | id: 5 537 | format: 'json'), '/snow_dogs/pet/5.json', @fail 538 | assert.equal router.url( 539 | controller: 'snow_dogs' 540 | action: 'pet' 541 | format: 'html'), '/snow_dogs/pet.html', @fail 542 | bench -> 543 | router.url 544 | controller: 'snow_dogs' 545 | action: 'pet' 546 | id: 5 547 | format: 'html' 548 | 549 | 550 | 'test Route Url Generates Route With QueryString Params': -> 551 | route = router.match('/:controller/:action(/:id)(.:format)') 552 | # test with QS params ON 553 | assert.equal router.url({ 554 | controller: 'snow_dogs' 555 | action: 'pet' 556 | awesome: 'yes' 557 | }, true), '/snow_dogs/pet?awesome=yes', @fail 558 | 559 | 'test Route Url Generates Route Without QueryString Params': -> 560 | route = router.match('/:controller/:action(/:id)(.:format)') 561 | # test with QS params OFF (default behaviour) 562 | assert.equal router.url({ 563 | controller: 'snow_dogs' 564 | action: 'pet' 565 | awesome: 'yes' 566 | }, false), '/snow_dogs/pet', @fail 567 | 568 | 'test Creating a route without a string path will throw an error': -> 569 | assert.throws -> 570 | route = router.match(5) 571 | , /path must be a string/, @fail 572 | assert.throws -> 573 | route = router.match(/bob/) 574 | , /path must be a string/, @fail 575 | assert.throws -> 576 | route = router.match({}) 577 | , /path must be a string/, @fail 578 | 579 | 'test A route with a glob': -> 580 | route = router.match('/timezones/*tzname').to( 581 | controller: 'Timezones' 582 | action: 'select') 583 | params = router.first('/timezones/America/New_York', 'GET') 584 | expectedParams = 585 | method: 'GET' 586 | controller: 'Timezones' 587 | action: 'select' 588 | tzname: 'America/New_York' 589 | # tests both parsing & generation 590 | assert.equal router.url(params), '/timezones/America/New_York', @fail 591 | assert.equal router.url(expectedParams), '/timezones/America/New_York', @fail 592 | 593 | 'test A route with a glob and a format': -> 594 | route = router.match('/timezones/*tzname(.:format)').to( 595 | controller: 'Timezones' 596 | action: 'select') 597 | params = router.first('/timezones/America/New_York.json', 'GET') 598 | expectedParams = 599 | method: 'GET' 600 | controller: 'Timezones' 601 | action: 'select' 602 | tzname: 'America/New_York' 603 | format: 'json' 604 | # tests both parsing & generation 605 | assert.equal router.url(params), '/timezones/America/New_York.json', @fail 606 | assert.equal router.url(expectedParams), '/timezones/America/New_York.json', @fail 607 | 608 | 'test A route with 2 globs': -> 609 | route = router.match('/*tzname_one/to/*tzname_two').to( 610 | controller: 'Timezones' 611 | action: 'between') 612 | params = router.first('/America/Toronto/to/America/San_Francisco', 'GET') 613 | expectedParams = 614 | method: 'GET' 615 | controller: 'Timezones' 616 | action: 'between' 617 | tzname_one: 'America/Toronto' 618 | tzname_two: 'America/San_Francisco' 619 | # tests both parsing & generation 620 | assert.equal router.url(params), '/America/Toronto/to/America/San_Francisco', @fail 621 | assert.equal router.url(expectedParams), '/America/Toronto/to/America/San_Francisco', @fail 622 | 623 | 'test A route with 2 globs and a format': -> 624 | route = router.match('/*tzname_one/to/*tzname_two(.:format)').to( 625 | controller: 'Timezones' 626 | action: 'between') 627 | params = router.first('/America/Toronto/to/America/San_Francisco.json', 'GET') 628 | expectedParams = 629 | method: 'GET' 630 | controller: 'Timezones' 631 | action: 'between' 632 | tzname_one: 'America/Toronto' 633 | tzname_two: 'America/San_Francisco' 634 | format: 'json' 635 | # tests both parsing & generation 636 | assert.equal router.url(params), '/America/Toronto/to/America/San_Francisco.json', @fail 637 | assert.equal router.url(expectedParams), '/America/Toronto/to/America/San_Francisco.json', @fail 638 | 639 | 'test A catch-all path': -> 640 | route = router.match('/*path(.:format)').to( 641 | controller: 'Errors' 642 | action: 'notFound') 643 | params = router.first('/One/Two/three/four/Five.json', 'GET') 644 | expectedParams = 645 | method: 'GET' 646 | controller: 'Errors' 647 | action: 'notFound' 648 | path: 'One/Two/three/four/Five' 649 | format: 'json' 650 | # tests both parsing & generation 651 | assert.equal router.url(params), '/One/Two/three/four/Five.json', @fail 652 | assert.equal router.url(expectedParams), '/One/Two/three/four/Five.json', @fail 653 | 654 | 'test finding all matching routes': -> 655 | routes = router.resource('snow_dogs').routes 656 | # assert.equal( 2, 2); 657 | # console.log( JSON.stringify(router.routes,null,2) ) 658 | # console.log( JSON.stringify( router.all('/snow_dogs'), null, 2 ) ) 659 | # assert.equal( router.all('/snow_dogs').length, 2); 660 | 661 | 'test A resource with member routes': -> 662 | route = router.resource('posts').member(-> 663 | @get('/print').to 'Posts.print' 664 | 665 | ) 666 | params = router.first('/posts/5/print', 'GET') 667 | expectedParams = 668 | method: 'GET' 669 | controller: 'Posts' 670 | action: 'print' 671 | post_id: 5 672 | # tests both parsing & generation 673 | assert.equal router.url(params), '/posts/5/print', @fail 674 | assert.equal router.url(expectedParams), '/posts/5/print', @fail 675 | 676 | 'test A resource with collection routes': -> 677 | route = router.resource('posts').collection(-> 678 | @get('/print').to 'Posts.printAll' 679 | 680 | ) 681 | params = router.first('/posts/print', 'GET') 682 | expectedParams = 683 | method: 'GET' 684 | controller: 'Posts' 685 | action: 'printAll' 686 | # tests both parsing & generation 687 | assert.equal router.url(params), '/posts/print', @fail 688 | assert.equal router.url(expectedParams), '/posts/print', @fail 689 | 690 | 'test A resource with member routes with optional segments': -> 691 | route = router.resource('Posts').member(-> 692 | @get('/print(.:format)').to 'Posts.printAll' 693 | 694 | ) 695 | params = router.first('/posts/5/print.pdf', 'GET') 696 | expectedParams = 697 | method: 'GET' 698 | controller: 'Posts' 699 | action: 'printAll' 700 | format: 'pdf' 701 | post_id: 5 702 | # tests both parsing & generation 703 | assert.equal router.url(params), '/posts/5/print.pdf', @fail 704 | assert.equal router.url(expectedParams), '/posts/5/print.pdf', @fail 705 | 706 | 'test A resource with member resource': -> 707 | route = router.resource('Posts').member(-> 708 | @resource 'Comments' 709 | 710 | ) 711 | params = router.first('/posts/5/comments/3', 'GET') 712 | expectedParams = 713 | method: 'GET' 714 | controller: 'Comments' 715 | action: 'show' 716 | post_id: 5 717 | id: 3 718 | # tests both parsing & generation 719 | assert.equal router.url(params), '/posts/5/comments/3', @fail 720 | assert.equal router.url(expectedParams), '/posts/5/comments/3', @fail 721 | 722 | 'test A resource with collection resource': -> 723 | route = router.resource('Posts').collection(-> 724 | @resource 'Comments' 725 | 726 | ) 727 | params = router.first('/posts/comments', 'GET') 728 | expectedParams = 729 | method: 'GET' 730 | controller: 'Comments' 731 | action: 'index' 732 | # tests both parsing & generation 733 | assert.equal router.url(params), '/posts/comments', @fail 734 | assert.equal router.url(expectedParams), '/posts/comments', @fail 735 | 736 | 'test A HEAD request should resolve to GET': -> 737 | route = router.get('/something').to('App.index') 738 | params = router.first('/something', 'HEAD') 739 | expectedParams = 740 | method: 'HEAD' 741 | controller: 'App' 742 | action: 'index' 743 | # tests both parsing & generation 744 | assert.equal router.url(params), '/something', @fail 745 | assert.equal router.url(expectedParams), '/something', @fail 746 | 747 | 'test A HEAD request should not resolve to not-GET': -> 748 | route = router.post('/something').to('App.index') 749 | params = router.first('/something', 'HEAD') 750 | assert.equal params, false, @fail 751 | 752 | 'test Nesting: Route -> Route': -> 753 | route = router.post('/something').to('App.index').nest(-> 754 | @get('/something_else').to 'App.somewhere' 755 | 756 | ) 757 | params = router.first('/something/something_else', 'GET') 758 | expectedParams = 759 | method: 'GET' 760 | controller: 'App' 761 | action: 'somewhere' 762 | assert.equal router.url(params), '/something/something_else', @fail 763 | assert.equal router.url(expectedParams), '/something/something_else', @fail 764 | 765 | 'test Nesting: Resource -> Route': -> 766 | route = router.resource('Posts').nest(-> 767 | @get('/print(.:format)').to 'Posts.print' 768 | 769 | ) 770 | url = '/posts/5/print.pdf' 771 | params = router.first(url, 'GET') 772 | expectedParams = 773 | method: 'GET' 774 | controller: 'Posts' 775 | action: 'print' 776 | format: 'pdf' 777 | post_id: 5 778 | assert.equal router.url(params), url, @fail 779 | assert.equal router.url(expectedParams), url, @fail 780 | 781 | 'test Nesting: Resource -> Resource': -> 782 | res = router.resource('Posts').nest(-> 783 | @resource 'Comments' 784 | 785 | ) 786 | url = '/posts/5/comments' 787 | params = router.first(url, 'GET') 788 | expectedParams = 789 | method: 'GET' 790 | controller: 'Comments' 791 | action: 'index' 792 | post_id: '5' 793 | assert.equal router.url(params), url, @fail 794 | assert.equal router.url(expectedParams), url, @fail 795 | 796 | 'test Simple Remove': -> 797 | #Start by repeating the simple route test, to make sure nothing was broken by using name 798 | route = router.match('/:controller/:action/:id').as('delete_me') 799 | params = router.first('/products/show/1', 'GET') 800 | assert.ok params, @fail 801 | assert.equal params.controller, 'products', @fail 802 | assert.equal params.action, 'show', @fail 803 | assert.equal params.id, 1, @fail 804 | assert.equal params.method, 'GET', @fail 805 | #Remove the route 806 | router.remove 'delete_me' 807 | params = router.first('/products/show/1', 'GET') 808 | assert.equal params, false, @fail 809 | bench -> 810 | router.first '/products/show/1', 'GET' 811 | 812 | 813 | 'test Remove With Multiple Routes': -> 814 | `var route` 815 | #Start by repeating the simple route test, to make sure nothing was broken by using name 816 | route = router.match('/:controller/:action/:id').as('delete_me') 817 | route = router.match('/a/:controller/:action/:id').as('do_not_delete_me') 818 | params = router.first('/products/show/1', 'GET') 819 | assert.ok params, @fail 820 | assert.equal params.controller, 'products', @fail 821 | assert.equal params.action, 'show', @fail 822 | assert.equal params.id, 1, @fail 823 | assert.equal params.method, 'GET', @fail 824 | params = router.first('/a/products/show/1', 'GET') 825 | assert.ok params, @fail 826 | assert.equal params.controller, 'products', @fail 827 | assert.equal params.action, 'show', @fail 828 | assert.equal params.id, 1, @fail 829 | assert.equal params.method, 'GET', @fail 830 | #Remove the route 831 | router.remove 'delete_me' 832 | params = router.first('/products/show/1', 'GET') 833 | assert.equal params, false, @fail 834 | params = router.first('/a/products/show/1', 'GET') 835 | assert.ok params, @fail 836 | assert.equal params.controller, 'products', @fail 837 | assert.equal params.action, 'show', @fail 838 | assert.equal params.id, 1, @fail 839 | assert.equal params.method, 'GET', @fail 840 | bench -> 841 | router.first '/products/show/1', 'GET' 842 | 843 | 844 | 'test Bad Remove does no damage and fails to remove good route': -> 845 | #Same as the simple route test, with the invalid delete in the middle 846 | route = router.match('/:controller/:action/:id').as('do_not_delete_me') 847 | router.remove 'delete_me' 848 | params = router.first('/products/show/1', 'GET') 849 | assert.ok params, @fail 850 | assert.equal params.controller, 'products', @fail 851 | assert.equal params.action, 'show', @fail 852 | assert.equal params.id, 1, @fail 853 | assert.equal params.method, 'GET', @fail 854 | bench -> 855 | router.first '/products/show/1', 'GET' 856 | 857 | 858 | 'test A route with URI encoded params': -> 859 | route = router.match('/something_with/:weird_shit').to( 860 | controller: 'Wat' 861 | action: 'lol') 862 | params = router.first('/something_with/cray cray param', 'GET') 863 | expectedParams = 864 | method: 'GET' 865 | controller: 'Wat' 866 | action: 'lol' 867 | weird_shit: 'cray cray param' 868 | # tests both parsing & generation 869 | assert.equal router.url(params), '/something_with/cray cray param', @fail 870 | assert.equal router.url(expectedParams), '/something_with/cray cray param', @fail 871 | 872 | 'test A route with base64 encoded params': -> 873 | b64_regex = /[\w\-\/+]+={0,2}/ 874 | route = router.match('/something_with/:weird_shit').to( 875 | controller: 'Wat' 876 | action: 'lol').where(weird_shit: b64_regex) 877 | test_url = '/something_with/R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=' 878 | params = router.first(test_url, 'GET') 879 | expectedParams = 880 | method: 'GET' 881 | controller: 'Wat' 882 | action: 'lol' 883 | weird_shit: 'R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=' 884 | # tests both parsing & generation 885 | assert.equal router.url(params), test_url, @fail 886 | assert.equal router.url(expectedParams), test_url, @fail 887 | 888 | 'test all with a simple route': -> 889 | # with thanks to @larzconwell 890 | route1 = router.match('/all_routes/test').to( 891 | controller: 'All' 892 | action: 'test') 893 | route2 = router.match('/*whatever_path').to( 894 | controller: 'All' 895 | action: 'test2') 896 | routes = router.all('/all_routes/test', 'GET') 897 | expectedRoutes = [ 898 | { 899 | method: 'GET' 900 | controller: 'All' 901 | action: 'test' 902 | } 903 | { 904 | method: 'GET' 905 | controller: 'All' 906 | action: 'test2' 907 | whatever_path: 'all_routes/test' 908 | } 909 | ] 910 | assert.deepEqual routes, expectedRoutes 911 | 912 | 'test Route With Regex Reqs and periods in the var': -> 913 | route = router.get('/sites/:id/edit').to('sites.edit').where(id: /[\w\-\s.]+/) 914 | params = router.first('/sites/site.ru/edit', 'GET') 915 | expectedParams = 916 | method: 'GET' 917 | controller: 'sites' 918 | action: 'edit' 919 | id: 'site.ru' 920 | assert.equal router.url(params), '/sites/site.ru/edit', @fail 921 | assert.equal router.url(expectedParams), '/sites/site.ru/edit', @fail 922 | 923 | 'test nested admin route works': -> 924 | admin_ns = router.match('/admin').to('errors.index').nest(-> 925 | @resource 'Posts' 926 | 927 | ) 928 | params = router.first('/admin/posts/456', 'GET') 929 | expectedParams = 930 | method: 'GET' 931 | controller: 'Posts' 932 | action: 'show' 933 | id: 456 934 | assert.equal router.url(params), '/admin/posts/456', @fail 935 | assert.equal router.url(expectedParams), '/admin/posts/456', @fail 936 | 937 | 'test nested optional parts 1': -> 938 | route = router.match('/:controller(/:action(/:id))(.:format)', 'GET') 939 | url = '/products/show/1.pdf' 940 | params = router.first(url, 'GET') 941 | expectedParams = 942 | method: 'GET' 943 | controller: 'products' 944 | action: 'show' 945 | id: 1 946 | format: 'pdf' 947 | assert.equal router.url(params), url, @fail 948 | assert.equal router.url(expectedParams), url, @fail 949 | 950 | 'test nested optional parts 2': -> 951 | route = router.match('/:controller(/:action(/:id))(.:format)', 'GET') 952 | url = '/products/show' 953 | params = router.first(url, 'GET') 954 | expectedParams = 955 | method: 'GET' 956 | controller: 'products' 957 | action: 'show' 958 | assert.equal router.url(params), url, @fail 959 | assert.equal router.url(expectedParams), url, @fail 960 | 961 | 'test nested optional parts 3': -> 962 | route = router.match('/:controller(/:action(/:id))(.:format)', 'GET') 963 | url = '/products' 964 | params = router.first(url, 'GET') 965 | expectedParams = 966 | method: 'GET' 967 | controller: 'products' 968 | assert.equal router.url(params), url, @fail 969 | assert.equal router.url(expectedParams), url, @fail 970 | 971 | 'test nested optional parts 4': -> 972 | route = router.match('/:controller(/:action(/:id))(.:format)', 'GET') 973 | url = '/products/show.pdf' 974 | params = router.first(url, 'GET') 975 | expectedParams = 976 | method: 'GET' 977 | controller: 'products' 978 | action: 'show' 979 | format: 'pdf' 980 | assert.equal router.url(params), url, @fail 981 | assert.equal router.url(expectedParams), url, @fail 982 | 983 | 'test nested optional parts 5': -> 984 | route = router.match('/:controller(/:action(/:id))(.:format)', 'GET') 985 | url = '/products.pdf' 986 | params = router.first(url, 'GET') 987 | expectedParams = 988 | method: 'GET' 989 | controller: 'products' 990 | format: 'pdf' 991 | assert.equal router.url(params), url, @fail 992 | assert.equal router.url(expectedParams), url, @fail 993 | 994 | 'test find by email': -> 995 | route = router.match('/users/find_by_email/:email', 'GET').where(email: /[\w.@]+?/).to('users.find_by_email') 996 | url = '/users/find_by_email/kieran@kieran.ca' 997 | params = router.first(url, 'GET') 998 | expectedParams = 999 | method: 'GET' 1000 | controller: 'users' 1001 | action: 'find_by_email' 1002 | email: 'kieran@kieran.ca' 1003 | assert.equal router.url(params), url, @fail 1004 | assert.equal router.url(expectedParams), url, @fail 1005 | 1006 | 'test find by email with sub-route': -> 1007 | route = router.match('/users/find_by_email/:email/favourites', 'GET').where(email: /[\w.@]+?/).to('users.favourites') 1008 | url = '/users/find_by_email/kieran@kieran.ca/favourites' 1009 | params = router.first(url, 'GET') 1010 | expectedParams = 1011 | method: 'GET' 1012 | controller: 'users' 1013 | action: 'favourites' 1014 | email: 'kieran@kieran.ca' 1015 | assert.equal router.url(params), url, @fail 1016 | assert.equal router.url(expectedParams), url, @fail 1017 | 1018 | 'test find by email with nested sub-route preserves where conditions': -> 1019 | route = router.match('/users/find_by_email/:email', 'GET').where(email: /[\w.@]+?/).to('users.find_by_email').nest(-> 1020 | @get('/favourites').to 'users.favourites' 1021 | 1022 | ) 1023 | url = '/users/find_by_email/kieran@kieran.ca/favourites' 1024 | params = router.first(url, 'GET') 1025 | expectedParams = 1026 | method: 'GET' 1027 | controller: 'users' 1028 | action: 'favourites' 1029 | email: 'kieran@kieran.ca' 1030 | assert.equal router.url(params), url, @fail 1031 | assert.equal router.url(expectedParams), url, @fail 1032 | 1033 | # Run tests -- additionally setting up custom failure message and calling setup() and teardown() 1034 | for e, test of RouterTests 1035 | if e.match /test/ 1036 | RouterTests.fail = "FAILED :: #{e}" 1037 | try 1038 | RouterTests.setup() 1039 | test() 1040 | RouterTests.teardown e 1041 | catch e 1042 | console.log RouterTests.fail 1043 | console.log e 1044 | --------------------------------------------------------------------------------