├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── benchmarks ├── gen2.rb ├── generation_bm.rb ├── rack_mount.rb ├── rack_recognition_bm.rb ├── rec2.rb └── recognition_bm.rb ├── examples ├── glob.ru ├── rack_mapper.ru ├── simple.ru ├── static │ ├── config.ru │ ├── favicon.ico │ └── images │ │ ├── cat1.jpg │ │ ├── cat2.jpg │ │ └── cat3.jpg ├── variable.ru └── variable_with_regex.ru ├── http_router.gemspec ├── js ├── lib │ ├── http_router.coffee │ └── http_router.js ├── package.json └── test │ ├── test.coffee │ └── test.js ├── lib ├── http_router.rb └── http_router │ ├── generation_helper.rb │ ├── generator.rb │ ├── node.rb │ ├── node │ ├── abstract_request_node.rb │ ├── free_regex.rb │ ├── glob.rb │ ├── glob_regex.rb │ ├── host.rb │ ├── lookup.rb │ ├── path.rb │ ├── regex.rb │ ├── request_method.rb │ ├── root.rb │ ├── scheme.rb │ ├── spanning_regex.rb │ ├── user_agent.rb │ └── variable.rb │ ├── regex_route_generation.rb │ ├── request.rb │ ├── response.rb │ ├── route.rb │ ├── route_helper.rb │ ├── util.rb │ └── version.rb └── test ├── common ├── generate.txt ├── http_recognize.txt └── recognize.txt ├── generation.rb ├── generic.rb ├── helper.rb ├── rack └── test_route.rb ├── recognition.rb ├── test_misc.rb ├── test_mounting.rb ├── test_recognition.rb └── test_trailing_slash.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | *.gem 3 | .bundle 4 | rdoc 5 | Gemfile.lock 6 | .rvmrc 7 | *.rbc 8 | js/npm-debug.log -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Joshua Hull, http://github.com/joshbuddy/http_router 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTTP Router 2 | 3 | ## What is it? 4 | 5 | This is an HTTP router for use in either a web framework, or on it's own using Rack. It takes a set of routes and attempts to find the best match for it. Take a look at the examples directory for how you'd use it in the Rack context. 6 | 7 | ## Features 8 | 9 | * Ordered route resolution. 10 | * Supports variables, and globbing, both named and unnamed. 11 | * Regex support for variables. 12 | * Request condition support. 13 | * Partial matches. 14 | * Supports interstitial variables (e.g. /my-:variable-brings.all.the.boys/yard) and unnamed variable /one/:/two 15 | * Very fast and small code base (~1,000 loc). 16 | * Sinatra via https://github.com/joshbuddy/http_router_sinatra 17 | 18 | ## Usage 19 | 20 | Please see the examples directory for a bunch of awesome rackup file examples, with tonnes of commentary. As well, the rdocs should provide a lot of useful specifics and exact usage. 21 | 22 | ### `HttpRouter.new` 23 | 24 | Takes the following options: 25 | 26 | * `:default_app` - The default #call made on non-matches. Defaults to a 404 generator. 27 | * `:ignore_trailing_slash` - Ignores the trailing slash when matching. Defaults to true. 28 | * `:middleware` - Perform matching without deferring to matched route. Defaults to false. 29 | 30 | ### `#add(name, options)` 31 | 32 | Maps a route. The format for variables in paths is: 33 | :variable 34 | *glob 35 | 36 | Everything else is treated literally. Optional parts are surrounded by brackets. Partially matching paths have a trailing `*`. Optional trailing slash matching is done with `/?`. 37 | 38 | As well, you can escape the following characters with a backslash: `( ) : *` 39 | 40 | Once you have a route object, use `HttpRouter::Route#to` to add a destination and `HttpRouter::Route#name` to name it. 41 | 42 | e.g. 43 | 44 | ```ruby 45 | r = HttpRouter.new 46 | r.add('/test/:variable(.:format)').name(:my_test_path).to {|env| [200, {}, "Hey dude #{env['router.params'][:variable]}"]} 47 | r.add('/test').redirect("http://www.google.com/") 48 | r.add('/static').static('/my_file_system') 49 | ``` 50 | 51 | As well, you can support regex matching and request conditions. To add a regex match, use `matching(:id => /\d+/)`. 52 | To match on a request condition you can use `condition(:request_method => %w(POST HEAD))` or more succinctly `request_method('POST', 'HEAD')`. 53 | 54 | There are convenience methods HttpRouter#get, HttpRouter#post, etc for each request method. 55 | 56 | Routes will not be recognized unless `#to` has been called on it. 57 | 58 | ### `#url(name or route, *args)` 59 | 60 | Generates a route. The args can either be a hash, a list, or a mix of both. 61 | 62 | ### `#call(env)` 63 | 64 | Recognizes and dispatches the request. `env` should be a Hash representing the rack environment. 65 | 66 | ### `#recognize(env)` 67 | 68 | Only performs recognition. `env` should be a Hash representing the rack environment. 69 | 70 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'bundler' 3 | Bundler::GemHelper.install_tasks 4 | # Rake::Task['release'].enhance([:test, :release_js]) FIXME, this just doesn't work. 5 | 6 | task :default => [:test] 7 | 8 | task :release_js do 9 | $: << 'lib' 10 | require 'http_router/version' 11 | File.open('js/package.json', 'w') do |f| 12 | f << <<-EOT 13 | { 14 | "name": "http_router", 15 | "description": "URL routing and generation in js", 16 | "author": "Joshua Hull ", 17 | "version": "#{HttpRouter::VERSION}", 18 | "directories": { 19 | "lib" : "./lib/http_router" 20 | }, 21 | "main": "lib/http_router" 22 | } 23 | EOT 24 | end 25 | sh "cd js && npm publish" 26 | `git commit js/package.json -m'bumped js version'` 27 | end 28 | 29 | test_tasks = ['test:generation', 'test:recognition', 'test:integration', 'test:examples', 'test:rdoc_examples'] 30 | #test_tasks << 'test:js' if `which coffee && which node` && $?.success? 31 | desc "Run all tests" 32 | task :test => test_tasks 33 | 34 | desc "Clean things" 35 | task :clean do 36 | sh 'find . -name "*.rbc" | xargs rm' 37 | sh 'rm -rf pkg' 38 | end 39 | 40 | namespace :test do 41 | desc "Run integration tests" 42 | task :integration do 43 | $: << 'lib' 44 | require 'http_router' 45 | require './test/helper' 46 | Dir['./test/**/test_*.rb'].each { |test| require test } 47 | end 48 | 49 | desc "Run js tests" 50 | task :js do 51 | sh "coffee -c js/test/test.coffee" 52 | sh "coffee -c js/lib/http_router.coffee" 53 | sh "node js/test/test.js" 54 | end 55 | 56 | desc "Run generic recognition tests" 57 | task :recognition do 58 | $: << 'lib' 59 | require 'http_router' 60 | require './test/recognition' 61 | end 62 | 63 | desc "Run generic recognition tests" 64 | task :generation do 65 | $: << 'lib' 66 | require 'http_router' 67 | require './test/generation' 68 | end 69 | 70 | desc "Run example tests" 71 | task :examples do 72 | $: << 'lib' 73 | require 'http_router' 74 | require 'thin' 75 | Dir['./examples/**/*.ru'].each do |example| 76 | print "running example #{example}..." 77 | comments = File.read(example).split(/\n/).select{|l| l[0] == ?#} 78 | pid = nil 79 | Thin::Logging.silent = true 80 | begin 81 | pid = fork { 82 | code = "Proc.new { \n#{File.read(example)}\n }" 83 | r = eval(code, binding, example, 2) 84 | Thin::Server.start(:signals => false, &r) 85 | } 86 | sleep 0.5 87 | out = nil 88 | assertion_count = 0 89 | comments.each do |c| 90 | c.gsub!(/^# ?/, '') 91 | case c 92 | when /^\$/ 93 | out = `#{c[1, c.size]} 2>/dev/null`.split(/\n/) 94 | raise "#{c} produced #{`#{c[1, c.size]} 2>&1`}" unless $?.success? 95 | when /^=> ?(.*)/ 96 | c = $1 97 | raise "out was nil" if out.nil? 98 | test = out.shift 99 | if c['Last-Modified'] == test['Last-Modified'] 100 | assertion_count += 1 101 | else 102 | raise "expected #{c.inspect}, received #{test.inspect}" unless c.strip == test.strip 103 | assertion_count += 1 104 | end 105 | end 106 | end 107 | raise "no assertions were raised in #{example}" if assertion_count.zero? 108 | puts "✔" 109 | ensure 110 | Process.kill('HUP', pid) if pid 111 | end 112 | end 113 | end 114 | desc "rdoc examples" 115 | task :rdoc_examples do 116 | $: << 'lib' 117 | require 'http_router' 118 | in_example = false 119 | examples = [] 120 | STDOUT.sync = true 121 | current_example = '' 122 | rb_files = Dir['./lib/**/*.rb'] 123 | puts "Scanning #{rb_files * ', '}" 124 | rb_files.each do |file| 125 | lines = File.read(file).split(/\n/) 126 | lines.each do |line| 127 | if line[/^\s*#(.*)/] # comment 128 | line = $1.strip 129 | case line 130 | when /^example:/i then in_example = true 131 | when /^(?:# )?=+> (.*)/ 132 | expected = $1.strip 133 | msg = expected.dup 134 | msg << " was expected to be " 135 | msg << "\#{__example_runner.inspect}" 136 | current_example << "raise \"#{msg.gsub('"', '\\"')}\" unless (__example_runner.respond_to?(:strip) ? __example_runner.strip : __example_runner) == #{expected}\n" if in_example 137 | when '' 138 | unless current_example.empty? 139 | examples << current_example 140 | current_example = '' 141 | end 142 | in_example = false 143 | else 144 | current_example << "__example_runner = (" << line << ")\n" if in_example 145 | end 146 | else 147 | unless current_example.empty? 148 | examples << current_example 149 | current_example = '' 150 | end 151 | in_example = false 152 | end 153 | end 154 | end 155 | puts "Running #{examples.size} example#{'s' if examples.size != 1}" 156 | examples.each do |example| 157 | print "." 158 | eval(example) 159 | end 160 | puts " ✔" 161 | end 162 | end 163 | 164 | begin 165 | require 'rake/rdoctask' 166 | rescue 167 | require 'rdoc/task' 168 | end 169 | desc "Generate documentation" 170 | Rake::RDocTask.new do |rd| 171 | rd.main = "README.md" 172 | rd.rdoc_files.include("README.md", "lib/**/*.rb") 173 | rd.rdoc_dir = 'rdoc' 174 | end 175 | 176 | require 'code_stats' 177 | CodeStats::Tasks.new 178 | -------------------------------------------------------------------------------- /benchmarks/gen2.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rbench' 3 | #require 'lib/usher' 4 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 5 | 6 | require 'http_router' 7 | 8 | u = HttpRouter.new 9 | u.add('/simple') .name(:simple).to{} 10 | u.add('/simple/:variable') .name(:one_variable).to{} 11 | u.add('/simple/:var1/:var2/:var3') .name(:three_variables).to{} 12 | u.add('/simple/:v1/:v2/:v3/:v4/:v5/:v6/:v7/:v8') .name(:eight_variables).to{} 13 | u.add('/with_condition/:cond1/:cond2').matches_with(:cond1 => /^\d+$/, :cond2 => /^[a-z]+$/) .name(:two_conditions).to{} 14 | 15 | TIMES = 50_000 16 | 17 | RBench.run(TIMES) do 18 | 19 | group "named" do 20 | report "simple" do 21 | u.url(:simple) 22 | end 23 | 24 | report "one variable (through array)" do 25 | u.url(:one_variable, 'variable') 26 | end 27 | 28 | report "one variable (through hash)" do 29 | u.url(:one_variable, :variable => 'variable') 30 | end 31 | 32 | report "three variable (through array)" do 33 | u.url(:three_variables, 'var1', 'var2', 'var3') 34 | end 35 | 36 | report "three variable (through hash)" do 37 | u.url(:three_variables, :var1 => 'var1', :var2 => 'var2', :var3 => 'var3') 38 | end 39 | 40 | report "eight variable (through array)" do 41 | u.url(:eight_variables, 'var1', 'var2', 'var3', 'var4', 'var5', 'var6', 'var7', 'var8') 42 | end 43 | 44 | report "eight variable (through hash)" do 45 | u.url(:eight_variables, :v1 => 'var1', :v2 => 'var2', :v3 => 'var3', :v4 => 'var4', :v5 => 'var5', :v6 => 'var6', :v7 => 'var7', :v8 => 'var8') 46 | end 47 | 48 | report "three variable + three extras" do 49 | u.url(:three_variables, :var1 => 'var1', :var2 => 'var2', :var3 => 'var3', :var4 => 'var4', :var5 => 'var5', :var6 => 'var6') 50 | end 51 | 52 | report "three variable + five extras" do 53 | u.url(:three_variables, :var1 => 'var1', :var2 => 'var2', :var3 => 'var3', :var4 => 'var4', :var5 => 'var5', :var6 => 'var6', :var7 => 'var7', :var8 => 'var8') 54 | end 55 | end 56 | 57 | end 58 | 59 | puts `ps -o rss= -p #{Process.pid}`.to_i -------------------------------------------------------------------------------- /benchmarks/generation_bm.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rbench' 3 | #require 'lib/usher' 4 | require 'usher' 5 | 6 | u = Usher.new(:generator => Usher::Util::Generators::URL.new) 7 | u.add_route('/simple') .name(:simple) 8 | u.add_route('/simple/:variable') .name(:one_variable) 9 | u.add_route('/simple/:var1/:var2/:var3') .name(:three_variables) 10 | u.add_route('/simple/:v1/:v2/:v3/:v4/:v5/:v6/:v7/:v8') .name(:eight_variables) 11 | u.add_route('/with_condition/:cond1/:cond2', :requirements => {:cond1 => /^\d+$/, :cond2 => /^[a-z]+$/}) .name(:two_conditions) 12 | u.add_route('/with_condition/{:cond1,^\d+$}/{:cond2,^[a-z]+$}') .name(:two_implicit_conditions) 13 | #u.add_route('/blog/:page', :default_values => {:page => 1}) .name(:default_value) 14 | #u.add_route('/blog', :default_values => {:page => 1}) .name(:default_value_not_as_variable) 15 | # 16 | TIMES = 50_000 17 | 18 | RBench.run(TIMES) do 19 | 20 | group "named" do 21 | report "simple" do 22 | u.generator.generate(:simple) 23 | end 24 | 25 | report "one variable (through array)" do 26 | u.generator.generate(:one_variable, 'variable') 27 | end 28 | 29 | report "one variable (through hash)" do 30 | u.generator.generate(:one_variable, :variable => 'variable') 31 | end 32 | 33 | report "three variable (through array)" do 34 | u.generator.generate(:three_variables, ['var1', 'var2', 'var3']) 35 | end 36 | 37 | report "three variable (through hash)" do 38 | u.generator.generate(:three_variables, :var1 => 'var1', :var2 => 'var2', :var3 => 'var3') 39 | end 40 | 41 | report "eight variable (through array)" do 42 | u.generator.generate(:eight_variables, ['var1', 'var2', 'var3', 'var4', 'var5', 'var6', 'var7', 'var8']) 43 | end 44 | 45 | report "eight variable (through hash)" do 46 | u.generator.generate(:eight_variables, :v1 => 'var1', :v2 => 'var2', :v3 => 'var3', :v4 => 'var4', :v5 => 'var5', :v6 => 'var6', :v7 => 'var7', :v8 => 'var8') 47 | end 48 | 49 | report "three variable + three extras" do 50 | u.generator.generate(:three_variables, :var1 => 'var1', :var2 => 'var2', :var3 => 'var3', :var4 => 'var4', :var5 => 'var5', :var6 => 'var6') 51 | end 52 | 53 | report "three variable + five extras" do 54 | u.generator.generate(:three_variables, :var1 => 'var1', :var2 => 'var2', :var3 => 'var3', :var4 => 'var4', :var5 => 'var5', :var6 => 'var6', :var7 => 'var7', :var8 => 'var8') 55 | end 56 | 57 | end 58 | 59 | #group "defaults" do 60 | # report "default variable" do 61 | # u.generator.generate(:default_value) 62 | # end 63 | # 64 | # report "default variable not represented in path" do 65 | # u.generator.generate(:default_value_not_as_variable) 66 | # end 67 | # 68 | # 69 | #end 70 | 71 | 72 | end 73 | puts `ps -o rss= -p #{Process.pid}`.to_i -------------------------------------------------------------------------------- /benchmarks/rack_mount.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rbench' 3 | require 'rack' 4 | require 'rack/mount' 5 | #require '../usher/lib/usher' 6 | $: << 'lib' 7 | require 'http_router' 8 | 9 | set = Rack::Mount::RouteSet.new do |set| 10 | set.add_route(proc{|env| [200, {'Content-type'=>'text/html'}, []]}, {:path => '/simple'}, {}, :simple) 11 | set.add_route(proc{|env| [200, {'Content-type'=>'text/html'}, []]}, {:path => '/simple/again'}, {}, :again) 12 | set.add_route(proc{|env| [200, {'Content-type'=>'text/html'}, []]}, {:path => %r{/simple/(.*?)}}, {}, :more) 13 | end 14 | 15 | #u = Usher::Interface.for(:rack) 16 | #u.add('/simple').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 17 | #u.add('/simple/again').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 18 | #u.add('/dynamic/anything').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 19 | 20 | TIMES = 50_000 21 | 22 | simple_env = Rack::MockRequest.env_for('/simple') 23 | simple2_env = Rack::MockRequest.env_for('/simple/again') 24 | dynamic_env = Rack::MockRequest.env_for('/simple/something') 25 | 26 | 27 | RBench.run(TIMES) do 28 | 29 | report "2 levels, static" do 30 | set.call(simple_env).first == 200 or raise 31 | end 32 | 33 | report "4 levels, static" do 34 | set.call(simple2_env).first == 200 or raise 35 | end 36 | 37 | report "4 levels, static" do 38 | set.call(dynamic_env).first == 200 or raise 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /benchmarks/rack_recognition_bm.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rbench' 3 | require '../usher/lib/usher' 4 | 5 | u = Usher::Interface.for(:rack) 6 | u.add('/simple').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 7 | u.add('/simple/again').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 8 | u.add('/simple/again/and/again').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 9 | u.add('/dynamic/:variable').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 10 | u.add('/rails/:controller/:action/:id').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 11 | u.add('/greedy/{!greed,.*}').to(proc{|env| [200, {'Content-type'=>'text/html'}, []]}) 12 | 13 | TIMES = 50_000 14 | 15 | simple_env = Rack::MockRequest.env_for('/simple') 16 | simple2_env = Rack::MockRequest.env_for('/simple/again') 17 | simple3_env = Rack::MockRequest.env_for('/simple/again/and/again') 18 | simple_and_dynamic_env = Rack::MockRequest.env_for('/dynamic/anything') 19 | simple_and_dynamic_env1 = Rack::MockRequest.env_for('/rails/controller/action/id') 20 | simple_and_dynamic_env2 = Rack::MockRequest.env_for('/greedy/controller/action/id') 21 | 22 | RBench.run(TIMES) do 23 | 24 | report "2 levels, static" do 25 | u.call(simple_env).first == 200 or raise 26 | end 27 | 28 | report "4 levels, static" do 29 | u.call(simple2_env).first == 200 or raise 30 | end 31 | 32 | report "8 levels, static" do 33 | u.call(simple3_env).first == 200 or raise 34 | end 35 | 36 | report "4 levels, 1 dynamic" do 37 | u.call(simple_and_dynamic_env).first == 200 or raise 38 | end 39 | 40 | report "8 levels, 3 dynamic" do 41 | u.call(simple_and_dynamic_env1).first == 200 or raise 42 | end 43 | 44 | report "4 levels, 1 greedy" do 45 | u.call(simple_and_dynamic_env2).first == 200 or raise 46 | end 47 | 48 | end 49 | 50 | puts `ps -o rss= -p #{Process.pid}`.to_i -------------------------------------------------------------------------------- /benchmarks/rec2.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rbench' 3 | 4 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib') 5 | require 'http_router' 6 | 7 | #require 'http_router' 8 | 9 | u = HttpRouter.new 10 | 11 | #puts Benchmark.measure { 12 | # ('aa'..'nn').each do |first| 13 | # ('a'..'n').each do |second| 14 | # u.add("/#{first}/#{second}").to {|env| [200, {'Content-type'=>'text/html'}, []]} 15 | # end 16 | # end 17 | # puts "u.routes.size: #{u.routes.size}" 18 | #} 19 | 20 | u.add('/').to {|env| [200, {'Content-type'=>'text/html'}, []]} 21 | 22 | u.add('/simple').to {|env| [200, {'Content-type'=>'text/html'}, []]} 23 | u.add('/simple/again').to {|env| [200, {'Content-type'=>'text/html'}, []]} 24 | #u.add('/simple/again/and/again').compile.to {|env| [200, {'Content-type'=>'text/html'}, []]} 25 | u.add('/dynamic/:variable').to {|env| [200, {'Content-type'=>'text/html'}, []]} 26 | #u.add('/rails/:controller/:action/:id').compile.to {|env| [200, {'Content-type'=>'text/html'}, []]} 27 | #u.add('/greedy/:greed').matching(:greed => /.*/).compile.to {|env| [200, {'Content-type'=>'text/html'}, []]} 28 | #u.add('/greedy/hey.:greed.html').to {|env| [200, {'Content-type'=>'text/html'}, []]} 29 | 30 | # 31 | TIMES = 50_000 32 | 33 | #simple_env = 34 | #simple2_env = 35 | #simple3_env = Rack::MockRequest.env_for('/simple/again/and/again') 36 | #simple_and_dynamic_env = 37 | #simple_and_dynamic_env1 = Rack::MockRequest.env_for('/rails/controller/action/id') 38 | #simple_and_dynamic_env2 = Rack::MockRequest.env_for('/greedy/controller/action/id') 39 | #simple_and_dynamic_env3 = Rack::MockRequest.env_for('/greedy/hey.hello.html') 40 | u.call(Rack::MockRequest.env_for('/simple')).first == 200 or raise 41 | 5.times { 42 | RBench.run(TIMES) do 43 | 44 | report "1 levels, static" do 45 | u.call(Rack::MockRequest.env_for('/')).first == 200 or raise 46 | end 47 | 48 | report "2 levels, static" do 49 | u.call(Rack::MockRequest.env_for('/simple')).first == 200 or raise 50 | end 51 | 52 | report "3 levels, static" do 53 | u.call(Rack::MockRequest.env_for('/simple/again')).first == 200 or raise 54 | end 55 | 56 | #report "8 levels, static" do 57 | # u.call(simple3_env).first == 200 or raise 58 | #end 59 | 60 | report "1 static, 1 dynamic" do 61 | u.call(Rack::MockRequest.env_for('/dynamic/anything')).first == 200 or raise 62 | end 63 | 64 | #report "8 levels, 3 dynamic" do 65 | # u.call(simple_and_dynamic_env1).first == 200 or raise 66 | #end 67 | # 68 | #report "4 levels, 1 greedy" do 69 | # u.call(simple_and_dynamic_env2).first == 200 or raise 70 | #end 71 | 72 | end 73 | } 74 | puts `ps -o rss= -p #{Process.pid}`.to_i -------------------------------------------------------------------------------- /benchmarks/recognition_bm.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rbench' 3 | require 'usher' 4 | 5 | u = Usher.new(:generator => Usher::Util::Generators::URL.new) 6 | u.add_route('/simple') 7 | u.add_route('/simple/again') 8 | u.add_route('/simple/again/and/again') 9 | u.add_route('/dynamic/:variable') 10 | u.add_route('/rails/:controller/:action/:id') 11 | u.add_route('/greedy/{!greed,.*}') 12 | 13 | TIMES = 50_000 14 | 15 | RBench.run(TIMES) do 16 | 17 | report "2 levels, static" do 18 | u.recognize_path('/simple') 19 | end 20 | 21 | report "4 levels, static" do 22 | u.recognize_path('/simple/again') 23 | end 24 | 25 | report "8 levels, static" do 26 | u.recognize_path('/simple/again/and/again') 27 | end 28 | 29 | report "4 levels, 1 dynamic" do 30 | u.recognize_path('/dynamic/anything') 31 | end 32 | 33 | report "8 levels, 3 dynamic" do 34 | u.recognize_path('/rails/controller/action/id') 35 | end 36 | 37 | report "4 levels, 1 greedy" do 38 | u.recognize_path('/greedy/controller/action/id') 39 | end 40 | 41 | end 42 | -------------------------------------------------------------------------------- /examples/glob.ru: -------------------------------------------------------------------------------- 1 | require 'http_router' 2 | 3 | run HttpRouter.new { 4 | get('/*glob').to { |env| [200, {'Content-type' => 'text/plain'}, ["My glob is\n#{env['router.params'][:glob].map{|v| " * #{v}\n"}.join}"]]} 5 | } 6 | 7 | # $ curl http://127.0.0.1:3000/123/345/123 8 | # => My glob is 9 | # => * 123 10 | # => * 345 11 | # => * 123 -------------------------------------------------------------------------------- /examples/rack_mapper.ru: -------------------------------------------------------------------------------- 1 | require 'http_router' 2 | run HttpRouter.new do 3 | add('/get/:id', :match_with => {:id => /\d+/}) { |env| 4 | [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]}, which is a number\n"]] 5 | } 6 | 7 | # you have post, get, head, put and delete. 8 | post('/get/:id') { |env| 9 | [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]} and you posted!\n"]] 10 | } 11 | 12 | map('/get/:id') { |env| 13 | [200, {'Content-type' => 'text/plain'}, ["My id is #{env['router.params'][:id]}\n"]] 14 | } 15 | end 16 | 17 | # $ curl http://127.0.0.1:3000/get/foo 18 | # => My id is foo 19 | # $ curl -X POST http://127.0.0.1:3000/get/foo 20 | # => My id is foo and you posted! 21 | # $ curl -X POST http://127.0.0.1:3000/get/123 22 | # => My id is 123, which is a number 23 | -------------------------------------------------------------------------------- /examples/simple.ru: -------------------------------------------------------------------------------- 1 | require 'http_router' 2 | 3 | run HttpRouter.new { 4 | get('/hi').to { |env| [200, {'Content-type' => 'text/plain'}, ["hi!\n"]]} 5 | } 6 | 7 | # $ curl http://127.0.0.1:3000/hi 8 | # => hi! 9 | -------------------------------------------------------------------------------- /examples/static/config.ru: -------------------------------------------------------------------------------- 1 | # static serving example 2 | 3 | require 'http_router' 4 | 5 | base = File.expand_path(File.dirname(__FILE__)) 6 | 7 | run HttpRouter.new { 8 | add('/favicon.ico').static("#{base}/favicon.ico") # from a single file 9 | add('/images').static("#{base}/images") # or from a directory 10 | } 11 | 12 | # $ curl -I http://localhost:3000/favicon.ico 13 | # => HTTP/1.1 200 OK 14 | # => Last-Modified: Sat, 26 Mar 2011 18:04:26 GMT 15 | # => Content-Type: image/vnd.microsoft.icon 16 | # => Content-Length: 1150 17 | # => Connection: keep-alive 18 | # => Server: thin 1.2.8 codename Black Keys 19 | # 20 | # $ curl -I http://localhost:3000/images/cat1.jpg 21 | # => HTTP/1.1 200 OK 22 | # => Last-Modified: Sat, 26 Mar 2011 18:04:26 GMT 23 | # => Content-Type: image/jpeg 24 | # => Content-Length: 29817 25 | # => Connection: keep-alive 26 | # => Server: thin 1.2.8 codename Black Keys 27 | -------------------------------------------------------------------------------- /examples/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshbuddy/http_router/defc049bf6fa7fb80c38f605a492a6185fae90b6/examples/static/favicon.ico -------------------------------------------------------------------------------- /examples/static/images/cat1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshbuddy/http_router/defc049bf6fa7fb80c38f605a492a6185fae90b6/examples/static/images/cat1.jpg -------------------------------------------------------------------------------- /examples/static/images/cat2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshbuddy/http_router/defc049bf6fa7fb80c38f605a492a6185fae90b6/examples/static/images/cat2.jpg -------------------------------------------------------------------------------- /examples/static/images/cat3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joshbuddy/http_router/defc049bf6fa7fb80c38f605a492a6185fae90b6/examples/static/images/cat3.jpg -------------------------------------------------------------------------------- /examples/variable.ru: -------------------------------------------------------------------------------- 1 | require 'http_router' 2 | 3 | run HttpRouter.new { 4 | get('/:variable').to { |env| [200, {'Content-type' => 'text/plain'}, ["my variables are\n#{env['router.params'].inspect}\n"]]} 5 | } 6 | 7 | # $ curl http://127.0.0.1:3000/heyguys 8 | # => my variables are 9 | # => {:variable=>"heyguys"} 10 | -------------------------------------------------------------------------------- /examples/variable_with_regex.ru: -------------------------------------------------------------------------------- 1 | require 'http_router' 2 | 3 | run HttpRouter.new { 4 | get('/get/:id', :id => /\d+/).to { |env| [200, {'Content-type' => 'text/plain'}, ["id is #{Integer(env['router.params'][:id]) * 2} * 2\n"]]} 5 | } 6 | 7 | # $ curl http://127.0.0.1:3000/get/123 8 | # => id is 246 * 2 9 | # $ curl http://127.0.0.1:3000/get/asd 10 | # => Your request couldn't be found 11 | -------------------------------------------------------------------------------- /http_router.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | require File.join(File.dirname(__FILE__), 'lib', 'http_router', 'version') 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'http_router' 7 | s.version = HttpRouter::VERSION 8 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 9 | s.authors = ["Joshua Hull"] 10 | s.summary = "A kick-ass HTTP router for use in Rack" 11 | s.description = "This library allows you to recognize and build URLs in a Rack application." 12 | s.email = %q{joshbuddy@gmail.com} 13 | s.extra_rdoc_files = ['README.md', 'LICENSE'] 14 | s.files = `git ls-files`.split("\n") 15 | s.homepage = %q{http://github.com/joshbuddy/http_router} 16 | s.rdoc_options = ["--charset=UTF-8"] 17 | s.require_paths = ["lib"] 18 | s.rubygems_version = %q{1.3.7} 19 | s.test_files = `git ls-files`.split("\n").select{|f| f =~ /^test/} 20 | s.rubyforge_project = 'http_router' 21 | 22 | # dependencies 23 | s.add_runtime_dependency 'rack', '>= 1.0.0' 24 | s.add_runtime_dependency 'url_mount', '~> 0.2.1' 25 | s.add_development_dependency 'minitest', '~> 2.0.0' 26 | s.add_development_dependency 'code_stats' 27 | s.add_development_dependency 'rake', '~> 0.8.7' 28 | s.add_development_dependency 'rbench' 29 | s.add_development_dependency 'json' 30 | s.add_development_dependency 'phocus' 31 | s.add_development_dependency 'bundler' 32 | s.add_development_dependency 'thin', '= 1.2.8' 33 | 34 | if s.respond_to? :specification_version then 35 | current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION 36 | s.specification_version = 3 37 | 38 | if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then 39 | else 40 | end 41 | else 42 | end 43 | end 44 | 45 | -------------------------------------------------------------------------------- /js/lib/http_router.coffee: -------------------------------------------------------------------------------- 1 | root.Sherpa = class Sherpa 2 | constructor: (@callback) -> 3 | @root = new Node() 4 | @routes = {} 5 | match: (httpRequest, httpResponse) -> 6 | request = if (httpRequest.url?) then new Request(httpRequest) else new PathRequest(httpRequest) 7 | @root.match(request) 8 | if request.destinations.length > 0 9 | new Response(request, httpResponse).invoke() 10 | else if @callback? 11 | @callback(request.underlyingRequest) 12 | findSubparts: (part) -> 13 | subparts = [] 14 | while match = part.match(/\\.|[:*][a-z0-9_]+|[^:*\\]+/) 15 | part = part.slice(match.index, part.length) 16 | subparts.push part.slice(0, match[0].length) 17 | part = part.slice(match[0].length, part.length) 18 | subparts 19 | generatePaths: (path) -> 20 | [paths, chars, startIndex, endIndex] = [[''], path.split(''), 0, 1] 21 | for charIndex in [0...chars.length] 22 | c = chars[charIndex] 23 | switch c 24 | when '\\' 25 | # do nothing ... 26 | charIndex++ 27 | add = if chars[charIndex] == ')' or chars[charIndex] == '(' 28 | chars[charIndex] 29 | else 30 | "\\#{chars[charIndex]}" 31 | paths[pathIndex] += add for pathIndex in [startIndex...endIndex] 32 | when '(' 33 | # over current working set, double paths 34 | paths.push(paths[pathIndex]) for pathIndex in [startIndex...endIndex] 35 | # move working set to newly copied paths 36 | startIndex = endIndex 37 | endIndex = paths.length 38 | when ')' 39 | startIndex -= endIndex - startIndex 40 | else 41 | paths[pathIndex] += c for pathIndex in [startIndex...endIndex] 42 | paths.reverse() 43 | paths 44 | url: (name, params) -> 45 | @routes[name]?.url(params) 46 | addComplexPart: (subparts, compiledPath, matchesWith, variableNames) -> 47 | escapeRegexp = (str) -> str.replace(/([\.*+?^=!:${}()|[\]\/\\])/g, '\\$1') 48 | [capturingIndicies, splittingIndicies, captures, spans] = [[], [], 0, false] 49 | regexSubparts = for part in subparts 50 | switch part[0] 51 | when '\\' 52 | compiledPath.push "'#{part[1]}'" 53 | escapeRegexp(part[1]) 54 | when ':', '*' 55 | spans = true if part[0] == '*' 56 | captures += 1 57 | name = part.slice(1, part.length) 58 | variableNames.push(name) 59 | if part[0] == '*' 60 | splittingIndicies.push(captures) 61 | compiledPath.push "params['#{name}'].join('/')" 62 | else 63 | capturingIndicies.push(captures) 64 | compiledPath.push "params['#{name}']" 65 | if spans 66 | if matchesWith[name]? then "((?:#{matchesWith[name].source}\\/?)+)" else '(.*?)' 67 | else 68 | "(#{(matchesWith[name]?.source || '[^/]*?')})" 69 | else 70 | compiledPath.push "'#{part}'" 71 | escapeRegexp(part) 72 | regexp = new RegExp("#{regexSubparts.join('')}$") 73 | if spans 74 | new SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies) 75 | else 76 | new RegexMatcher(regexp, capturingIndicies, splittingIndicies) 77 | addSimplePart: (subparts, compiledPath, matchesWith, variableNames) -> 78 | part = subparts[0] 79 | switch part[0] 80 | when ':' 81 | variableName = part.slice(1, part.length) 82 | compiledPath.push "params['#{variableName}']" 83 | variableNames.push(variableName) 84 | if matchesWith[variableName]? then new SpanningRegexMatcher(matchesWith[variableName], [0], []) else new Variable() 85 | when '*' 86 | compiledPath.push "params['#{variableName}'].join('/')" 87 | variableName = part.slice(1, part.length) 88 | variableNames.push(variableName) 89 | new Glob(matchesWith[variableName]) 90 | else 91 | compiledPath.push "'#{part}'" 92 | new Lookup(part) 93 | add: (rawPath, opts) -> 94 | matchesWith = opts?.matchesWith || {} 95 | defaults = opts?.default || {} 96 | routeName = opts?.name 97 | partiallyMatch = false 98 | route = if rawPath.exec? 99 | new Route([@root.add(new RegexPath(@root, rawPath))]) 100 | else 101 | if rawPath.substring(rawPath.length - 1) == '*' 102 | rawPath = rawPath.substring(0, rawPath.length - 1) 103 | partiallyMatch = true 104 | pathSet = for path in @generatePaths(rawPath) 105 | node = @root 106 | variableNames = [] 107 | parts = path.split('/') 108 | compiledPath = [] 109 | for part in parts 110 | unless part == '' 111 | compiledPath.push "'/'" 112 | subparts = @findSubparts(part) 113 | nextNodeFn = if subparts.length == 1 then @addSimplePart else @addComplexPart 114 | node = node.add(nextNodeFn(subparts, compiledPath, matchesWith, variableNames)) 115 | if opts?.conditions? 116 | node = node.add(new RequestMatcher(opts.conditions)) 117 | path = new Path(node, variableNames) 118 | path.partial = partiallyMatch 119 | path.compiled = if compiledPath.length == 0 then "'/'" else compiledPath.join('+') 120 | path 121 | new Route(pathSet, matchesWith) 122 | route.default = defaults 123 | route.name = routeName 124 | @routes[routeName] = route if routeName? 125 | route 126 | 127 | class Response 128 | constructor: (@request, @httpResponse, @position) -> 129 | @position ||= 0 130 | next: -> 131 | if @position == @destinations.length - 1 132 | false 133 | else 134 | new Response(@request, @httpResponse, @position + 1).invoke() 135 | invoke: -> 136 | req = if typeof(@request.underlyingRequest) == 'string' then {} else @request.underlyingRequest 137 | req.params = @request.destinations[@position].params 138 | req.route = @request.destinations[@position].route 139 | req.pathInfo = @request.destinations[@position].pathInfo 140 | @request.destinations[@position].route.destination(req, @httpResponse) 141 | 142 | class Node 143 | constructor: -> 144 | @type ||= 'node' 145 | @matchers = [] 146 | add: (n) -> 147 | @matchers.push(n) if !@matchers[@matchers.length - 1]?.usable(n) 148 | @matchers[@matchers.length - 1].use(n) 149 | usable: (n) -> n.type == @type 150 | match: (request) -> 151 | m.match(request) for m in @matchers 152 | superMatch: Node::match 153 | use: (n) -> this 154 | 155 | class Lookup extends Node 156 | constructor: (part) -> 157 | @part = part 158 | @type = 'lookup' 159 | @map = {} 160 | super 161 | match: (request) -> 162 | if @map[request.path[0]]? 163 | request = request.clone() 164 | part = request.path.shift() 165 | @map[part].match(request) 166 | use: (n) -> 167 | @map[n.part] ||= new Node() 168 | @map[n.part] 169 | 170 | class Variable extends Node 171 | constructor: -> 172 | @type ||= 'variable' 173 | super 174 | match: (request) -> 175 | if request.path.length > 0 176 | request = request.clone() 177 | request.variables.push(request.path.shift()) 178 | super(request) 179 | 180 | class Glob extends Variable 181 | constructor: (@regexp) -> 182 | @type = 'glob' 183 | super 184 | match: (request) -> 185 | if request.path.length > 0 186 | original_request = request 187 | cloned_path = request.path.slice(0, request.path) 188 | for i in [1..original_request.path.length] 189 | request = original_request.clone() 190 | match = request.path[i - 1].match(@regexp) if @regexp? 191 | return if @regexp? and (!match? or match[0].length != request.path[i - 1].length) 192 | request.variables.push(request.path.slice(0, i)) 193 | request.path = request.path.slice(i, request.path.length) 194 | @superMatch(request) 195 | 196 | class RegexMatcher extends Node 197 | constructor: (@regexp, @capturingIndicies, @splittingIndicies) -> 198 | @type ||= 'regex' 199 | @varIndicies = [] 200 | @varIndicies[i] = [i, 'split'] for i in @splittingIndicies 201 | @varIndicies[i] = [i, 'capture'] for i in @capturingIndicies 202 | @varIndicies.sort (a, b) -> a[0] - b[0] 203 | super 204 | match: (request) -> 205 | if request.path[0]? and match = request.path[0].match(@regexp) 206 | return unless match[0].length == request.path[0].length 207 | request = request.clone() 208 | @addVariables(request, match) 209 | request.path.shift() 210 | super(request) 211 | addVariables: (request, match) -> 212 | for v in @varIndicies when v? 213 | idx = v[0] 214 | type = v[1] 215 | switch type 216 | when 'split' then request.variables.push match[idx].split('/') 217 | when 'capture' then request.variables.push match[idx] 218 | usable: (n) -> 219 | n.type == @type && n.regexp == @regexp && n.capturingIndicies == @capturingIndicies && n.splittingIndicies == @splittingIndicies 220 | 221 | class SpanningRegexMatcher extends RegexMatcher 222 | constructor: (@regexp, @capturingIndicies, @splittingIndicies) -> 223 | @type = 'spanning' 224 | super 225 | match: (request) -> 226 | if request.path.length > 0 227 | wholePath = request.wholePath() 228 | if match = wholePath.match(@regexp) 229 | return unless match.index == 0 230 | request = request.clone() 231 | @addVariables(request, match) 232 | request.path = request.splitPath(wholePath.slice(match.index + match[0].length, wholePath.length)) 233 | @superMatch(request) 234 | 235 | class RequestMatcher extends Node 236 | constructor: (@conditions) -> 237 | @type = 'request' 238 | super 239 | match: (request) -> 240 | conditionCount = 0 241 | satisfiedConditionCount = 0 242 | for type, matcher of @conditions 243 | val = request.underlyingRequest[type] 244 | conditionCount++ 245 | v = if matcher instanceof Array 246 | matching = -> 247 | for cond in matcher 248 | if cond.exec? 249 | return true if matcher.exec(val) 250 | else 251 | return true if cond == val 252 | false 253 | matching() 254 | else 255 | if matcher.exec? then matcher.exec(val) else matcher == val 256 | satisfiedConditionCount++ if v 257 | if conditionCount == satisfiedConditionCount 258 | super(request) 259 | usable: (n) -> 260 | n.type == @type && n.conditions == @conditions 261 | 262 | class Path extends Node 263 | constructor: (@parent, @variableNames) -> 264 | @type = 'path' 265 | @partial = false 266 | addDestination: (request) -> request.destinations.push({route: @route, request: request, params: @constructParams(request)}) 267 | match: (request) -> 268 | if @partial or request.path.length == 0 269 | @addDestination(request) 270 | if @partial 271 | request.destinations[request.destinations.length - 1].pathInfo = "/#{request.wholePath()}" 272 | constructParams: (request) -> 273 | params = {} 274 | for i in [0...@variableNames.length] 275 | params[@variableNames[i]] = request.variables[i] 276 | params 277 | url: (rawParams) -> 278 | rawParams = {} unless rawParams? 279 | params = {} 280 | for key in @variableNames 281 | params[key] = if @route.default? then rawParams[key] || @route.default[key] else rawParams[key] 282 | return undefined if !params[key]? 283 | for name in @variableNames 284 | if @route.matchesWith[name]? 285 | match = params[name].match(@route.matchesWith[name]) 286 | return undefined unless match? && match[0].length == params[name].length 287 | path = if @compiled == '' then '' else eval(@compiled) 288 | if path? 289 | delete rawParams[name] for name in @variableNames 290 | path 291 | 292 | class RegexPath extends Path 293 | constructor: (@parent, @regexp) -> 294 | @type = 'regexp_route' 295 | super 296 | match: (request) -> 297 | request.regexpRouteMatch = @regexp.exec(request.decodedPath()) 298 | if request.regexpRouteMatch? && request.regexpRouteMatch[0].length == request.decodedPath().length 299 | request = request.clone() 300 | request.path = [] 301 | super(request) 302 | constructParams: (request) -> request.regexpRouteMatch 303 | url: (rawParams) -> throw("This route cannot be generated") 304 | 305 | class Route 306 | constructor: (@pathSet, @matchesWith) -> 307 | path.route = this for path in @pathSet 308 | to: (@destination) -> 309 | path.parent.add(path) for path in @pathSet 310 | generateQuery: (params, base, query) -> 311 | query = "" 312 | base ||= "" 313 | if params? 314 | if params instanceof Array 315 | for idx in [0...(params.length)] 316 | query += @generateQuery(params[idx], "#{base}[]") 317 | else if params instanceof Object 318 | for k,v of params 319 | query += @generateQuery(v, if base == '' then k else "#{base}[#{k}]") 320 | else 321 | query += encodeURIComponent(base).replace(/%20/g, '+') 322 | query += '=' 323 | query += encodeURIComponent(params).replace(/%20/g, '+') 324 | query += '&' 325 | query 326 | url: (params) -> 327 | path = undefined 328 | for pathObj in @pathSet 329 | path = pathObj.url(params) 330 | break if path? 331 | if path? 332 | query = @generateQuery(params) 333 | joiner = if query != '' then '?' else '' 334 | "#{encodeURI(path)}#{joiner}#{query.substr(0, query.length - 1)}" 335 | else 336 | undefined 337 | 338 | class Request 339 | constructor: (@underlyingRequest, @callback) -> 340 | @variables = [] 341 | @destinations = [] 342 | if @underlyingRequest? 343 | @path = @splitPath() 344 | toString: -> "" 345 | wholePath: -> @path.join('/') 346 | decodedPath: (path) -> 347 | unless path? 348 | path = require('url').parse(@underlyingRequest.url).pathname 349 | decodeURI(path) 350 | splitPath: (path) -> 351 | decodedPath = @decodedPath(path) 352 | splitPath = if decodedPath == '/' then [] else decodedPath.split('/') 353 | splitPath.shift() if splitPath[0] == '' 354 | splitPath 355 | clone: -> 356 | c = new Request() 357 | c.path = @path.slice(0, @path.length) 358 | c.variables = @variables.slice(0, @variables.length) 359 | c.underlyingRequest = @underlyingRequest 360 | c.callback = @callback 361 | c.destinations = @destinations 362 | c 363 | 364 | class PathRequest extends Request 365 | decodedPath: (path) -> 366 | unless path? 367 | path = @underlyingRequest 368 | decodeURI(path) 369 | -------------------------------------------------------------------------------- /js/lib/http_router.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Sherpa; 3 | var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { 4 | for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } 5 | function ctor() { this.constructor = child; } 6 | ctor.prototype = parent.prototype; 7 | child.prototype = new ctor; 8 | child.__super__ = parent.prototype; 9 | return child; 10 | }; 11 | root.Sherpa = Sherpa = (function() { 12 | var Glob, Lookup, Node, Path, PathRequest, RegexMatcher, RegexPath, Request, RequestMatcher, Response, Route, SpanningRegexMatcher, Variable; 13 | function Sherpa(callback) { 14 | this.callback = callback; 15 | this.root = new Node(); 16 | this.routes = {}; 17 | } 18 | Sherpa.prototype.match = function(httpRequest, httpResponse) { 19 | var request; 20 | request = (httpRequest.url != null) ? new Request(httpRequest) : new PathRequest(httpRequest); 21 | this.root.match(request); 22 | if (request.destinations.length > 0) { 23 | return new Response(request, httpResponse).invoke(); 24 | } else if (this.callback != null) { 25 | return this.callback(request.underlyingRequest); 26 | } 27 | }; 28 | Sherpa.prototype.findSubparts = function(part) { 29 | var match, subparts; 30 | subparts = []; 31 | while (match = part.match(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)) { 32 | part = part.slice(match.index, part.length); 33 | subparts.push(part.slice(0, match[0].length)); 34 | part = part.slice(match[0].length, part.length); 35 | } 36 | return subparts; 37 | }; 38 | Sherpa.prototype.generatePaths = function(path) { 39 | var add, c, charIndex, chars, endIndex, pathIndex, paths, startIndex, _ref, _ref2; 40 | _ref = [[''], path.split(''), 0, 1], paths = _ref[0], chars = _ref[1], startIndex = _ref[2], endIndex = _ref[3]; 41 | for (charIndex = 0, _ref2 = chars.length; 0 <= _ref2 ? charIndex < _ref2 : charIndex > _ref2; 0 <= _ref2 ? charIndex++ : charIndex--) { 42 | c = chars[charIndex]; 43 | switch (c) { 44 | case '\\': 45 | charIndex++; 46 | add = chars[charIndex] === ')' || chars[charIndex] === '(' ? chars[charIndex] : "\\" + chars[charIndex]; 47 | for (pathIndex = startIndex; startIndex <= endIndex ? pathIndex < endIndex : pathIndex > endIndex; startIndex <= endIndex ? pathIndex++ : pathIndex--) { 48 | paths[pathIndex] += add; 49 | } 50 | break; 51 | case '(': 52 | for (pathIndex = startIndex; startIndex <= endIndex ? pathIndex < endIndex : pathIndex > endIndex; startIndex <= endIndex ? pathIndex++ : pathIndex--) { 53 | paths.push(paths[pathIndex]); 54 | } 55 | startIndex = endIndex; 56 | endIndex = paths.length; 57 | break; 58 | case ')': 59 | startIndex -= endIndex - startIndex; 60 | break; 61 | default: 62 | for (pathIndex = startIndex; startIndex <= endIndex ? pathIndex < endIndex : pathIndex > endIndex; startIndex <= endIndex ? pathIndex++ : pathIndex--) { 63 | paths[pathIndex] += c; 64 | } 65 | } 66 | } 67 | paths.reverse(); 68 | return paths; 69 | }; 70 | Sherpa.prototype.url = function(name, params) { 71 | var _ref; 72 | return (_ref = this.routes[name]) != null ? _ref.url(params) : void 0; 73 | }; 74 | Sherpa.prototype.addComplexPart = function(subparts, compiledPath, matchesWith, variableNames) { 75 | var captures, capturingIndicies, escapeRegexp, name, part, regexSubparts, regexp, spans, splittingIndicies, _ref; 76 | escapeRegexp = function(str) { 77 | return str.replace(/([\.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); 78 | }; 79 | _ref = [[], [], 0, false], capturingIndicies = _ref[0], splittingIndicies = _ref[1], captures = _ref[2], spans = _ref[3]; 80 | regexSubparts = (function() { 81 | var _i, _len, _results; 82 | _results = []; 83 | for (_i = 0, _len = subparts.length; _i < _len; _i++) { 84 | part = subparts[_i]; 85 | _results.push((function() { 86 | var _ref2; 87 | switch (part[0]) { 88 | case '\\': 89 | compiledPath.push("'" + part[1] + "'"); 90 | return escapeRegexp(part[1]); 91 | case ':': 92 | case '*': 93 | if (part[0] === '*') { 94 | spans = true; 95 | } 96 | captures += 1; 97 | name = part.slice(1, part.length); 98 | variableNames.push(name); 99 | if (part[0] === '*') { 100 | splittingIndicies.push(captures); 101 | compiledPath.push("params['" + name + "'].join('/')"); 102 | } else { 103 | capturingIndicies.push(captures); 104 | compiledPath.push("params['" + name + "']"); 105 | } 106 | if (spans) { 107 | if (matchesWith[name] != null) { 108 | return "((?:" + matchesWith[name].source + "\\/?)+)"; 109 | } else { 110 | return '(.*?)'; 111 | } 112 | } else { 113 | return "(" + (((_ref2 = matchesWith[name]) != null ? _ref2.source : void 0) || '[^/]*?') + ")"; 114 | } 115 | break; 116 | default: 117 | compiledPath.push("'" + part + "'"); 118 | return escapeRegexp(part); 119 | } 120 | })()); 121 | } 122 | return _results; 123 | })(); 124 | regexp = new RegExp("" + (regexSubparts.join('')) + "$"); 125 | if (spans) { 126 | return new SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies); 127 | } else { 128 | return new RegexMatcher(regexp, capturingIndicies, splittingIndicies); 129 | } 130 | }; 131 | Sherpa.prototype.addSimplePart = function(subparts, compiledPath, matchesWith, variableNames) { 132 | var part, variableName; 133 | part = subparts[0]; 134 | switch (part[0]) { 135 | case ':': 136 | variableName = part.slice(1, part.length); 137 | compiledPath.push("params['" + variableName + "']"); 138 | variableNames.push(variableName); 139 | if (matchesWith[variableName] != null) { 140 | return new SpanningRegexMatcher(matchesWith[variableName], [0], []); 141 | } else { 142 | return new Variable(); 143 | } 144 | break; 145 | case '*': 146 | compiledPath.push("params['" + variableName + "'].join('/')"); 147 | variableName = part.slice(1, part.length); 148 | variableNames.push(variableName); 149 | return new Glob(matchesWith[variableName]); 150 | default: 151 | compiledPath.push("'" + part + "'"); 152 | return new Lookup(part); 153 | } 154 | }; 155 | Sherpa.prototype.add = function(rawPath, opts) { 156 | var compiledPath, defaults, matchesWith, nextNodeFn, node, part, partiallyMatch, parts, path, pathSet, route, routeName, subparts, variableNames; 157 | matchesWith = (opts != null ? opts.matchesWith : void 0) || {}; 158 | defaults = (opts != null ? opts["default"] : void 0) || {}; 159 | routeName = opts != null ? opts.name : void 0; 160 | partiallyMatch = false; 161 | route = rawPath.exec != null ? new Route([this.root.add(new RegexPath(this.root, rawPath))]) : (rawPath.substring(rawPath.length - 1) === '*' ? (rawPath = rawPath.substring(0, rawPath.length - 1), partiallyMatch = true) : void 0, pathSet = (function() { 162 | var _i, _j, _len, _len2, _ref, _results; 163 | _ref = this.generatePaths(rawPath); 164 | _results = []; 165 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 166 | path = _ref[_i]; 167 | node = this.root; 168 | variableNames = []; 169 | parts = path.split('/'); 170 | compiledPath = []; 171 | for (_j = 0, _len2 = parts.length; _j < _len2; _j++) { 172 | part = parts[_j]; 173 | if (part !== '') { 174 | compiledPath.push("'/'"); 175 | subparts = this.findSubparts(part); 176 | nextNodeFn = subparts.length === 1 ? this.addSimplePart : this.addComplexPart; 177 | node = node.add(nextNodeFn(subparts, compiledPath, matchesWith, variableNames)); 178 | } 179 | } 180 | if ((opts != null ? opts.conditions : void 0) != null) { 181 | node = node.add(new RequestMatcher(opts.conditions)); 182 | } 183 | path = new Path(node, variableNames); 184 | path.partial = partiallyMatch; 185 | path.compiled = compiledPath.length === 0 ? "'/'" : compiledPath.join('+'); 186 | _results.push(path); 187 | } 188 | return _results; 189 | }).call(this), new Route(pathSet, matchesWith)); 190 | route["default"] = defaults; 191 | route.name = routeName; 192 | if (routeName != null) { 193 | this.routes[routeName] = route; 194 | } 195 | return route; 196 | }; 197 | Response = (function() { 198 | function Response(request, httpResponse, position) { 199 | this.request = request; 200 | this.httpResponse = httpResponse; 201 | this.position = position; 202 | this.position || (this.position = 0); 203 | } 204 | Response.prototype.next = function() { 205 | if (this.position === this.destinations.length - 1) { 206 | return false; 207 | } else { 208 | return new Response(this.request, this.httpResponse, this.position + 1).invoke(); 209 | } 210 | }; 211 | Response.prototype.invoke = function() { 212 | var req; 213 | req = typeof this.request.underlyingRequest === 'string' ? {} : this.request.underlyingRequest; 214 | req.params = this.request.destinations[this.position].params; 215 | req.route = this.request.destinations[this.position].route; 216 | req.pathInfo = this.request.destinations[this.position].pathInfo; 217 | return this.request.destinations[this.position].route.destination(req, this.httpResponse); 218 | }; 219 | return Response; 220 | })(); 221 | Node = (function() { 222 | function Node() { 223 | this.type || (this.type = 'node'); 224 | this.matchers = []; 225 | } 226 | Node.prototype.add = function(n) { 227 | var _ref; 228 | if (!((_ref = this.matchers[this.matchers.length - 1]) != null ? _ref.usable(n) : void 0)) { 229 | this.matchers.push(n); 230 | } 231 | return this.matchers[this.matchers.length - 1].use(n); 232 | }; 233 | Node.prototype.usable = function(n) { 234 | return n.type === this.type; 235 | }; 236 | Node.prototype.match = function(request) { 237 | var m, _i, _len, _ref, _results; 238 | _ref = this.matchers; 239 | _results = []; 240 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 241 | m = _ref[_i]; 242 | _results.push(m.match(request)); 243 | } 244 | return _results; 245 | }; 246 | Node.prototype.superMatch = Node.prototype.match; 247 | Node.prototype.use = function(n) { 248 | return this; 249 | }; 250 | return Node; 251 | })(); 252 | Lookup = (function() { 253 | __extends(Lookup, Node); 254 | function Lookup(part) { 255 | this.part = part; 256 | this.type = 'lookup'; 257 | this.map = {}; 258 | Lookup.__super__.constructor.apply(this, arguments); 259 | } 260 | Lookup.prototype.match = function(request) { 261 | var part; 262 | if (this.map[request.path[0]] != null) { 263 | request = request.clone(); 264 | part = request.path.shift(); 265 | return this.map[part].match(request); 266 | } 267 | }; 268 | Lookup.prototype.use = function(n) { 269 | var _base, _name; 270 | (_base = this.map)[_name = n.part] || (_base[_name] = new Node()); 271 | return this.map[n.part]; 272 | }; 273 | return Lookup; 274 | })(); 275 | Variable = (function() { 276 | __extends(Variable, Node); 277 | function Variable() { 278 | this.type || (this.type = 'variable'); 279 | Variable.__super__.constructor.apply(this, arguments); 280 | } 281 | Variable.prototype.match = function(request) { 282 | if (request.path.length > 0) { 283 | request = request.clone(); 284 | request.variables.push(request.path.shift()); 285 | return Variable.__super__.match.call(this, request); 286 | } 287 | }; 288 | return Variable; 289 | })(); 290 | Glob = (function() { 291 | __extends(Glob, Variable); 292 | function Glob(regexp) { 293 | this.regexp = regexp; 294 | this.type = 'glob'; 295 | Glob.__super__.constructor.apply(this, arguments); 296 | } 297 | Glob.prototype.match = function(request) { 298 | var cloned_path, i, match, original_request, _ref, _results; 299 | if (request.path.length > 0) { 300 | original_request = request; 301 | cloned_path = request.path.slice(0, request.path); 302 | _results = []; 303 | for (i = 1, _ref = original_request.path.length; 1 <= _ref ? i <= _ref : i >= _ref; 1 <= _ref ? i++ : i--) { 304 | request = original_request.clone(); 305 | if (this.regexp != null) { 306 | match = request.path[i - 1].match(this.regexp); 307 | } 308 | if ((this.regexp != null) && (!(match != null) || match[0].length !== request.path[i - 1].length)) { 309 | return; 310 | } 311 | request.variables.push(request.path.slice(0, i)); 312 | request.path = request.path.slice(i, request.path.length); 313 | _results.push(this.superMatch(request)); 314 | } 315 | return _results; 316 | } 317 | }; 318 | return Glob; 319 | })(); 320 | RegexMatcher = (function() { 321 | __extends(RegexMatcher, Node); 322 | function RegexMatcher(regexp, capturingIndicies, splittingIndicies) { 323 | var i, _i, _j, _len, _len2, _ref, _ref2; 324 | this.regexp = regexp; 325 | this.capturingIndicies = capturingIndicies; 326 | this.splittingIndicies = splittingIndicies; 327 | this.type || (this.type = 'regex'); 328 | this.varIndicies = []; 329 | _ref = this.splittingIndicies; 330 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 331 | i = _ref[_i]; 332 | this.varIndicies[i] = [i, 'split']; 333 | } 334 | _ref2 = this.capturingIndicies; 335 | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { 336 | i = _ref2[_j]; 337 | this.varIndicies[i] = [i, 'capture']; 338 | } 339 | this.varIndicies.sort(function(a, b) { 340 | return a[0] - b[0]; 341 | }); 342 | RegexMatcher.__super__.constructor.apply(this, arguments); 343 | } 344 | RegexMatcher.prototype.match = function(request) { 345 | var match; 346 | if ((request.path[0] != null) && (match = request.path[0].match(this.regexp))) { 347 | if (match[0].length !== request.path[0].length) { 348 | return; 349 | } 350 | request = request.clone(); 351 | this.addVariables(request, match); 352 | request.path.shift(); 353 | return RegexMatcher.__super__.match.call(this, request); 354 | } 355 | }; 356 | RegexMatcher.prototype.addVariables = function(request, match) { 357 | var idx, type, v, _i, _len, _ref, _results; 358 | _ref = this.varIndicies; 359 | _results = []; 360 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 361 | v = _ref[_i]; 362 | if (v != null) { 363 | idx = v[0]; 364 | type = v[1]; 365 | _results.push((function() { 366 | switch (type) { 367 | case 'split': 368 | return request.variables.push(match[idx].split('/')); 369 | case 'capture': 370 | return request.variables.push(match[idx]); 371 | } 372 | })()); 373 | } 374 | } 375 | return _results; 376 | }; 377 | RegexMatcher.prototype.usable = function(n) { 378 | return n.type === this.type && n.regexp === this.regexp && n.capturingIndicies === this.capturingIndicies && n.splittingIndicies === this.splittingIndicies; 379 | }; 380 | return RegexMatcher; 381 | })(); 382 | SpanningRegexMatcher = (function() { 383 | __extends(SpanningRegexMatcher, RegexMatcher); 384 | function SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies) { 385 | this.regexp = regexp; 386 | this.capturingIndicies = capturingIndicies; 387 | this.splittingIndicies = splittingIndicies; 388 | this.type = 'spanning'; 389 | SpanningRegexMatcher.__super__.constructor.apply(this, arguments); 390 | } 391 | SpanningRegexMatcher.prototype.match = function(request) { 392 | var match, wholePath; 393 | if (request.path.length > 0) { 394 | wholePath = request.wholePath(); 395 | if (match = wholePath.match(this.regexp)) { 396 | if (match.index !== 0) { 397 | return; 398 | } 399 | request = request.clone(); 400 | this.addVariables(request, match); 401 | request.path = request.splitPath(wholePath.slice(match.index + match[0].length, wholePath.length)); 402 | return this.superMatch(request); 403 | } 404 | } 405 | }; 406 | return SpanningRegexMatcher; 407 | })(); 408 | RequestMatcher = (function() { 409 | __extends(RequestMatcher, Node); 410 | function RequestMatcher(conditions) { 411 | this.conditions = conditions; 412 | this.type = 'request'; 413 | RequestMatcher.__super__.constructor.apply(this, arguments); 414 | } 415 | RequestMatcher.prototype.match = function(request) { 416 | var conditionCount, matcher, matching, satisfiedConditionCount, type, v, val, _ref; 417 | conditionCount = 0; 418 | satisfiedConditionCount = 0; 419 | _ref = this.conditions; 420 | for (type in _ref) { 421 | matcher = _ref[type]; 422 | val = request.underlyingRequest[type]; 423 | conditionCount++; 424 | v = matcher instanceof Array ? (matching = function() { 425 | var cond, _i, _len; 426 | for (_i = 0, _len = matcher.length; _i < _len; _i++) { 427 | cond = matcher[_i]; 428 | if (cond.exec != null) { 429 | if (matcher.exec(val)) { 430 | return true; 431 | } 432 | } else { 433 | if (cond === val) { 434 | return true; 435 | } 436 | } 437 | } 438 | return false; 439 | }, matching()) : matcher.exec != null ? matcher.exec(val) : matcher === val; 440 | if (v) { 441 | satisfiedConditionCount++; 442 | } 443 | } 444 | if (conditionCount === satisfiedConditionCount) { 445 | return RequestMatcher.__super__.match.call(this, request); 446 | } 447 | }; 448 | RequestMatcher.prototype.usable = function(n) { 449 | return n.type === this.type && n.conditions === this.conditions; 450 | }; 451 | return RequestMatcher; 452 | })(); 453 | Path = (function() { 454 | __extends(Path, Node); 455 | function Path(parent, variableNames) { 456 | this.parent = parent; 457 | this.variableNames = variableNames; 458 | this.type = 'path'; 459 | this.partial = false; 460 | } 461 | Path.prototype.addDestination = function(request) { 462 | return request.destinations.push({ 463 | route: this.route, 464 | request: request, 465 | params: this.constructParams(request) 466 | }); 467 | }; 468 | Path.prototype.match = function(request) { 469 | if (this.partial || request.path.length === 0) { 470 | this.addDestination(request); 471 | if (this.partial) { 472 | return request.destinations[request.destinations.length - 1].pathInfo = "/" + (request.wholePath()); 473 | } 474 | } 475 | }; 476 | Path.prototype.constructParams = function(request) { 477 | var i, params, _ref; 478 | params = {}; 479 | for (i = 0, _ref = this.variableNames.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) { 480 | params[this.variableNames[i]] = request.variables[i]; 481 | } 482 | return params; 483 | }; 484 | Path.prototype.url = function(rawParams) { 485 | var key, match, name, params, path, _i, _j, _k, _len, _len2, _len3, _ref, _ref2, _ref3; 486 | if (rawParams == null) { 487 | rawParams = {}; 488 | } 489 | params = {}; 490 | _ref = this.variableNames; 491 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 492 | key = _ref[_i]; 493 | params[key] = this.route["default"] != null ? rawParams[key] || this.route["default"][key] : rawParams[key]; 494 | if (!(params[key] != null)) { 495 | return; 496 | } 497 | } 498 | _ref2 = this.variableNames; 499 | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { 500 | name = _ref2[_j]; 501 | if (this.route.matchesWith[name] != null) { 502 | match = params[name].match(this.route.matchesWith[name]); 503 | if (!((match != null) && match[0].length === params[name].length)) { 504 | return; 505 | } 506 | } 507 | } 508 | path = this.compiled === '' ? '' : eval(this.compiled); 509 | if (path != null) { 510 | _ref3 = this.variableNames; 511 | for (_k = 0, _len3 = _ref3.length; _k < _len3; _k++) { 512 | name = _ref3[_k]; 513 | delete rawParams[name]; 514 | } 515 | return path; 516 | } 517 | }; 518 | return Path; 519 | })(); 520 | RegexPath = (function() { 521 | __extends(RegexPath, Path); 522 | function RegexPath(parent, regexp) { 523 | this.parent = parent; 524 | this.regexp = regexp; 525 | this.type = 'regexp_route'; 526 | RegexPath.__super__.constructor.apply(this, arguments); 527 | } 528 | RegexPath.prototype.match = function(request) { 529 | request.regexpRouteMatch = this.regexp.exec(request.decodedPath()); 530 | if ((request.regexpRouteMatch != null) && request.regexpRouteMatch[0].length === request.decodedPath().length) { 531 | request = request.clone(); 532 | request.path = []; 533 | return RegexPath.__super__.match.call(this, request); 534 | } 535 | }; 536 | RegexPath.prototype.constructParams = function(request) { 537 | return request.regexpRouteMatch; 538 | }; 539 | RegexPath.prototype.url = function(rawParams) { 540 | throw "This route cannot be generated"; 541 | }; 542 | return RegexPath; 543 | })(); 544 | Route = (function() { 545 | function Route(pathSet, matchesWith) { 546 | var path, _i, _len, _ref; 547 | this.pathSet = pathSet; 548 | this.matchesWith = matchesWith; 549 | _ref = this.pathSet; 550 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 551 | path = _ref[_i]; 552 | path.route = this; 553 | } 554 | } 555 | Route.prototype.to = function(destination) { 556 | var path, _i, _len, _ref, _results; 557 | this.destination = destination; 558 | _ref = this.pathSet; 559 | _results = []; 560 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 561 | path = _ref[_i]; 562 | _results.push(path.parent.add(path)); 563 | } 564 | return _results; 565 | }; 566 | Route.prototype.generateQuery = function(params, base, query) { 567 | var idx, k, v, _ref; 568 | query = ""; 569 | base || (base = ""); 570 | if (params != null) { 571 | if (params instanceof Array) { 572 | for (idx = 0, _ref = params.length; 0 <= _ref ? idx < _ref : idx > _ref; 0 <= _ref ? idx++ : idx--) { 573 | query += this.generateQuery(params[idx], "" + base + "[]"); 574 | } 575 | } else if (params instanceof Object) { 576 | for (k in params) { 577 | v = params[k]; 578 | query += this.generateQuery(v, base === '' ? k : "" + base + "[" + k + "]"); 579 | } 580 | } else { 581 | query += encodeURIComponent(base).replace(/%20/g, '+'); 582 | query += '='; 583 | query += encodeURIComponent(params).replace(/%20/g, '+'); 584 | query += '&'; 585 | } 586 | } 587 | return query; 588 | }; 589 | Route.prototype.url = function(params) { 590 | var joiner, path, pathObj, query, _i, _len, _ref; 591 | path = void 0; 592 | _ref = this.pathSet; 593 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 594 | pathObj = _ref[_i]; 595 | path = pathObj.url(params); 596 | if (path != null) { 597 | break; 598 | } 599 | } 600 | if (path != null) { 601 | query = this.generateQuery(params); 602 | joiner = query !== '' ? '?' : ''; 603 | return "" + (encodeURI(path)) + joiner + (query.substr(0, query.length - 1)); 604 | } else { 605 | return; 606 | } 607 | }; 608 | return Route; 609 | })(); 610 | Request = (function() { 611 | function Request(underlyingRequest, callback) { 612 | this.underlyingRequest = underlyingRequest; 613 | this.callback = callback; 614 | this.variables = []; 615 | this.destinations = []; 616 | if (this.underlyingRequest != null) { 617 | this.path = this.splitPath(); 618 | } 619 | } 620 | Request.prototype.toString = function() { 621 | return ""; 622 | }; 623 | Request.prototype.wholePath = function() { 624 | return this.path.join('/'); 625 | }; 626 | Request.prototype.decodedPath = function(path) { 627 | if (path == null) { 628 | path = require('url').parse(this.underlyingRequest.url).pathname; 629 | } 630 | return decodeURI(path); 631 | }; 632 | Request.prototype.splitPath = function(path) { 633 | var decodedPath, splitPath; 634 | decodedPath = this.decodedPath(path); 635 | splitPath = decodedPath === '/' ? [] : decodedPath.split('/'); 636 | if (splitPath[0] === '') { 637 | splitPath.shift(); 638 | } 639 | return splitPath; 640 | }; 641 | Request.prototype.clone = function() { 642 | var c; 643 | c = new Request(); 644 | c.path = this.path.slice(0, this.path.length); 645 | c.variables = this.variables.slice(0, this.variables.length); 646 | c.underlyingRequest = this.underlyingRequest; 647 | c.callback = this.callback; 648 | c.destinations = this.destinations; 649 | return c; 650 | }; 651 | return Request; 652 | })(); 653 | PathRequest = (function() { 654 | __extends(PathRequest, Request); 655 | function PathRequest() { 656 | PathRequest.__super__.constructor.apply(this, arguments); 657 | } 658 | PathRequest.prototype.decodedPath = function(path) { 659 | if (path == null) { 660 | path = this.underlyingRequest; 661 | } 662 | return decodeURI(path); 663 | }; 664 | return PathRequest; 665 | })(); 666 | return Sherpa; 667 | })(); 668 | }).call(this); 669 | -------------------------------------------------------------------------------- /js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http_router", 3 | "description": "URL routing and generation in js", 4 | "author": "Joshua Hull ", 5 | "version": "0.9.2", 6 | "directories": { 7 | "lib" : "./lib/http_router" 8 | }, 9 | "main": "lib/http_router" 10 | } 11 | -------------------------------------------------------------------------------- /js/test/test.coffee: -------------------------------------------------------------------------------- 1 | require.paths.push("#{__dirname}/../lib") 2 | fs = require('fs') 3 | util = require('util') 4 | sys = require('sys') 5 | http_router = require('http_router') 6 | assert = require('assert') 7 | 8 | class Example 9 | constructor: (@routes, @tests) -> 10 | 11 | class Test 12 | constructor: (file)-> 13 | @examples = [] 14 | contents = fs.readFileSync(file, 'utf8') 15 | lines = contents.split(/\n/m) 16 | currentTest = null 17 | routes = [] 18 | tests = [] 19 | for line in lines 20 | if line.match(/^#/) 21 | # this is a comment, skip 22 | else if line.match(/^\s*$/) 23 | # empty line, skip 24 | else if line.match(/^( |\t)/) 25 | # this is a test 26 | tests.push(JSON.parse(line)) 27 | else 28 | # this is a route 29 | if tests.length != 0 30 | @examples.push new Example(routes, tests) 31 | routes = [] 32 | tests = [] 33 | parsedRoutes = JSON.parse(line) 34 | if parsedRoutes instanceof Array 35 | for r in parsedRoutes 36 | routes.push(r) 37 | else 38 | routes.push(parsedRoutes) 39 | @examples.push new Example(routes, tests) 40 | interpretValue: (v) -> 41 | if v.regex? then new RegExp(v.regex) else v 42 | invoke: -> throw("need to implement") 43 | constructRouter: (example) -> 44 | router = new Sherpa() 45 | for route in example.routes 46 | for name, vals of route 47 | path = null 48 | opts = {name: name} 49 | if vals.path? 50 | path = @interpretValue(vals.path) 51 | delete vals.path 52 | if vals.conditions? 53 | conditions = {} 54 | for k, v of vals.conditions 55 | switch k 56 | when 'request_method' then conditions.method = @interpretValue(v) 57 | else conditions[k] = @interpretValue(v) 58 | opts.conditions = conditions 59 | delete vals.conditions 60 | if vals.default? 61 | opts.default = vals.default 62 | delete vals.default 63 | matchesWith = {} 64 | for k, v of vals 65 | matchesWith[k] = @interpretValue(v) 66 | delete vals.k 67 | opts.matchesWith = matchesWith 68 | else 69 | path = @interpretValue(vals) 70 | name = "" + name 71 | router.add(path, opts).to (req, response) -> 72 | response.params = req.params 73 | response.end(req.route.name) 74 | router 75 | 76 | class GenerationTest extends Test 77 | constructor: -> super 78 | invoke: -> 79 | console.log("Running #{@examples.length} generation tests") 80 | for example in @examples 81 | process.stdout.write "*" 82 | router = @constructRouter(example) 83 | for test in example.tests 84 | process.stdout.write "." 85 | [expectedResult, name, params] = test 86 | continue if params? && params instanceof Array 87 | continue if name instanceof Object 88 | actualResult = router.url(name, params) 89 | assert.equal(expectedResult, actualResult) 90 | console.log("\nDone!") 91 | 92 | class RecognitionTest extends Test 93 | constructor: -> super 94 | invoke: -> 95 | console.log("Running #{@examples.length} recognition tests") 96 | for example in @examples 97 | process.stdout.write "*" 98 | router = @constructRouter(example) 99 | for test in example.tests 100 | mockResponse = end: (part) -> @val = part 101 | process.stdout.write "." 102 | [expectedRouteName, requestingPath, expectedParams] = test 103 | mockRequest = {} 104 | complex = false 105 | if requestingPath.path? 106 | complex = true 107 | mockRequest.url = requestingPath.path 108 | delete requestingPath.path 109 | for k, v of requestingPath 110 | mockRequest[k] = v 111 | else 112 | mockRequest.url = requestingPath 113 | mockRequest.url = "http://host#{mockRequest.url}" unless mockRequest.url.match(/^http/) 114 | router.match(mockRequest, mockResponse) 115 | assert.equal(expectedRouteName, mockResponse.val) 116 | expectedParams ||= {} 117 | mockResponse.params ||= {} 118 | pathInfoExcpectation = null 119 | if expectedParams.PATH_INFO? 120 | pathInfoExcpectation = expectedParams.PATH_INFO 121 | delete expectedParams.PATH_INFO 122 | assert.equal(pathInfoExcpectation, mockRequest.pathInfo) if pathInfoExcpectation 123 | assert.deepEqual(expectedParams, mockResponse.params) 124 | unless complex 125 | mockResponse = end: (part) -> @val = part 126 | router.match(requestingPath, mockResponse) 127 | assert.equal(expectedRouteName, mockResponse.val) 128 | expectedParams ||= {} 129 | mockResponse.params ||= {} 130 | assert.equal(pathInfoExcpectation, mockRequest.pathInfo) if pathInfoExcpectation 131 | assert.deepEqual(expectedParams, mockResponse.params) 132 | console.log("\nDone!") 133 | 134 | new GenerationTest("#{__dirname}/../../test/common/generate.txt").invoke() 135 | new RecognitionTest("#{__dirname}/../../test/common/recognize.txt").invoke() 136 | 137 | -------------------------------------------------------------------------------- /js/test/test.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var Example, GenerationTest, RecognitionTest, Test, assert, fs, http_router, sys, util; 3 | var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) { 4 | for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } 5 | function ctor() { this.constructor = child; } 6 | ctor.prototype = parent.prototype; 7 | child.prototype = new ctor; 8 | child.__super__ = parent.prototype; 9 | return child; 10 | }; 11 | require.paths.push("" + __dirname + "/../lib"); 12 | fs = require('fs'); 13 | util = require('util'); 14 | sys = require('sys'); 15 | http_router = require('http_router'); 16 | assert = require('assert'); 17 | Example = (function() { 18 | function Example(routes, tests) { 19 | this.routes = routes; 20 | this.tests = tests; 21 | } 22 | return Example; 23 | })(); 24 | Test = (function() { 25 | function Test(file) { 26 | var contents, currentTest, line, lines, parsedRoutes, r, routes, tests, _i, _j, _len, _len2; 27 | this.examples = []; 28 | contents = fs.readFileSync(file, 'utf8'); 29 | lines = contents.split(/\n/m); 30 | currentTest = null; 31 | routes = []; 32 | tests = []; 33 | for (_i = 0, _len = lines.length; _i < _len; _i++) { 34 | line = lines[_i]; 35 | if (line.match(/^#/)) {} else if (line.match(/^\s*$/)) {} else if (line.match(/^( |\t)/)) { 36 | tests.push(JSON.parse(line)); 37 | } else { 38 | if (tests.length !== 0) { 39 | this.examples.push(new Example(routes, tests)); 40 | routes = []; 41 | tests = []; 42 | } 43 | parsedRoutes = JSON.parse(line); 44 | if (parsedRoutes instanceof Array) { 45 | for (_j = 0, _len2 = parsedRoutes.length; _j < _len2; _j++) { 46 | r = parsedRoutes[_j]; 47 | routes.push(r); 48 | } 49 | } else { 50 | routes.push(parsedRoutes); 51 | } 52 | } 53 | } 54 | this.examples.push(new Example(routes, tests)); 55 | } 56 | Test.prototype.interpretValue = function(v) { 57 | if (v.regex != null) { 58 | return new RegExp(v.regex); 59 | } else { 60 | return v; 61 | } 62 | }; 63 | Test.prototype.invoke = function() { 64 | throw "need to implement"; 65 | }; 66 | Test.prototype.constructRouter = function(example) { 67 | var conditions, k, matchesWith, name, opts, path, route, router, v, vals, _i, _len, _ref, _ref2; 68 | router = new Sherpa(); 69 | _ref = example.routes; 70 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 71 | route = _ref[_i]; 72 | for (name in route) { 73 | vals = route[name]; 74 | path = null; 75 | opts = { 76 | name: name 77 | }; 78 | if (vals.path != null) { 79 | path = this.interpretValue(vals.path); 80 | delete vals.path; 81 | if (vals.conditions != null) { 82 | conditions = {}; 83 | _ref2 = vals.conditions; 84 | for (k in _ref2) { 85 | v = _ref2[k]; 86 | switch (k) { 87 | case 'request_method': 88 | conditions.method = this.interpretValue(v); 89 | break; 90 | default: 91 | conditions[k] = this.interpretValue(v); 92 | } 93 | } 94 | opts.conditions = conditions; 95 | delete vals.conditions; 96 | } 97 | if (vals["default"] != null) { 98 | opts["default"] = vals["default"]; 99 | delete vals["default"]; 100 | } 101 | matchesWith = {}; 102 | for (k in vals) { 103 | v = vals[k]; 104 | matchesWith[k] = this.interpretValue(v); 105 | delete vals.k; 106 | } 107 | opts.matchesWith = matchesWith; 108 | } else { 109 | path = this.interpretValue(vals); 110 | } 111 | name = "" + name; 112 | router.add(path, opts).to(function(req, response) { 113 | response.params = req.params; 114 | return response.end(req.route.name); 115 | }); 116 | } 117 | } 118 | return router; 119 | }; 120 | return Test; 121 | })(); 122 | GenerationTest = (function() { 123 | __extends(GenerationTest, Test); 124 | function GenerationTest() { 125 | GenerationTest.__super__.constructor.apply(this, arguments); 126 | } 127 | GenerationTest.prototype.invoke = function() { 128 | var actualResult, example, expectedResult, name, params, router, test, _i, _j, _len, _len2, _ref, _ref2; 129 | console.log("Running " + this.examples.length + " generation tests"); 130 | _ref = this.examples; 131 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 132 | example = _ref[_i]; 133 | process.stdout.write("*"); 134 | router = this.constructRouter(example); 135 | _ref2 = example.tests; 136 | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { 137 | test = _ref2[_j]; 138 | process.stdout.write("."); 139 | expectedResult = test[0], name = test[1], params = test[2]; 140 | if ((params != null) && params instanceof Array) { 141 | continue; 142 | } 143 | if (name instanceof Object) { 144 | continue; 145 | } 146 | actualResult = router.url(name, params); 147 | assert.equal(expectedResult, actualResult); 148 | } 149 | } 150 | return console.log("\nDone!"); 151 | }; 152 | return GenerationTest; 153 | })(); 154 | RecognitionTest = (function() { 155 | __extends(RecognitionTest, Test); 156 | function RecognitionTest() { 157 | RecognitionTest.__super__.constructor.apply(this, arguments); 158 | } 159 | RecognitionTest.prototype.invoke = function() { 160 | var complex, example, expectedParams, expectedRouteName, k, mockRequest, mockResponse, pathInfoExcpectation, requestingPath, router, test, v, _i, _j, _len, _len2, _ref, _ref2; 161 | console.log("Running " + this.examples.length + " recognition tests"); 162 | _ref = this.examples; 163 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 164 | example = _ref[_i]; 165 | process.stdout.write("*"); 166 | router = this.constructRouter(example); 167 | _ref2 = example.tests; 168 | for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { 169 | test = _ref2[_j]; 170 | mockResponse = { 171 | end: function(part) { 172 | return this.val = part; 173 | } 174 | }; 175 | process.stdout.write("."); 176 | expectedRouteName = test[0], requestingPath = test[1], expectedParams = test[2]; 177 | mockRequest = {}; 178 | complex = false; 179 | if (requestingPath.path != null) { 180 | complex = true; 181 | mockRequest.url = requestingPath.path; 182 | delete requestingPath.path; 183 | for (k in requestingPath) { 184 | v = requestingPath[k]; 185 | mockRequest[k] = v; 186 | } 187 | } else { 188 | mockRequest.url = requestingPath; 189 | } 190 | if (!mockRequest.url.match(/^http/)) { 191 | mockRequest.url = "http://host" + mockRequest.url; 192 | } 193 | router.match(mockRequest, mockResponse); 194 | assert.equal(expectedRouteName, mockResponse.val); 195 | expectedParams || (expectedParams = {}); 196 | mockResponse.params || (mockResponse.params = {}); 197 | pathInfoExcpectation = null; 198 | if (expectedParams.PATH_INFO != null) { 199 | pathInfoExcpectation = expectedParams.PATH_INFO; 200 | delete expectedParams.PATH_INFO; 201 | } 202 | if (pathInfoExcpectation) { 203 | assert.equal(pathInfoExcpectation, mockRequest.pathInfo); 204 | } 205 | assert.deepEqual(expectedParams, mockResponse.params); 206 | if (!complex) { 207 | mockResponse = { 208 | end: function(part) { 209 | return this.val = part; 210 | } 211 | }; 212 | router.match(requestingPath, mockResponse); 213 | assert.equal(expectedRouteName, mockResponse.val); 214 | expectedParams || (expectedParams = {}); 215 | mockResponse.params || (mockResponse.params = {}); 216 | if (pathInfoExcpectation) { 217 | assert.equal(pathInfoExcpectation, mockRequest.pathInfo); 218 | } 219 | assert.deepEqual(expectedParams, mockResponse.params); 220 | } 221 | } 222 | } 223 | return console.log("\nDone!"); 224 | }; 225 | return RecognitionTest; 226 | })(); 227 | new GenerationTest("" + __dirname + "/../../test/common/generate.txt").invoke(); 228 | new RecognitionTest("" + __dirname + "/../../test/common/recognize.txt").invoke(); 229 | }).call(this); 230 | -------------------------------------------------------------------------------- /lib/http_router.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'rack' 3 | require 'uri' 4 | require 'cgi' 5 | require 'url_mount' 6 | require 'http_router/node' 7 | require 'http_router/request' 8 | require 'http_router/response' 9 | require 'http_router/route' 10 | require 'http_router/generator' 11 | require 'http_router/route_helper' 12 | require 'http_router/generation_helper' 13 | require 'http_router/regex_route_generation' 14 | require 'http_router/util' 15 | 16 | class HttpRouter 17 | # Raised when a url is not able to be generated for the given parameters 18 | InvalidRouteException = Class.new(RuntimeError) 19 | # Raised when a Route is not able to be generated due to a missing parameter. 20 | MissingParameterException = Class.new(RuntimeError) 21 | # Raised an invalid request value is used 22 | InvalidRequestValueError = Class.new(RuntimeError) 23 | # Raised when there are extra parameters passed in to #url 24 | TooManyParametersException = Class.new(RuntimeError) 25 | # Raised when there are left over options 26 | LeftOverOptions = Class.new(RuntimeError) 27 | # Raised when there are duplicate param names specified in a Path 28 | AmbiguousVariableException = Class.new(RuntimeError) 29 | 30 | RecognizeResponse = Struct.new(:matches, :acceptable_methods) 31 | 32 | attr_reader :root, :routes, :named_routes, :nodes 33 | attr_writer :route_class 34 | attr_accessor :default_app, :url_mount, :default_host, :default_port, :default_scheme 35 | 36 | # Creates a new HttpRouter. 37 | # Can be called with either HttpRouter.new(proc{|env| ... }, { .. options .. }) or with the first argument omitted. 38 | # If there is a proc first, then it's used as the default app in the case of a non-match. 39 | # Supported options are 40 | # * :default_app -- Default application used if there is a non-match on #call. Defaults to 404 generator. 41 | # * :ignore_trailing_slash -- Ignore a trailing / when attempting to match. Defaults to +true+. 42 | # * :redirect_trailing_slash -- On trailing /, redirect to the same path without the /. Defaults to +false+. 43 | def initialize(*args, &blk) 44 | default_app, options = args.first.is_a?(Hash) ? [nil, args.first] : [args.first, args[1]] 45 | @options = options 46 | @default_app = default_app || options && options[:default_app] || proc{|env| ::Rack::Response.new("Not Found", 404, {'X-Cascade' => 'pass'}).finish } 47 | @ignore_trailing_slash = options && options.key?(:ignore_trailing_slash) ? options[:ignore_trailing_slash] : true 48 | @redirect_trailing_slash = options && options.key?(:redirect_trailing_slash) ? options[:redirect_trailing_slash] : false 49 | @route_class = Route 50 | reset! 51 | instance_eval(&blk) if blk 52 | end 53 | 54 | # Adds a path to be recognized. 55 | # 56 | # To assign a part of the path to a specific variable, use :variable_name within the route. 57 | # For example, add('/path/:id') would match /path/test, with the variable :id having the value "test". 58 | # 59 | # You can receive mulitple parts into a single variable by using the glob syntax. 60 | # For example, add('/path/*id') would match /path/123/456/789, with the variable :id having the value ["123", "456", "789"]. 61 | # 62 | # As well, paths can end with two optional parts, * and /?. If it ends with a *, it will match partially, returning the part of the path unmatched in the PATH_INFO value of the env. The part matched to will be returned in the SCRIPT_NAME. If it ends with /?, then a trailing / on the path will be optionally matched for that specific route. As trailing /'s are ignored by default, you probably don't actually want to use this option that frequently. 63 | # 64 | # Routes can also contain optional parts. There are surrounded with ( )'s. If you need to match on a bracket in the route itself, you can escape the parentheses with a backslash. 65 | # 66 | # As well, options can be passed in that modify the route in further ways. See HttpRouter::Route#with_options for details. Typically, you want to add further options to the route by calling additional methods on it. See HttpRouter::Route for further details. 67 | # 68 | # Returns the route object. 69 | def add(*args, &app) 70 | uncompile 71 | opts = args.last.is_a?(Hash) ? args.pop : nil 72 | path = args.first 73 | route = route_class.new 74 | add_route route 75 | route.path = path if path 76 | route.process_opts(opts) if opts 77 | route.to(app) if app 78 | route 79 | end 80 | 81 | def add_route(route) 82 | @routes << route 83 | @named_routes[route.name] << route if route.name 84 | route.router = self 85 | end 86 | 87 | # Extends the route class with custom features. 88 | # 89 | # Example: 90 | # router = HttpRouter.new { extend_route { attr_accessor :controller } } 91 | # router.add('/foo', :controller => :foo).to{|env| [200, {}, ['foo!']]} 92 | # matches, other_methods = router.recognize(Rack::MockRequest.env_for('/foo')) 93 | # matches.first.route.controller 94 | # # ==> :foo 95 | def extend_route(&blk) 96 | @route_class = Class.new(Route) if @route_class == Route 97 | @route_class.class_eval(&blk) 98 | @extended_route_class = nil 99 | end 100 | 101 | def route_class 102 | @extended_route_class ||= begin 103 | @route_class.send(:include, RouteHelper) 104 | @route_class.send(:include, GenerationHelper) 105 | @route_class 106 | end 107 | end 108 | 109 | # Creates helper methods for each supported HTTP verb, except GET, which is 110 | # a special case that accepts both GET and HEAD requests. 111 | Route::VALID_HTTP_VERBS_WITHOUT_GET.each do |request_method| 112 | request_method_symbol = request_method.downcase.to_sym 113 | define_method(request_method_symbol) do |path, opts = {}, &app| 114 | add_with_request_method(path, request_method_symbol, opts, &app) 115 | end 116 | end 117 | 118 | # Adds a path that only responds to the request method +GET+. 119 | # 120 | # Returns the route object. 121 | def get(path, opts = {}, &app); add_with_request_method(path, [:get, :head], opts, &app); end 122 | 123 | # Performs recoginition without actually calling the application and returns an array of all 124 | # matching routes or nil if no match was found. 125 | def recognize(env, &callback) 126 | if callback 127 | request = call(env, &callback) 128 | [request.called?, request.acceptable_methods] 129 | else 130 | matches = [] 131 | callback ||= Proc.new {|match| matches << match} 132 | request = call(env, &callback) 133 | [matches.empty? ? nil : matches, request.acceptable_methods] 134 | end 135 | end 136 | 137 | # Rack compatible #call. If matching route is found, and +dest+ value responds to #call, processing will pass to the matched route. Otherwise, 138 | # the default application will be called. The router will be available in the env under the key router. And parameters matched will 139 | # be available under the key router.params. 140 | def call(env, &callback) 141 | compile 142 | call(env, &callback) 143 | end 144 | alias_method :compiling_call, :call 145 | 146 | # Resets the router to a clean state. 147 | def reset! 148 | uncompile 149 | @routes, @named_routes, @root = [], Hash.new{|h,k| h[k] = []}, Node::Root.new(self) 150 | @default_app = Proc.new{ |env| ::Rack::Response.new("Your request couldn't be found", 404).finish } 151 | @default_host, @default_port, @default_scheme = 'localhost', 80, 'http' 152 | end 153 | 154 | # Assigns the default application. 155 | def default(app) 156 | @default_app = app 157 | end 158 | 159 | # Generate a URL for a specified route. This will accept a list of variable values plus any other variable names named as a hash. 160 | # This first value must be either the Route object or the name of the route. 161 | # 162 | # Example: 163 | # router = HttpRouter.new 164 | # router.add('/:foo.:format', :name => :test).to{|env| [200, {}, []]} 165 | # router.path(:test, 123, 'html') 166 | # # ==> "/123.html" 167 | # router.path(:test, 123, :format => 'html') 168 | # # ==> "/123.html" 169 | # router.path(:test, :foo => 123, :format => 'html') 170 | # # ==> "/123.html" 171 | # router.path(:test, :foo => 123, :format => 'html', :fun => 'inthesun') 172 | # # ==> "/123.html?fun=inthesun" 173 | def url(route, *args) 174 | compile 175 | url(route, *args) 176 | end 177 | alias_method :compiling_url, :url 178 | 179 | def url_ns(route, *args) 180 | compile 181 | url_ns(route, *args) 182 | end 183 | alias_method :compiling_url_ns, :url_ns 184 | 185 | def path(route, *args) 186 | compile 187 | path(route, *args) 188 | end 189 | alias_method :compiling_path, :path 190 | 191 | # This method is invoked when a Path object gets called with an env. Override it to implement custom path processing. 192 | def process_destination_path(path, env) 193 | path.route.dest.call(env) 194 | end 195 | 196 | # This method defines what sort of responses are considered "passes", and thus, route processing will continue. Override 197 | # it to implement custom passing. 198 | def pass_on_response(response) 199 | response[1]['X-Cascade'] == 'pass' 200 | end 201 | 202 | # Ignore trailing slash feature enabled? See #initialize for details. 203 | def ignore_trailing_slash? 204 | @ignore_trailing_slash 205 | end 206 | 207 | # Redirect trailing slash feature enabled? See #initialize for details. 208 | def redirect_trailing_slash? 209 | @redirect_trailing_slash 210 | end 211 | 212 | # Creates a deep-copy of the router. 213 | def clone(klass = self.class) 214 | cloned_router = klass.new(@options) 215 | @routes.each do |route| 216 | new_route = route.create_clone(cloned_router) 217 | cloned_router.add_route(new_route) 218 | end 219 | cloned_router 220 | end 221 | 222 | def rewrite_partial_path_info(env, request) 223 | env['PATH_INFO'] = "/#{request.path.join('/')}" 224 | env['SCRIPT_NAME'] += request.rack_request.path_info[0, request.rack_request.path_info.size - env['PATH_INFO'].size] 225 | end 226 | 227 | def rewrite_path_info(env, request) 228 | env['SCRIPT_NAME'] += request.rack_request.path_info 229 | env['PATH_INFO'] = '' 230 | end 231 | 232 | def no_response(request, env) 233 | request.acceptable_methods.empty? ? 234 | @default_app.call(env) : [405, {'Allow' => request.acceptable_methods.sort.join(", ")}, []] 235 | end 236 | 237 | def to_s 238 | compile 239 | "#" 240 | end 241 | 242 | def inspect 243 | head = to_s 244 | "#{to_s}\n#{'=' * head.size}\n#{@root.inspect}" 245 | end 246 | 247 | def uncompile 248 | return unless @compiled 249 | instance_eval "undef :path; alias :path :compiling_path 250 | undef :url; alias :url :compiling_url 251 | undef :url_ns; alias :url_ns :compiling_url_ns 252 | undef :call; alias :call :compiling_call", __FILE__, __LINE__ 253 | @root.uncompile 254 | @compiled = false 255 | end 256 | 257 | def raw_url(route, *args) 258 | case route 259 | when Symbol then @named_routes.key?(route) && @named_routes[route].each{|r| url = r.url(*args); return url if url} 260 | when Route then return route.url(*args) 261 | end 262 | raise(InvalidRouteException.new "No route (url) could be generated for #{route.inspect}") 263 | end 264 | 265 | def raw_url_ns(route, *args) 266 | case route 267 | when Symbol then @named_routes.key?(route) && @named_routes[route].each{|r| url = r.url_ns(*args); return url if url} 268 | when Route then return route.url_ns(*args) 269 | end 270 | raise(InvalidRouteException.new "No route (url_ns) could be generated for #{route.inspect}") 271 | end 272 | 273 | def raw_path(route, *args) 274 | case route 275 | when Symbol then @named_routes.key?(route) && @named_routes[route].each{|r| path = r.path(*args); return path if path} 276 | when Route then return route.path(*args) 277 | end 278 | raise(InvalidRouteException.new "No route (path) could be generated for #{route.inspect}") 279 | end 280 | 281 | def raw_call(env, &blk) 282 | rack_request = ::Rack::Request.new(env) 283 | request = Request.new(rack_request.path_info, rack_request) 284 | if blk 285 | @root.call(request, &blk) 286 | request 287 | else 288 | @root.call(request) or no_response(request, env) 289 | end 290 | end 291 | 292 | private 293 | def compile 294 | return if @compiled 295 | @root.compile(@routes) 296 | @named_routes.each do |_, routes| 297 | routes.sort!{|r1, r2| r2.max_param_count <=> r1.max_param_count } 298 | end 299 | 300 | instance_eval "undef :path; alias :path :raw_path 301 | undef :url; alias :url :raw_url 302 | undef :url_ns; alias :url_ns :raw_url_ns 303 | undef :call; alias :call :raw_call", __FILE__, __LINE__ 304 | @compiled = true 305 | end 306 | 307 | def add_with_request_method(path, method, opts = {}, &app) 308 | opts[:request_method] = method 309 | route = add(path, opts) 310 | route.to(app) if app 311 | route 312 | end 313 | end 314 | -------------------------------------------------------------------------------- /lib/http_router/generation_helper.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | module GenerationHelper 3 | def max_param_count 4 | @generator.max_param_count 5 | end 6 | 7 | def url(*args) 8 | @generator.url(*args) 9 | rescue InvalidRouteException 10 | nil 11 | end 12 | 13 | def url_ns(*args) 14 | @generator.url_ns(*args) 15 | rescue InvalidRouteException 16 | nil 17 | end 18 | 19 | def path(*args) 20 | @generator.path(*args) 21 | rescue InvalidRouteException 22 | nil 23 | end 24 | 25 | def param_names 26 | @generator.param_names 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/http_router/generator.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Generator 3 | SCHEME_PORTS = {'http' => 80, 'https' => 443} 4 | 5 | class PathGenerator 6 | attr_reader :path 7 | attr_accessor :param_names 8 | def initialize(route, path, validation_regex = nil) 9 | @route = route 10 | @path = path.dup 11 | @param_names = [] 12 | if path.is_a?(String) 13 | path[0, 0] = '/' unless path[0] == ?/ 14 | regex_parts = path.split(/([:\*][a-zA-Z0-9_]+)/) 15 | regex, code = '', '' 16 | dynamic = false 17 | regex_parts.each_with_index do |part, index| 18 | case part[0] 19 | when ?:, ?* 20 | if index != 0 && regex_parts[index - 1][-1] == ?\\ 21 | regex << Regexp.quote(part) unless validation_regex 22 | code << part 23 | dynamic = true 24 | else 25 | regex << (@route.matches_with(part[1, part.size].to_sym) || '.*?').to_s unless validation_regex 26 | code << "\#{args.shift || (options && options.delete(:#{part[1, part.size]})) || return}" 27 | dynamic = true 28 | end 29 | else 30 | regex << Regexp.quote(part) unless validation_regex 31 | code << part 32 | end 33 | end 34 | validation_regex ||= Regexp.new("^#{regex}$") if dynamic 35 | if validation_regex 36 | instance_eval <<-EOT, __FILE__, __LINE__ + 1 37 | def generate(args, options) 38 | generated_path = \"#{code}\" 39 | #{validation_regex.inspect}.match(generated_path) ? URI::DEFAULT_PARSER.escape(generated_path) : nil 40 | end 41 | EOT 42 | else 43 | instance_eval <<-EOT, __FILE__, __LINE__ + 1 44 | def generate(args, options) 45 | URI::DEFAULT_PARSER.escape(\"#{code}\") 46 | end 47 | EOT 48 | end 49 | end 50 | end 51 | end 52 | 53 | def initialize(route, paths) 54 | @route, @paths = route, paths 55 | @router = @route.router 56 | @route.generator = self 57 | @path_generators = @paths.map do |p| 58 | generator = PathGenerator.new(route, p.is_a?(String) ? p : route.path_for_generation, p.is_a?(Regexp) ? p : nil) 59 | end 60 | end 61 | 62 | def param_names 63 | @param_names ||= @path_generators.map{|path| path.param_names}.flatten.uniq 64 | end 65 | 66 | def max_param_count 67 | @max_param_count ||= @path_generators.map{|p| p.param_names.size}.max 68 | end 69 | 70 | def each_path 71 | @path_generators.each {|p| yield p } 72 | @path_generators.sort! do |p1, p2| 73 | p2.param_names.size <=> p1.param_names.size 74 | end 75 | end 76 | 77 | def url(*args) 78 | "#{scheme_port.first}#{url_ns(*args)}" 79 | end 80 | 81 | def url_ns(*args) 82 | "://#{@route.host || @router.default_host}#{scheme_port.last}#{path(*args)}" 83 | end 84 | 85 | def path(*args) 86 | result, extra_params = path_with_params(*args) 87 | append_querystring(result, extra_params) 88 | end 89 | 90 | private 91 | def scheme_port 92 | @scheme_port ||= begin 93 | scheme = @route.scheme || @router.default_scheme 94 | port = @router.default_port 95 | port_part = SCHEME_PORTS.key?(scheme) && SCHEME_PORTS[scheme] == port ? '' : ":#{port}" 96 | [scheme, port_part] 97 | end 98 | end 99 | 100 | def path_with_params(*a) 101 | path_args_processing(a) do |args, options| 102 | path = args.empty? ? matching_path(options) : matching_path(args, options) 103 | path &&= path.generate(args, options) 104 | raise TooManyParametersException unless args.empty? 105 | raise InvalidRouteException.new("Error generating #{@route.path_for_generation}") unless path 106 | path ? [path, options] : nil 107 | end 108 | end 109 | 110 | def path_args_processing(args) 111 | options = args.last.is_a?(Hash) ? args.pop : nil 112 | options = options.nil? ? @route.default_values.dup : @route.default_values.merge(options) if @route.default_values 113 | options.delete_if{ |k,v| v.nil? } if options 114 | result, params = yield args, options 115 | mount_point = @router.url_mount && (options ? @router.url_mount.url(options) : @router.url_mount.url) 116 | mount_point ? [File.join(mount_point, result), params] : [result, params] 117 | end 118 | 119 | def matching_path(params, other_hash = nil) 120 | return @path_generators.first if @path_generators.size == 1 121 | case params 122 | when Array, nil 123 | @path_generators.find do |path| 124 | significant_key_count = params ? params.size : 0 125 | significant_key_count += (path.param_names & other_hash.keys).size if other_hash 126 | significant_key_count >= path.param_names.size 127 | end 128 | when Hash 129 | @path_generators.find { |path| (params && !params.empty? && (path.param_names & params.keys).size == path.param_names.size) || path.param_names.empty? } 130 | end 131 | end 132 | 133 | def append_querystring_value(uri, key, value) 134 | case value 135 | when Array then value.each{ |v| append_querystring_value(uri, "#{key}[]", v) } 136 | when Hash then value.each{ |k, v| append_querystring_value(uri, "#{key}[#{k}]", v) } 137 | else uri << "&#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" 138 | end 139 | end 140 | 141 | def append_querystring(uri, params) 142 | if params && !params.empty? 143 | uri_size = uri.size 144 | params.each{ |k,v| append_querystring_value(uri, k, v) } 145 | uri[uri_size] = ?? 146 | end 147 | uri 148 | end 149 | end 150 | end 151 | -------------------------------------------------------------------------------- /lib/http_router/node.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | autoload :Root, 'http_router/node/root' 4 | autoload :Glob, 'http_router/node/glob' 5 | autoload :GlobRegex, 'http_router/node/glob_regex' 6 | autoload :Variable, 'http_router/node/variable' 7 | autoload :Regex, 'http_router/node/regex' 8 | autoload :SpanningRegex, 'http_router/node/spanning_regex' 9 | autoload :GlobRegex, 'http_router/node/glob_regex' 10 | autoload :FreeRegex, 'http_router/node/free_regex' 11 | autoload :AbstractRequestNode, 'http_router/node/abstract_request_node' 12 | autoload :Host, 'http_router/node/host' 13 | autoload :UserAgent, 'http_router/node/user_agent' 14 | autoload :RequestMethod, 'http_router/node/request_method' 15 | autoload :Scheme, 'http_router/node/scheme' 16 | autoload :Lookup, 'http_router/node/lookup' 17 | autoload :Path, 'http_router/node/path' 18 | 19 | attr_reader :router 20 | 21 | def initialize(router, parent, matchers = []) 22 | @router, @parent, @matchers = router, parent, matchers 23 | end 24 | 25 | def add_variable 26 | add(Variable.new(@router, self)) 27 | end 28 | 29 | def add_glob 30 | add(Glob.new(@router, self)) 31 | end 32 | 33 | def add_glob_regexp(matcher) 34 | add(GlobRegex.new(@router, self, matcher)) 35 | end 36 | 37 | def add_host(hosts) 38 | add(Host.new(@router, self, hosts)) 39 | end 40 | 41 | def add_user_agent(uas) 42 | add(UserAgent.new(@router, self, uas)) 43 | end 44 | 45 | def add_request_method(rm) 46 | add(RequestMethod.new(@router, self, rm)) 47 | end 48 | 49 | def add_scheme(scheme) 50 | add(Scheme.new(@router, self, scheme)) 51 | end 52 | 53 | def add_match(regexp, matching_indicies = [0], splitting_indicies = nil) 54 | add(Regex.new(@router, self, regexp, matching_indicies, splitting_indicies)) 55 | end 56 | 57 | def add_spanning_match(regexp, matching_indicies = [0], splitting_indicies = nil) 58 | add(SpanningRegex.new(@router, self, regexp, matching_indicies, splitting_indicies)) 59 | end 60 | 61 | def add_free_match(regexp) 62 | add(FreeRegex.new(@router, self, regexp)) 63 | end 64 | 65 | def add_destination(route, path, param_names = []) 66 | add(Path.new(@router, self, route, path, param_names)) 67 | end 68 | 69 | def add_lookup(part) 70 | add(Lookup.new(@router, self)).add(part) 71 | end 72 | 73 | def usable?(other) 74 | false 75 | end 76 | 77 | def inspect 78 | ins = "#{' ' * depth}#{inspect_label}" 79 | body = inspect_matchers_body 80 | unless body =~ /^\s*$/ 81 | ins << "\n" << body 82 | end 83 | ins 84 | end 85 | 86 | def inspect_label 87 | "#{self.class.name.split("::").last} (#{@matchers.size} matchers)" 88 | end 89 | 90 | def inspect_matchers_body 91 | @matchers.map{ |m| m.inspect}.join("\n") 92 | end 93 | 94 | def depth 95 | @parent.send(:depth) + 1 96 | end 97 | 98 | private 99 | def inject_root_methods(code = nil, &blk) 100 | code ? root.methods_module.module_eval(code) : root.methods_module.module_eval(&blk) 101 | end 102 | 103 | def inject_root_ivar(obj) 104 | root.inject_root_ivar(obj) 105 | end 106 | 107 | def add(matcher) 108 | @matchers << matcher unless matcher.usable?(@matchers.last) 109 | @matchers.last 110 | end 111 | 112 | def to_code 113 | @matchers.map{ |m| "# #{m.class}\n" << m.to_code }.join("\n") << "\n" 114 | end 115 | 116 | def root 117 | @router.root 118 | end 119 | 120 | def use_named_captures? 121 | //.respond_to?(:names) 122 | end 123 | end 124 | end -------------------------------------------------------------------------------- /lib/http_router/node/abstract_request_node.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class AbstractRequestNode < Node 4 | attr_reader :request_method, :tests 5 | 6 | def initialize(route, parent, tests, request_method) 7 | @request_method = request_method 8 | @tests = case tests 9 | when Array then tests 10 | when Set then tests.to_a 11 | else [tests] 12 | end 13 | super(route, parent) 14 | end 15 | 16 | def usable?(other) 17 | other.class == self.class && other.tests == tests && other.request_method == request_method 18 | end 19 | 20 | def to_code 21 | "if #{@tests.map { |test| "#{test.inspect} === request.rack_request.#{request_method}" } * ' or '} 22 | #{super} 23 | end" 24 | end 25 | 26 | def inspect_label 27 | "#{self.class.name.split("::").last} #{tests.inspect} (#{@matchers.size} matchers)" 28 | end 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /lib/http_router/node/free_regex.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class FreeRegex < Node 4 | attr_reader :matcher 5 | def initialize(router, parent, matcher) 6 | @matcher = matcher 7 | super(router, parent) 8 | end 9 | 10 | def to_code 11 | id = root.next_counter 12 | "whole_path#{id} = \"/\#{request.joined_path}\" 13 | if match = #{matcher.inspect}.match(whole_path#{id}) and match[0].size == whole_path#{id}.size 14 | request.extra_env['router.regex_match'] = match 15 | old_path = request.path 16 | request.path = [''] 17 | " << (use_named_captures? ? 18 | "match.names.size.times{|i| request.params << match[i + 1]} if match.respond_to?(:names) && match.names" : "") << " 19 | #{super} 20 | request.path = old_path 21 | request.extra_env.delete('router.regex_match') 22 | " << (use_named_captures? ? 23 | "params.slice!(-match.names.size, match.names.size)" : "" 24 | ) << " 25 | end" 26 | end 27 | 28 | def usable?(other) 29 | other.class == self.class && other.matcher == matcher 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/http_router/node/glob.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Glob < Node 4 | alias_method :node_to_code, :to_code 5 | def usable?(other) 6 | other.class == self.class 7 | end 8 | 9 | def to_code 10 | id = root.next_counter 11 | "request.params << (globbed_params#{id} = []) 12 | until request.path.empty? 13 | globbed_params#{id} << request.path.shift 14 | #{super} 15 | end 16 | request.path[0,0] = globbed_params#{id}" 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /lib/http_router/node/glob_regex.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class GlobRegex < Glob 4 | attr_reader :matcher 5 | def initialize(router, parent, matcher) 6 | @matcher = matcher 7 | super router, parent 8 | end 9 | 10 | def usable?(other) 11 | other.class == self.class && other.matcher == matcher 12 | end 13 | 14 | def to_code 15 | id = root.next_counter 16 | "request.params << (globbed_params#{id} = []) 17 | remaining_parts = request.path.dup 18 | while !remaining_parts.empty? and match = remaining_parts.first.match(#{@matcher.inspect}) and match[0] == remaining_parts.first 19 | globbed_params#{id} << remaining_parts.shift 20 | request.path = remaining_parts 21 | #{node_to_code} 22 | end 23 | request.path[0,0] = request.params.pop" 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/http_router/node/host.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Host < AbstractRequestNode 4 | def initialize(router, parent, hosts) 5 | super(router, parent, hosts, :host) 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/http_router/node/lookup.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Lookup < Node 4 | def initialize(router, parent) 5 | @map = {} 6 | super(router, parent) 7 | end 8 | 9 | def add(part) 10 | Node.new(@router, self, @map[part] ||= []) 11 | end 12 | 13 | def usable?(other) 14 | other.class == self.class 15 | end 16 | 17 | def inspect_matchers_body 18 | @map.map { |key, values| 19 | ins = "#{' ' * depth}when #{key.inspect}:\n" 20 | ins << values.map{|v| v.inspect}.join("\n") }.join("\n") 21 | end 22 | 23 | def inspect_label 24 | "#{self.class.name}" 25 | end 26 | 27 | def to_code 28 | part_name = "part#{root.next_counter}" 29 | "unless request.path_finished? 30 | #{part_name} = request.path.shift 31 | case #{part_name} 32 | #{@map.map{|k, v| "when #{k.inspect}; #{v.map(&:to_code) * "\n"};"} * "\n"} 33 | end 34 | request.path.unshift #{part_name} 35 | end" 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/http_router/node/path.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Path < Node 4 | attr_reader :route, :param_names, :dynamic, :path 5 | alias_method :dynamic?, :dynamic 6 | def initialize(router, parent, route, path, param_names = []) 7 | @route, @path, @param_names, @dynamic = route, path, param_names, !param_names.empty? 8 | @route.add_path(self) 9 | 10 | raise AmbiguousVariableException, "You have duplicate variable names present: #{duplicates.join(', ')}" if param_names.uniq.size != param_names.size 11 | super router, parent 12 | router.uncompile 13 | end 14 | 15 | def hashify_params(params) 16 | @dynamic && params ? Hash[param_names.zip(params)] : {} 17 | end 18 | 19 | def to_code 20 | path_ivar = inject_root_ivar(self) 21 | "#{"if !callback && request.path.size == 1 && request.path.first == '' && (request.rack_request.head? || request.rack_request.get?) && request.rack_request.path_info[-1] == ?/ 22 | response = ::Rack::Response.new 23 | response.redirect(request.rack_request.path_info[0, request.rack_request.path_info.size - 1], 302) 24 | return response.finish 25 | end" if router.redirect_trailing_slash?} 26 | 27 | #{"if request.#{router.ignore_trailing_slash? ? 'path_finished?' : 'path.empty?'}" unless route.match_partially} 28 | if callback 29 | request.called = true 30 | callback.call(Response.new(request, #{path_ivar})) 31 | else 32 | env = request.rack_request.env 33 | env['router.request'] = request 34 | env['router.params'] ||= {} 35 | #{"env['router.params'].merge!(Hash[#{param_names.inspect}.zip(request.params)])" if dynamic?} 36 | @router.rewrite#{"_partial" if route.match_partially}_path_info(env, request) 37 | response = @router.process_destination_path(#{path_ivar}, env) 38 | return response unless router.pass_on_response(response) 39 | end 40 | #{"end" unless route.match_partially}" 41 | end 42 | 43 | def usable?(other) 44 | other == self 45 | end 46 | 47 | def inspect_label 48 | "Path: #{path.inspect} for route #{route.name || 'unnamed route'} to #{route.dest.inspect}" 49 | end 50 | 51 | def duplicates 52 | param_names.group_by { |e| e }.select { |k, v| v.size > 1 }.map(&:first) 53 | end 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /lib/http_router/node/regex.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Regex < Node 4 | alias_method :node_to_code, :to_code 5 | attr_reader :matcher, :splitting_indicies, :capturing_indicies, :ordered_indicies 6 | 7 | def initialize(router, parent, matcher, capturing_indicies, splitting_indicies = nil) 8 | @matcher, @capturing_indicies, @splitting_indicies = matcher, capturing_indicies, splitting_indicies 9 | @ordered_indicies = [] 10 | @ordered_indicies.concat(capturing_indicies.map{|i| [i, :capture]}) if capturing_indicies 11 | @ordered_indicies.concat(splitting_indicies.map{|i| [i, :split]}) if splitting_indicies 12 | @ordered_indicies.sort! 13 | super(router, parent) 14 | end 15 | 16 | def usable?(other) 17 | other.class == self.class && other.matcher == matcher && other.splitting_indicies == splitting_indicies && other.capturing_indicies == capturing_indicies 18 | end 19 | 20 | def to_code 21 | params_size = @splitting_indicies.size + @capturing_indicies.size 22 | "if match = #{@matcher.inspect}.match(request.path.first) and match.begin(0).zero? 23 | part = request.path.shift\n" << param_capturing_code << 24 | "#{super} 25 | request.path.unshift part 26 | #{params_size == 1 ? "request.params.pop" : "request.params.slice!(#{-params_size}, #{params_size})"} 27 | end" 28 | end 29 | 30 | def param_capturing_code 31 | @ordered_indicies.map{|(i, type)| 32 | case type 33 | when :capture then "request.params << match[#{i}]\n" 34 | when :split then "request.params << match[#{i}].split(/\\//)\n" 35 | end 36 | }.join("") 37 | end 38 | end 39 | end 40 | end -------------------------------------------------------------------------------- /lib/http_router/node/request_method.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class RequestMethod < AbstractRequestNode 4 | def initialize(router, parent, request_methods) 5 | super(router, parent, request_methods, :request_method) 6 | end 7 | 8 | def to_code 9 | "if #{@tests.map { |test| "#{test.inspect} === request.rack_request.#{request_method}" } * ' or '} 10 | #{super} 11 | end 12 | request.acceptable_methods.merge(#{@tests.inspect})" 13 | end 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/http_router/node/root.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Root < Node 4 | attr_reader :methods_module, :compiled 5 | alias_method :compiled?, :compiled 6 | def initialize(router) 7 | super(router, nil) 8 | @counter, @methods_module = 0, Module.new 9 | end 10 | 11 | def uncompile 12 | instance_eval "undef :call; def call(req); raise 'uncompiled root'; end", __FILE__, __LINE__ if compiled? 13 | end 14 | 15 | def next_counter 16 | @counter += 1 17 | end 18 | 19 | def inject_root_ivar(obj) 20 | name = :"@ivar_#{@counter += 1}" 21 | root.instance_variable_set(name, obj) 22 | name 23 | end 24 | 25 | def depth 26 | 0 27 | end 28 | 29 | def inspect_label 30 | "Root (#{@matchers.size} matchers)" 31 | end 32 | 33 | def compile(routes) 34 | routes.each {|route| add_route(route)} 35 | root.extend(root.methods_module) 36 | instance_eval "def call(request, &callback)\n#{to_code}\nnil\nend" 37 | @compiled = true 38 | end 39 | 40 | private 41 | def add_route(route) 42 | paths = if route.path_for_generation.nil? 43 | route.match_partially = true 44 | [] 45 | elsif route.path_for_generation.is_a?(Regexp) 46 | [route.path_for_generation] 47 | else 48 | path_for_generation = route.path_for_generation.dup 49 | start_index, end_index = 0, 1 50 | raw_paths, chars = [""], path_for_generation.split('') 51 | until chars.empty? 52 | case chars.first[0] 53 | when ?( 54 | chars.shift 55 | (start_index...end_index).each { |path_index| raw_paths << raw_paths[path_index].dup } 56 | start_index = end_index 57 | end_index = raw_paths.size 58 | when ?) 59 | chars.shift 60 | start_index -= end_index - start_index 61 | else 62 | c = if chars[0][0] == ?\\ && (chars[1][0] == ?( || chars[1][0] == ?)); chars.shift; chars.shift; else; chars.shift; end 63 | (start_index...end_index).each { |path_index| raw_paths[path_index] << c } 64 | end 65 | end 66 | raw_paths 67 | end 68 | paths.reverse! 69 | if paths.empty? 70 | add_non_path_to_tree(route, @router.root, nil, []) 71 | else 72 | Generator.new(route, paths).each_path do |path_generator| 73 | case path_generator.path 74 | when Regexp 75 | path_generator.param_names = path_generator.path.names.map(&:to_sym) if path_generator.path.respond_to?(:names) 76 | add_non_path_to_tree(route, add_free_match(path_generator.path), path_generator.path, path_generator.param_names) 77 | else 78 | node = self 79 | path_generator.path.split(/\//).each do |part| 80 | next if part == '' 81 | parts = part.scan(/\\.|[:*][a-z0-9_]+|[^:*\\]+/) 82 | node = parts.size == 1 ? add_normal_part(route, node, part, path_generator) : add_complex_part(route, node, parts, path_generator) 83 | end 84 | add_non_path_to_tree(route, node, path_generator.path, path_generator.param_names) 85 | end 86 | end 87 | end 88 | end 89 | 90 | def add_normal_part(route, node, part, path_generator) 91 | name = part[1, part.size] 92 | node = case part[0] 93 | when ?\\ 94 | node.add_lookup(part[1].chr) 95 | when ?: 96 | path_generator.param_names << name.to_sym 97 | route.matches_with(name) ? node.add_spanning_match(route.matches_with(name)) : node.add_variable 98 | when ?* 99 | path_generator.param_names << name.to_sym 100 | route.matches_with(name) ? node.add_glob_regexp(route.matches_with(name)) : node.add_glob 101 | else 102 | node.add_lookup(part) 103 | end 104 | end 105 | 106 | def add_complex_part(route, node, parts, path_generator) 107 | capturing_indicies, splitting_indicies, captures, spans = [], [], 0, false 108 | regex = parts.inject('') do |reg, part| 109 | reg << case part[0] 110 | when ?\\ then Regexp.quote(part[1].chr) 111 | when ?:, ?* 112 | spans = true if part[0] == ?* 113 | captures += 1 114 | (part[0] == ?* ? splitting_indicies : capturing_indicies) << captures 115 | name = part[1, part.size].to_sym 116 | path_generator.param_names << name.to_sym 117 | if spans 118 | route.matches_with(name) ? "((?:#{route.matches_with(name)}\\/?)+)" : '(.*?)' 119 | else 120 | "(#{(route.matches_with(name) || '[^/]*?')})" 121 | end 122 | else 123 | Regexp.quote(part) 124 | end 125 | end 126 | spans ? node.add_spanning_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies) : 127 | node.add_match(Regexp.new("#{regex}$"), capturing_indicies, splitting_indicies) 128 | end 129 | 130 | def add_non_path_to_tree(route, node, path, param_names) 131 | node = node.add_host([route.host, route.other_hosts].flatten.compact) if route.host or route.other_hosts 132 | node = node.add_user_agent(route.user_agent) if route.user_agent 133 | node = node.add_scheme(route.scheme) if route.scheme 134 | node = node.add_request_method(route.request_methods) if route.request_methods 135 | path_obj = node.add_destination(route, path, param_names) 136 | path_obj 137 | end 138 | 139 | end 140 | end 141 | end -------------------------------------------------------------------------------- /lib/http_router/node/scheme.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Scheme < AbstractRequestNode 4 | def initialize(router, parent, schemes) 5 | super(router, parent, schemes, :scheme) 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/http_router/node/spanning_regex.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class SpanningRegex < Regex 4 | def to_code 5 | params_count = @ordered_indicies.size 6 | whole_path_var = "whole_path#{root.next_counter}" 7 | "#{whole_path_var} = request.joined_path 8 | if match = #{@matcher.inspect}.match(#{whole_path_var}) and match.begin(0).zero? 9 | _#{whole_path_var} = request.path.dup 10 | " << param_capturing_code << " 11 | remaining_path = #{whole_path_var}[match[0].size + (#{whole_path_var}[match[0].size] == ?/ ? 1 : 0), #{whole_path_var}.size] 12 | request.path = remaining_path.split('/') 13 | #{node_to_code} 14 | request.path = _#{whole_path_var} 15 | request.params.slice!(#{-params_count}, #{params_count}) 16 | end 17 | " 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/http_router/node/user_agent.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class UserAgent < AbstractRequestNode 4 | def initialize(router, parent, user_agents) 5 | super(router, parent, user_agents, :user_agent) 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/http_router/node/variable.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Node 3 | class Variable < Node 4 | def usable?(other) 5 | other.class == self.class 6 | end 7 | 8 | def to_code 9 | "unless request.path_finished? 10 | request.params << request.path.shift 11 | #{super} 12 | request.path.unshift request.params.pop 13 | end" 14 | end 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/http_router/regex_route_generation.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | module RegexRouteGeneration 3 | def url_with_params(*a) 4 | url_args_processing(a) do |args, options| 5 | respond_to?(:raw_url) or raise InvalidRouteException 6 | raw_url(args, options) 7 | end 8 | end 9 | end 10 | end -------------------------------------------------------------------------------- /lib/http_router/request.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Request 3 | attr_accessor :path, :params, :rack_request, :extra_env, :continue, :passed_with, :called 4 | attr_reader :acceptable_methods 5 | alias_method :rack, :rack_request 6 | alias_method :called?, :called 7 | 8 | def initialize(path, rack_request) 9 | @rack_request = rack_request 10 | @path = URI::DEFAULT_PARSER.unescape(path).split(/\//) 11 | @path.shift if @path.first == '' 12 | @path.push('') if path[-1] == ?/ 13 | @extra_env = {} 14 | @params = [] 15 | @acceptable_methods = Set.new 16 | end 17 | 18 | def joined_path 19 | @path * '/' 20 | end 21 | 22 | def to_s 23 | "request path, #{path.inspect}" 24 | end 25 | 26 | def path_finished? 27 | @path.size == 0 or @path.size == 1 && @path.first == '' 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/http_router/response.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | class Response < Struct.new(:request, :path) 3 | attr_reader :params 4 | def initialize(request, path) 5 | super(request, path) 6 | @params = path.hashify_params(request.params) 7 | end 8 | 9 | def route 10 | path.route 11 | end 12 | 13 | def param_values 14 | request.params 15 | end 16 | end 17 | end -------------------------------------------------------------------------------- /lib/http_router/route.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | class HttpRouter 4 | class Route 5 | # The list of HTTP request methods supported by HttpRouter. 6 | VALID_HTTP_VERBS = %w{GET POST PUT DELETE HEAD OPTIONS TRACE PATCH OPTIONS LINK UNLINK} 7 | VALID_HTTP_VERBS_WITHOUT_GET = VALID_HTTP_VERBS - %w{GET} 8 | 9 | attr_reader :default_values, :other_hosts, :paths, :request_methods, :name 10 | attr_accessor :match_partially, :router, :host, :user_agent, :ignore_trailing_slash, 11 | :path_for_generation, :path_validation_regex, :generator, :scheme, :original_path, :dest 12 | 13 | def create_clone(new_router) 14 | r = clone 15 | r.dest = (begin; dest.clone; rescue; dest; end) 16 | r 17 | end 18 | 19 | def to_s 20 | "#" 21 | end 22 | 23 | def matches_with(var_name) 24 | @match_with && @match_with[:"#{var_name}"] 25 | end 26 | 27 | def name=(name) 28 | @name = name 29 | router.named_routes[name] << self if router 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/http_router/route_helper.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | module RouteHelper 3 | def path 4 | @route.path_for_generation 5 | end 6 | 7 | def path=(path) 8 | @original_path = path 9 | if path.respond_to?(:[]) and path[/[^\\]\*$/] 10 | @match_partially = true 11 | @path_for_generation = path[0..path.size - 2] 12 | else 13 | @path_for_generation = path 14 | end 15 | end 16 | 17 | def name(name = nil) 18 | if name 19 | self.name = name 20 | self 21 | else 22 | @name 23 | end 24 | end 25 | 26 | def add_default_values(hash) 27 | @default_values ||= {} 28 | @default_values.merge!(hash) 29 | end 30 | 31 | def add_match_with(matchers) 32 | @match_with ||= {} 33 | @match_with.merge!(matchers) 34 | end 35 | 36 | def add_other_host(hosts) 37 | (@other_hosts ||= []).concat(hosts) 38 | end 39 | 40 | def add_path(path) 41 | (@paths ||= []) << path 42 | end 43 | 44 | def add_request_method(methods) 45 | @request_methods ||= Set.new 46 | methods = [methods] unless methods.is_a?(Array) 47 | methods.each do |method| 48 | method = method.to_s.upcase 49 | unless Route::VALID_HTTP_VERBS.include?(method) 50 | raise ArgumentError, "Unsupported HTTP request method: #{method}" 51 | end 52 | @request_methods << method 53 | end 54 | end 55 | 56 | def process_opts(opts) 57 | if opts[:conditions] 58 | opts.merge!(opts[:conditions]) 59 | opts.delete(:conditions) 60 | end 61 | opts.each do |k, v| 62 | if respond_to?(:"#{k}=") 63 | send(:"#{k}=", v) 64 | elsif respond_to?(:"add_#{k}") 65 | send(:"add_#{k}", v) 66 | else 67 | add_match_with(k => v) 68 | end 69 | end 70 | end 71 | 72 | def to(dest = nil, &dest_block) 73 | @dest = dest || dest_block || raise("you didn't specify a destination") 74 | if @dest.respond_to?(:url_mount=) 75 | urlmount = UrlMount.new(@path_for_generation, @default_values || {}) # TODO url mount should accept nil here. 76 | urlmount.url_mount = @router.url_mount if @router.url_mount 77 | @dest.url_mount = urlmount 78 | end 79 | self 80 | end 81 | 82 | # Creates helper methods for each supported HTTP verb. 83 | Route::VALID_HTTP_VERBS_WITHOUT_GET.each do |request_method| 84 | define_method(request_method.downcase) do 85 | add_request_method(request_method) 86 | self 87 | end 88 | end 89 | 90 | def get 91 | add_request_method("GET") 92 | add_request_method("HEAD") 93 | self 94 | end 95 | 96 | def redirect(path, status = 302) 97 | raise ArgumentError, "Status has to be an integer between 300 and 399" unless (300..399).include?(status) 98 | to { |env| 99 | params = env['router.params'] 100 | response = ::Rack::Response.new 101 | response.redirect(eval(%|"#{path}"|), status) 102 | response.finish 103 | } 104 | end 105 | 106 | # Sets the destination of this route to serve static files from either a directory or a single file. 107 | def static(root) 108 | @match_partially = true if File.directory?(root) 109 | to File.directory?(root) ? 110 | ::Rack::File.new(root) : 111 | proc {|env| 112 | env['PATH_INFO'] = File.basename(root) 113 | ::Rack::File.new(File.dirname(root)).call(env) 114 | } 115 | end 116 | end 117 | end -------------------------------------------------------------------------------- /lib/http_router/util.rb: -------------------------------------------------------------------------------- 1 | class HttpRouter 2 | module Util 3 | 4 | end 5 | end -------------------------------------------------------------------------------- /lib/http_router/version.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | class HttpRouter #:nodoc 3 | VERSION = '0.11.2' 4 | end -------------------------------------------------------------------------------- /test/common/generate.txt: -------------------------------------------------------------------------------- 1 | {"a": "/:var"} 2 | ["/test", "a", {"var":"test"}] 3 | ["/test", "a", ["test"]] 4 | 5 | {"a": "/"} 6 | {"b": "/test"} 7 | {"c": "/test/time"} 8 | {"d": "/one/more/what"} 9 | {"e": "/test.html"} 10 | ["/", "a"] 11 | ["/test", "b"] 12 | ["/test/time", "c"] 13 | ["/one/more/what", "d"] 14 | ["/test.html", "e"] 15 | 16 | {"a": "/:var"} 17 | ["/test", "a", {"var":"test"}] 18 | ["/test", "a", ["test"]] 19 | 20 | {"a": "/:var"} 21 | ["/test?query=string", "a", {"var":"test", "query": "string"}] 22 | ["/test?query=string", "a", ["test", {"query": "string"}]] 23 | 24 | {"a": "/:var/:baz"} 25 | ["/one/two", "a", {"var":"one", "baz": "two"}] 26 | ["/one/two", "a", ["one", "two"]] 27 | 28 | {"a": "/test.:format"} 29 | ["/test.html", "a", {"format":"html"}] 30 | ["/test.html", "a", ["html"]] 31 | 32 | {"a": "/test(.:format)"} 33 | ["/test.html", "a", {"format":"html"}] 34 | ["/test.html", "a", ["html"]] 35 | ["/test", "a"] 36 | 37 | {"a": "/:var.:format"} 38 | ["/test.html", "a", {"var": "test", "format":"html"}] 39 | ["/test.html", "a", ["test", "html"]] 40 | ["/test.html", "a", ["test", {"format": "html"}]] 41 | [null, "a", {"format": "html"}] 42 | 43 | {"a": "/:var(.:format)"} 44 | ["/test.html", "a", {"var": "test", "format":"html"}] 45 | ["/test.html", "a", ["test", "html"]] 46 | ["/test.html", "a", ["test", {"format": "html"}]] 47 | ["/test", "a", ["test"]] 48 | ["/test", "a", {"var": "test"}] 49 | [null, "a", {"format": "html"}] 50 | [null, "a"] 51 | 52 | {"a": "/:var1(/:var2)"} 53 | ["/foo/bar", "a", {"var1": "foo", "var2":"bar"}] 54 | [null, "a", ["foo", {"var1": "bar"}]] 55 | ["/foo", "a", {"var1": "foo"}] 56 | ["/foo", "a", ["foo"]] 57 | ["/foo", "a", ["foo", null]] 58 | 59 | {"a": "/:var1(/:var2.:format)"} 60 | ["/test/test2.html", "a", {"var1": "test", "var2": "test2", "format":"html"}] 61 | ["/test/test2.html", "a", ["test", "test2", "html"]] 62 | ["/test", "a", ["test"]] 63 | 64 | {"a": "/:var1(/:var2(/:var3))"} 65 | ["/var/fooz/baz", "a", {"var1": "var", "var2":"fooz", "var3": "baz"}] 66 | ["/var/fooz", "a", {"var1": "var", "var2":"fooz"}] 67 | ["/var", "a", {"var1": "var"}] 68 | ["/var/fooz/baz", "a", ["var", "fooz", "baz"]] 69 | ["/var/fooz", "a", ["var", "fooz"]] 70 | ["/var", "a", ["var"]] 71 | 72 | {"a": {"path":"/:var", "default":{"page":1}}} 73 | ["/123?page=1", "a", [123]] 74 | 75 | {"a": {"path":"/:page/:entry", "default":{"page":1}}} 76 | ["/1/123", "a", {"entry": "123"}] 77 | 78 | {"a": "/:var"} 79 | ["/%C3%A4", "a", ["ä"]] 80 | ["/%C3%A4", "a", {"var": "ä"}] 81 | 82 | {"a": {"path": ":var", "var": {"regex": "\\d+"}}} 83 | [null, "a", "asd"] 84 | ["/123", "a", "123"] 85 | 86 | {"a": "/var"} 87 | ["/var?foo%5B%5D=baz&foo%5B%5D=bar", "a", {"foo": ["baz", "bar"]}] 88 | 89 | {"a": "/var"} 90 | ["/var?foo%5Baz%5D=baz", "a", {"foo": {"az": "baz"}}] 91 | 92 | {"a": "/var"} 93 | ["/var?foo%5Baz%5D%5B%5D=baz", "a", {"foo": {"az": ["baz"]}}] 94 | -------------------------------------------------------------------------------- /test/common/http_recognize.txt: -------------------------------------------------------------------------------- 1 | {"nothing":{"path":"/", "conditions": {"request_method": "GET"}}} 2 | {"post":{"path":"/test", "conditions": {"request_method": "POST"}}} 3 | {"put":{"path":"/test", "conditions": {"request_method": "PUT"}}} 4 | ["post", {"path": "/test", "method": "POST"}] 5 | [[405, {"Allow": "POST, PUT"}], {"path": "/test", "method": "GET"}] 6 | 7 | {"router": {"path": "/test", "conditions": {"request_method": ["POST", "GET"]}}} 8 | ["router", {"path": "/test", "method": "POST"}] 9 | ["router", {"path": "/test", "method": "GET"}] 10 | [[405, {"Allow": "GET, POST"}], {"path": "/test", "method": "PUT"}] 11 | 12 | {"get": {"path": "/test(.:format)", "conditions": {"request_method": "GET"}}} 13 | {"post": {"path": "/test(.:format)", "conditions": {"request_method": "POST"}}} 14 | {"delete": {"path": "/test(.:format)", "conditions": {"request_method": "DELETE"}}} 15 | ["get", {"path": "/test", "method": "GET"}] 16 | ["post", {"path": "/test", "method": "POST"}] 17 | ["delete", {"path": "/test", "method": "DELETE"}] 18 | ["get", {"path": "/test.html", "method": "GET"}, {"format": "html"}] 19 | ["post", {"path": "/test.html", "method": "POST"}, {"format": "html"}] 20 | ["delete", {"path": "/test.html", "method": "DELETE"}, {"format": "html"}] 21 | [[405, {"Allow": "DELETE, GET, POST"}], {"path": "/test", "method": "PUT"}] 22 | 23 | {"post": {"path": "/test", "conditions": {"request_method": "POST"}}} 24 | {"post_2": {"path": "/test/post", "conditions": {"request_method": "POST"}}} 25 | {"get": {"path": "/test", "conditions": {"request_method": "GET"}}} 26 | {"get_2": {"path": "/test/post", "conditions": {"request_method": "GET"}}} 27 | {"any_2": "/test/post"} 28 | {"any": "/test"} 29 | ["post", {"path": "/test", "method": "POST"}] 30 | ["get", {"path": "/test", "method": "GET"}] 31 | ["any", {"path": "/test", "method": "PUT"}] 32 | ["post_2", {"path": "/test/post", "method": "POST"}] 33 | ["get_2", {"path": "/test/post", "method": "GET"}] 34 | ["any_2", {"path": "/test/post", "method": "PUT"}] 35 | 36 | {"post": {"path": "/test", "conditions": {"request_method": "POST"}}} 37 | {"any": "/test"} 38 | ["post", {"path": "/test", "method": "POST"}] 39 | ["any", {"path": "/test", "method": "PUT"}] 40 | 41 | {"host2_post": {"path": "/test", "conditions": {"request_method": "POST", "host": "host2"}}} 42 | {"host2_get": {"path": "/test", "conditions": {"request_method": "GET", "host": "host2"}}} 43 | {"host2": {"path": "/test", "conditions": {"host": "host2"}}} 44 | {"post": {"path": "/test", "conditions": {"request_method": "POST"}}} 45 | ["host2", {"path": "http://host2/test", "method": "PUT"}] 46 | ["post", {"path": "http://host1/test", "method": "POST"}] 47 | ["host2_get", {"path": "http://host2/test", "method": "GET"}] 48 | ["host2_post", {"path": "http://host2/test", "method": "POST"}] 49 | 50 | {"with": {"path": "/test", "conditions": {"request_method": "GET", "host": {"regex": "host1"}}}} 51 | {"without": {"path": "/test", "conditions": {"request_method": "GET"}}} 52 | ["without", "http://host2/test"] 53 | ["with", "http://host2.host1.com/test"] 54 | 55 | {"http": {"path": "/test", "conditions": {"scheme": "http"}}} 56 | {"https": {"path": "/test", "conditions": {"scheme": "https"}}} 57 | ["http", {"path": "/test", "scheme": "http"}] 58 | ["https", {"path": "/test", "scheme": "https"}] 59 | -------------------------------------------------------------------------------- /test/common/recognize.txt: -------------------------------------------------------------------------------- 1 | {"route": ":one"} 2 | ["route", "/two", {"one": "two"}] 3 | 4 | {"route": "test/:one"} 5 | ["route", "/test/three", {"one": "three"}] 6 | 7 | {"static": "one"} 8 | {"dynamic": ":one"} 9 | ["dynamic", "/two", {"one": "two"}] 10 | ["static", "/one"] 11 | 12 | [{"variable": ":var/one"}, {"static": "one"}] 13 | ["variable", "/two/one", {"var": "two"}] 14 | ["static", "/one"] 15 | [null, "/two"] 16 | 17 | [{"dynamic": "/foo/:id"}, {"static": "/foo"}] 18 | ["dynamic", "/foo/id", {"id": "id"}] 19 | ["static", "/foo"] 20 | 21 | [{"static": "/foo/foo"}, {"dynamic": "/:foo/foo2"}] 22 | ["dynamic", "/foo/foo2", {"foo": "foo"}] 23 | ["static", "/foo/foo"] 24 | 25 | [{"route": ":var"}] 26 | ["route", "/%E6%AE%BA%E3%81%99", {"var": "殺す"}] 27 | 28 | [{"route": {"path":{"regex": "/(test123|\\d+)"}}}] 29 | ["route", "/test123"] 30 | ["route", "/123"] 31 | [null, "/test123andmore"] 32 | [null, "/lesstest123"] 33 | 34 | [{"route": "/test.:format"}] 35 | ["route", "/test.html", {"format": "html"}] 36 | 37 | [{"route": "/test(.:format)"}] 38 | ["route", "/test.html", {"format": "html"}] 39 | ["route", "/test"] 40 | 41 | {"route": "/"} 42 | ["route", "/"] 43 | 44 | [{"route": "(.:format)"}] 45 | ["route", "/.html", {"format": "html"}] 46 | ["route", "/"] 47 | 48 | [{"route": "/:test.:format"}] 49 | ["route", "/foo.bar", {"format": "bar", "test": "foo"}] 50 | 51 | [{"route": "/:test(.:format)"}] 52 | ["route", "/foo", {"test": "foo"}] 53 | ["route", "/foo.bar", {"format": "bar", "test": "foo"}] 54 | 55 | [{"route": {"path": "/:test(.:format)", "format": {"regex": "[^\\.]+"}}}] 56 | ["route", "/asd@asd.com.json", {"test": "asd@asd.com", "format": "json"}] 57 | 58 | [{"route": "/test/*variable"}] 59 | ["route", "/test/one/two/three", {"variable": ["one", "two", "three"]}] 60 | 61 | [{"route": "test/*variable/test"}] 62 | [null, "/test/one/two/three"] 63 | ["route", "/test/one/two/three/test", {"variable": ["one", "two", "three"]}] 64 | 65 | [{"route": "test/*variable/test/*variable2"}] 66 | [null, "/test/one/two/three"] 67 | ["route", "/test/one/two/three/test/four/five/six", {"variable": ["one", "two", "three"], "variable2": ["four", "five", "six"]}] 68 | 69 | [{"route": "/test/:test-*variable.:format"}] 70 | ["route", "/test/one-two/three/four/five.six", {"test": "one", "variable": ["two", "three", "four", "five"], "format": "six"}] 71 | 72 | [{"route": {"path": "test/*variable", "variable": {"regex": "[a-z]+"}}}] 73 | [null, "/test/asd/123"] 74 | [null, "/test/asd/asd123"] 75 | ["route", "/test/asd/qwe", {"variable": ["asd", "qwe"]}] 76 | 77 | [{"route": {"path": "test/*variable/test", "variable": {"regex": "[a-z]+"}}}] 78 | [null, "/test/asd/123"] 79 | [null, "/test/asd/asd123"] 80 | [null, "/test/asd/qwe"] 81 | ["route", "/test/asd/qwe/test", {"variable": ["asd", "qwe"]}] 82 | 83 | [{"route": {"path": "test/*variable/:test", "variable": {"regex": "[a-z]+"}}}] 84 | ["route", "/test/asd/qwe/help", {"variable": ["asd", "qwe"], "test": "help"}] 85 | 86 | [{"route": {"path": "test/*variable.:format"}}] 87 | ["route", "/test/asd/qwe.html", {"variable": ["asd", "qwe"], "format": "html"}] 88 | 89 | [{"route": {"path": "test/*variable.:format", "variable": {"regex": "[a-z]+"}}}] 90 | [null, "/test/asd/123"] 91 | [null, "/test/asd/asd123"] 92 | [null, "/test/asd/qwe"] 93 | ["route", "/test/asd/qwe.html", {"variable": ["asd", "qwe"], "format": "html"}] 94 | 95 | 96 | [{"route": {"path": "test/*variable(.:format)", "variable": {"regex": "[a-z]+"}}}] 97 | [null, "/test/asd/123"] 98 | [null, "/test/asd/asd123"] 99 | ["route", "/test/asd/qwe", {"variable": ["asd", "qwe"]}] 100 | ["route", "/test/asd/qwe.html", {"variable": ["asd", "qwe"], "format": "html"}] 101 | 102 | [{"route": {"path": "test/*variable.html"}}] 103 | [null, "/test/asd/123"] 104 | ["route", "/test/asd/qwe.html", {"variable": ["asd", "qwe"]}] 105 | 106 | [{"with_regex": {"path": "/:common_variable/:matched", "matched": {"regex": "\\d+"}}}, {"with_post": {"path": "/:common_variable/:matched", "conditions": {"request_method": "POST"}}}, {"without_regex": "/:common_variable/:unmatched"}] 107 | ["with_regex", "/common/123", {"common_variable": "common", "matched": "123"}] 108 | ["without_regex", "/common/other", {"common_variable": "common", "unmatched": "other"}] 109 | ["with_regex", {"path": "/common/123", "method": "POST"}, {"common_variable": "common", "matched": "123"}] 110 | ["with_post", {"path": "/common/other", "method": "POST"}, {"common_variable": "common", "matched": "other"}] 111 | 112 | [{"regex": {"path":":test/number", "test": {"regex": "\\d+"}}}, {"greedy": ":test/anything"}] 113 | ["regex", "/123/number", {"test": "123"}] 114 | ["greedy", "/123/anything", {"test": "123"}] 115 | 116 | [{"route": {"path": ":test", "test": {"regex": ".*"}}}] 117 | ["route", "/test/", {"test": "test/"}] 118 | 119 | [{"route": {"path": "/:test", "test": {"regex": ".*"}}}] 120 | ["route", "/test.html", {"test": "test.html"}] 121 | 122 | [{"route": {"path": ":test", "test": {"regex": "\\d+"}}}] 123 | ["route", "/123", {"test": "123"}] 124 | [null, "/a123"] 125 | 126 | [{"route": ""}] 127 | ["route", "/"] 128 | ["route", ""] 129 | 130 | [{"route": "/"}] 131 | ["route", "/"] 132 | 133 | [{"route": "/test"}] 134 | ["route", "/test"] 135 | 136 | [{"route": "/test/one"}] 137 | ["route", "/test/one"] 138 | 139 | [{"route": "/test/one/two"}] 140 | ["route", "/test/one/two"] 141 | 142 | [{"route": "/test.html"}] 143 | ["route", "/test.html"] 144 | 145 | [{"route": ".html"}] 146 | ["route", "/.html"] 147 | 148 | [{"route": "one(/two(/three(/four)(/five)))"}] 149 | ["route", "/one"] 150 | ["route", "/one/two"] 151 | ["route", "/one/two/three"] 152 | ["route", "/one/two/three/four"] 153 | ["route", "/one/two/three/five"] 154 | ["route", "/one/two/three/four/five"] 155 | [null, "/one/two/four/five"] 156 | 157 | [{"route": "test\\(:variable\\)"}] 158 | ["route", "/test(hello)", {"variable": "hello"}] 159 | 160 | [{"route": "test\\:variable"}] 161 | ["route", "/test:variable"] 162 | 163 | [{"route": "test\\*variable"}] 164 | ["route", "/test*variable"] 165 | 166 | [{"route": "testvariable\\*"}] 167 | ["route", "/testvariable*"] 168 | 169 | [{"route": "/føø"}] 170 | ["route", "/f%C3%B8%C3%B8"] 171 | 172 | [{"route": "/test*"}] 173 | ["route", "/test/optional", {"PATH_INFO": "/optional"}] 174 | ["route", "/test", {"PATH_INFO": "/"}] 175 | 176 | [{"route": "/*"}] 177 | ["route", "/optional", {"PATH_INFO": "/optional"}] 178 | ["route", "/", {"PATH_INFO": "/"}] 179 | 180 | [{"test": "/test*"}, {"root": "/*"}] 181 | ["test", "/test/optional", {"PATH_INFO": "/optional"}] 182 | ["test", "/test/optional/", {"PATH_INFO": "/optional/"}] 183 | ["root", "/testing/optional", {"PATH_INFO": "/testing/optional"}] 184 | 185 | {"route": "/one-:variable-time"} 186 | ["route", "/one-value-time", {"variable": "value"}] 187 | 188 | [{"route": {"path": "/one-:variable-time", "variable": {"regex": "\\d+"}}}] 189 | ["route", "/one-123-time", {"variable": "123"}] 190 | [null, "/one-value-time"] 191 | 192 | [{"route": {"path": "/one-:variable-time", "variable": {"regex": "\\d+"}}}] 193 | ["route", "/one-123-time", {"variable": "123"}] 194 | [null, "/one-value-time"] 195 | 196 | [{"route": "hey.:greed.html"}] 197 | ["route", "/hey.greedybody.html", {"greed": "greedybody"}] 198 | 199 | [{"r6": "/:v1-:v2-:v3-:v4-:v5-:v6"}, {"r5": "/:v1-:v2-:v3-:v4-:v5"}, {"r4": "/:v1-:v2-:v3-:v4"}, {"r3": "/:v1-:v2-:v3"}, {"r2": "/:v1-:v2"}, {"r1":"/:v1"}] 200 | ["r1", "/one", {"v1": "one"}] 201 | ["r2", "/one-two", {"v1": "one", "v2": "two"}] 202 | ["r3", "/one-two-three", {"v1": "one", "v2":"two", "v3":"three"}] 203 | ["r4", "/one-two-three-four", {"v1": "one", "v2":"two", "v3":"three", "v4":"four"}] 204 | ["r5", "/one-two-three-four-five", {"v1": "one", "v2":"two", "v3":"three", "v4":"four", "v5":"five"}] 205 | ["r6", "/one-two-three-four-five-six", {"v1": "one", "v2":"two", "v3":"three", "v4":"four", "v5":"five", "v6":"six"}] 206 | 207 | {"with_regex": {"path": "/:common_variable.:matched", "matched": {"regex": "\\d+"}}} 208 | {"without_regex": "/:common_variable.:unmatched"} 209 | ["with_regex", "/common.123", {"common_variable": "common", "matched": "123"}] 210 | ["without_regex", "/common.other", {"common_variable": "common", "unmatched": "other"}] 211 | -------------------------------------------------------------------------------- /test/generation.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | require "#{File.dirname(__FILE__)}/generic" 3 | class GenerationTest < AbstractTest 4 | def run_tests 5 | @tests.map(&:case).each do |(expected_result, name, oargs)| 6 | oargs = [oargs] unless oargs.is_a?(Array) 7 | oargs.compact! 8 | oargs.map!{|a| a.is_a?(Hash) ? Hash[a.map{|k,v| [k.to_sym, v]}] : a } 9 | [[:path, ''], [:url_ns, '://localhost'], [:url, 'http://localhost']].each do |(meth, prefix)| 10 | args = oargs.map{|o| o.dup rescue o} 11 | result = begin 12 | path = @router.send(meth, name.to_sym, *args.dup) 13 | path 14 | rescue HttpRouter::InvalidRouteException 15 | nil 16 | rescue HttpRouter::MissingParameterException 17 | nil 18 | end 19 | error("Result #{result.inspect} did not match expectation #{expected_result.inspect}") unless result == (expected_result ? prefix + expected_result : expected_result) 20 | end 21 | end 22 | print '.' 23 | end 24 | end 25 | 26 | GenerationTest.run("#{File.dirname(__FILE__)}/common/generate.txt") 27 | -------------------------------------------------------------------------------- /test/generic.rb: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | class AbstractTest 4 | def self.run(file) 5 | contents = File.read(file) 6 | tests = [] 7 | test = nil 8 | num = 0 9 | contents.each_line do |line| 10 | begin 11 | case line 12 | when /^#/, /^\s*$/ 13 | # skip 14 | when /^( |\t)/ 15 | test.add_test(line, num) 16 | else 17 | if test.nil? || !test.tests.empty? 18 | tests << test if test 19 | test = new(file) 20 | end 21 | test.add_routes(line, num) 22 | end 23 | rescue 24 | warn "There was a problem with #{num}:#{line}" 25 | raise 26 | end 27 | num += 1 28 | end 29 | tests << test 30 | 31 | puts "Running tests (#{name}) (Routes: #{tests.size}, Tests: #{tests.inject(0){|s, t| s+=t.tests.size}})..." 32 | tests.each(&:invoke) 33 | puts "\ndone!" 34 | end 35 | 36 | Info = Struct.new(:case, :original_line, :num) 37 | 38 | attr_reader :routes, :tests 39 | def initialize(file) 40 | @tests = [] 41 | @routes = Info.new([], "", 0) 42 | end 43 | 44 | def error(msg) 45 | raise("Error in case: #{@routes.original_line.strip}:#{@routes.num + 1}\n#{msg}") 46 | end 47 | 48 | def add_test(line, num) 49 | @tests << Info.new(JSON.parse(line), line, num) 50 | end 51 | 52 | def add_routes(line, num) 53 | info = Info.new(JSON.parse(line), line, num) 54 | error("Routes have already been defined without tests") if info.case.is_a?(Array) && !@routes.case.empty? 55 | if info.case.is_a?(Array) 56 | @routes = info 57 | elsif @routes.case.empty? 58 | info.case = [info.case] 59 | @routes = info 60 | else 61 | @routes.case << info.case 62 | end 63 | end 64 | 65 | def interpret_val(val) 66 | case val 67 | when nil 68 | error("Unable to interpret #{val.inspect}") 69 | when Hash 70 | val['regex'] ? Regexp.new(val['regex']) : error("Okay serious, no idea #{val.inspect}") 71 | else 72 | val 73 | end 74 | end 75 | 76 | def run_tests 77 | raise 78 | end 79 | 80 | def invoke 81 | error("invoke called with no tests or routes") if @tests.empty? || @routes.nil? 82 | @router = HttpRouter.new 83 | @routes.case.each do |route_definition| 84 | error("Too many keys! #{route_definition.keys.inspect}") unless route_definition.keys.size == 1 85 | route_name, route_properties = route_definition.keys.first, route_definition.values.first 86 | opts = {:name => route_name.to_sym} 87 | route = case route_properties 88 | when String 89 | @router.add(route_properties, opts) 90 | when Hash 91 | route_path = interpret_val(route_properties.delete("path")) 92 | if route_properties.key?("conditions") 93 | opts[:conditions] = Hash[route_properties.delete("conditions").map{|k, v| [k.to_sym, interpret_val(v)]}] 94 | end 95 | if route_properties.key?("default") 96 | opts[:default_values] = Hash[route_properties.delete("default").map{|k, v| [k.to_sym, interpret_val(v)]}] 97 | end 98 | route_properties.each do |key, val| 99 | opts[key.to_sym] = interpret_val(val) 100 | end 101 | @router.add(route_path, opts) 102 | else 103 | error("Route isn't a String or hash") 104 | end 105 | route.to{|env| [200, {"env-to-test" => env.dup}, [route_name]]} 106 | end 107 | run_tests 108 | print '.' 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'minitest/autorun' 3 | require 'phocus' 4 | 5 | class HttpRouter 6 | module RouteHelper 7 | def default_destination 8 | to proc {|env| ::Rack::Response.new("Routing to #{object_id}").finish} 9 | self 10 | end 11 | end 12 | end 13 | 14 | class MiniTest::Unit::TestCase 15 | def router(opts = nil, &blk) 16 | @router ||= HttpRouter.new(opts, &blk) 17 | if blk 18 | @router.routes.each { |route| route.dest ||= proc {|env| Rack::Response.new("Routing to #{route.object_id}").finish} } 19 | @router.routes.size > 1 ? @router.routes : @router.routes.first 20 | else 21 | @router 22 | end 23 | end 24 | 25 | def assert_body(expect, response) 26 | response = router.call(response) if response.is_a?(Hash) 27 | body = case expect 28 | when Array then [] 29 | when String then "" 30 | else raise 31 | end 32 | response.last.each {|p| body << p} 33 | assert_equal expect, body 34 | end 35 | 36 | def assert_header(header, response) 37 | response = Rack::MockRequest.env_for(response) if response.is_a?(String) 38 | response = router.call(response) if response.is_a?(Hash) 39 | header.each{|k, v| assert_equal v, response[1][k]} 40 | end 41 | 42 | def assert_status(status, response) 43 | response = Rack::MockRequest.env_for(response) if response.is_a?(String) 44 | response = router.call(response) if response.is_a?(Hash) 45 | assert_equal status, response.first 46 | end 47 | 48 | def assert_route(route, request, params = nil, &blk) 49 | route = case route 50 | when String 51 | router.reset! 52 | router.add(route) 53 | else 54 | route 55 | end 56 | route.default_destination if route && route.dest.nil? 57 | request = Rack::MockRequest.env_for(request) if request.is_a?(String) 58 | response = @router.call(request) 59 | if route 60 | dest = "Routing to #{route.object_id}" 61 | assert_equal [dest], response.last.body 62 | if params 63 | raise "Params was nil, but you expected params" if request['router.params'].nil? 64 | assert_equal params.size, request['router.params'].size 65 | params.each { |k, v| assert_equal v, request['router.params'][k] } 66 | elsif !request['router.params'].nil? and !request['router.params'].empty? 67 | raise "Wasn't expecting any parameters, got #{request['router.params'].inspect}" 68 | end 69 | else 70 | assert_equal 404, response.first 71 | end 72 | end 73 | 74 | def assert_generate(path, route, *args) 75 | if route.is_a?(String) 76 | router.reset! 77 | route = router.add(route) 78 | end 79 | route.to{|env| Rack::Response.new("Routing to #{route.to_s}").finish} if route && route.respond_to?(:to) && !route.dest 80 | assert_equal path.gsub('[','%5B').gsub(']','%5D'), router.path(route, *args) 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /test/rack/test_route.rb: -------------------------------------------------------------------------------- 1 | class TestRouteExtensions < MiniTest::Unit::TestCase 2 | 3 | def test_redirect 4 | router.get("/index.html").redirect("/") 5 | response = router.call(Rack::MockRequest.env_for("/index.html")) 6 | assert_header({'Location' => '/'}, response) 7 | assert_status 302, response 8 | end 9 | 10 | def test_redirect_with_params 11 | router.get("/:id.html").redirect('/#{params[:id]}') 12 | response = router.call(Rack::MockRequest.env_for("/123.html")) 13 | assert_header({'Location' => '/123'}, response) 14 | assert_status 302, response 15 | end 16 | 17 | def test_static_directory 18 | router.get("/static").static(File.dirname(__FILE__)) 19 | status, headers, body = router.call(Rack::MockRequest.env_for("/static/#{File.basename(__FILE__)}")) 20 | assert_equal File.join(File.dirname(__FILE__), File.basename(__FILE__)), body.path 21 | end 22 | 23 | def test_static_file 24 | router.get("/static-file").static(__FILE__) 25 | status, headers, body = router.call(Rack::MockRequest.env_for("/static-file")) 26 | assert_equal __FILE__, body.path 27 | end 28 | 29 | def test_custom_status 30 | router.get("/index.html").redirect("/", 303) 31 | response = router.call(Rack::MockRequest.env_for("/index.html")) 32 | assert_header({'Location' => '/'}, response) 33 | assert_status 303, response 34 | end 35 | 36 | def test_raise_error_on_invalid_status 37 | assert_raises(ArgumentError) { router.get("/index.html").redirect("/", 200) } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/recognition.rb: -------------------------------------------------------------------------------- 1 | require "#{File.dirname(__FILE__)}/generic" 2 | class RecognitionTest < AbstractTest 3 | def run_tests 4 | @tests.map(&:case).each do |(name, req, params)| 5 | env = case req 6 | when String 7 | Rack::MockRequest.env_for(req) 8 | when Hash 9 | e = Rack::MockRequest.env_for(req['path']) 10 | e['REQUEST_METHOD'] = req['method'] if req.key?('method') 11 | e['rack.url_scheme'] = req['scheme'] if req.key?('scheme') 12 | e 13 | end 14 | response = @router.call(env) 15 | case name 16 | when nil 17 | error("Expected no response") unless response.first == 404 18 | when Array 19 | name.each_with_index do |part, i| 20 | case part 21 | when Hash then part.keys.all? or error("#{part.inspect} didn't match #{response[i].inspect}") 22 | else part == response[i] or error("#{part.inspect} didn't match #{response[i].inspect}") 23 | end 24 | end 25 | else 26 | error("Expected #{name} for #{req.inspect} got #{response.inspect}") unless response.last == [name] 27 | end 28 | env['router.params'] ||= {} 29 | params ||= {} 30 | if params['PATH_INFO'] 31 | path_info = params.delete("PATH_INFO") 32 | error("path_info #{env['PATH_INFO'].inspect} is not #{path_info.inspect}") unless path_info == env['PATH_INFO'] 33 | end 34 | 35 | env['router.params'].keys.each do |k| 36 | p_v = params.delete(k.to_s) 37 | v = env['router.params'].delete(k.to_sym) 38 | error("I got #{p_v.inspect} but expected #{v.inspect}") unless p_v == v 39 | end 40 | error("Left over expectations: #{params.inspect}") unless params.empty? 41 | error("Left over matched params: #{env['router.params'].inspect}") unless env['router.params'].empty? 42 | end 43 | print '.' 44 | end 45 | end 46 | 47 | RecognitionTest.run("#{File.dirname(__FILE__)}/common/recognize.txt") 48 | RecognitionTest.run("#{File.dirname(__FILE__)}/common/http_recognize.txt") 49 | -------------------------------------------------------------------------------- /test/test_misc.rb: -------------------------------------------------------------------------------- 1 | class TestMisc < MiniTest::Unit::TestCase 2 | 3 | def test_cloning 4 | r1 = HttpRouter.new { add('/test', :name => :test_route).to(:test) } 5 | r2 = r1.clone 6 | 7 | r2.add('/test2', :name => :test).to(:test2) 8 | assert_equal 2, r2.routes.size 9 | 10 | matches, other_methods = r1.recognize(Rack::Request.new(Rack::MockRequest.env_for('/test2'))) 11 | assert_equal nil, matches 12 | assert r2.recognize(Rack::MockRequest.env_for('/test2')).first 13 | assert_equal r1.routes.size, 1 14 | assert_equal r2.routes.size, 2 15 | 16 | r1.add('/another', :name => :test).to(:test2) 17 | 18 | assert_equal r1.routes.size, r2.routes.size 19 | assert_equal '/another', r1.path(:test) 20 | assert_equal '/test2', r2.path(:test) 21 | assert_equal :test, r1.routes.first.dest 22 | assert_equal :test, r2.routes.first.dest 23 | end 24 | 25 | def test_reseting 26 | router = HttpRouter.new 27 | r = router.add('/hi').to(:test) 28 | matches, other_methods = router.recognize(Rack::MockRequest.env_for('/hi')) 29 | assert_equal r, matches.first.route 30 | router.reset! 31 | assert_equal nil, router.recognize(Rack::MockRequest.env_for('/hi')).first 32 | end 33 | 34 | def test_redirect_trailing_slash 35 | r = HttpRouter.new(:redirect_trailing_slash => true) { add('/hi').to(:test) } 36 | response = r.call(Rack::MockRequest.env_for('/hi/')) 37 | assert_equal 302, response.first 38 | assert_equal '/hi', response[1]['Location'] 39 | end 40 | 41 | def test_multi_recognize 42 | r1, r2, r3, r4 = router { 43 | add('/hi/there') 44 | add('/:var/:var2') 45 | add('/hi/:var2') 46 | add('/:var1/there') 47 | } 48 | response = router.recognize(Rack::MockRequest.env_for('/hi/there')) 49 | assert_equal [r1, r2, r3, r4], response.first.map{|resp| resp.path.route} 50 | response = router.recognize(Rack::MockRequest.env_for('/hi/var')) 51 | assert_equal [r2, r3], response.first.map{|resp| resp.path.route} 52 | response = router.recognize(Rack::MockRequest.env_for('/you/there')) 53 | assert_equal [r2, r4], response.first.map{|resp| resp.path.route} 54 | end 55 | 56 | def test_multi_name_gen 57 | r = router 58 | r.add('/', :name => :index).default_destination 59 | r.add('/:name', :name => :index).default_destination 60 | r.add('/:name/:category', :name => :index).default_destination 61 | assert_equal '/', r.path(:index) 62 | assert_equal '/name', r.path(:index, 'name') 63 | assert_equal '/name/category', r.path(:index, 'name', 'category') 64 | end 65 | 66 | def test_yielding_from_recognize 67 | r = HttpRouter.new 68 | r1 = r.add('/:name').default_destination 69 | r2 = r.add('/:name').default_destination 70 | r3 = r.add('/:name').default_destination 71 | matches = [] 72 | r.recognize(Rack::MockRequest.env_for('/test')) { |r| matches << r.route } 73 | assert_equal [r1, r2, r3], matches 74 | end 75 | 76 | def test_regex_generation 77 | r = router 78 | r.add(%r|/test/.*|, :path_for_generation => '/test/:variable', :name => :route).default_destination 79 | assert_equal '/test/var', r.path(:route, "var") 80 | end 81 | 82 | def test_too_many_params 83 | r = router 84 | r.add(%r|/test/.*|, :path_for_generation => '/test/:variable', :name => :route).default_destination 85 | assert_equal '/test/var', r.path(:route, "var") 86 | assert_equal '/test/var', r.path(:route, :variable => "var") 87 | assert_raises(HttpRouter::InvalidRouteException) { r.path(:route) } 88 | end 89 | 90 | def test_ambigiuous_parameters_in_route 91 | r = router 92 | r.add("/abc/:id/test/:id", :name => :route).default_destination 93 | assert_raises(HttpRouter::AmbiguousVariableException) { r.path(:route, :id => 'fail') } 94 | end 95 | 96 | def test_public_interface 97 | methods = HttpRouter.public_instance_methods.map(&:to_sym) 98 | assert methods.include?(:url_mount) 99 | assert methods.include?(:url_mount=) 100 | assert methods.include?(:call) 101 | assert methods.include?(:recognize) 102 | assert methods.include?(:url) 103 | assert methods.include?(:pass_on_response) 104 | assert methods.include?(:ignore_trailing_slash?) 105 | assert methods.include?(:redirect_trailing_slash?) 106 | assert methods.include?(:process_destination_path) 107 | assert methods.include?(:rewrite_partial_path_info) 108 | assert methods.include?(:rewrite_path_info) 109 | assert methods.include?(:extend_route) 110 | end 111 | 112 | def test_to_s_and_inspect 113 | router = HttpRouter.new 114 | router.add('/').to(:test) 115 | router.add('/test').to(:test2) 116 | router.post('/test').to(:test3) 117 | assert router.to_s.match(/^#$/) 118 | assert router.inspect.match(/^#/) 119 | assert router.inspect.match(/Path: "\/test" for route unnamed route to :test3/) 120 | end 121 | 122 | def test_naming_route_with_no_router 123 | route = HttpRouter::Route.new 124 | route.name = 'named_route' 125 | assert_equal 'named_route', route.name 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /test/test_mounting.rb: -------------------------------------------------------------------------------- 1 | class TestMounting < MiniTest::Unit::TestCase 2 | def setup 3 | @r1 = HttpRouter.new 4 | @r2 = HttpRouter.new 5 | @r2.add("/bar", :name => :test).to{|env| [200, {}, []]} 6 | end 7 | 8 | def test_url_mount_for_child_route 9 | route = @r1.add("/foo").to(@r2) 10 | assert_equal "/foo", @r2.url_mount.url 11 | assert_equal "/foo/bar", @r2.path(:test) 12 | end 13 | 14 | def test_default_values 15 | route = @r1.add("/foo/:bar", :default_values => {:bar => "baz"}).to(@r2) 16 | assert_equal "/foo/baz/bar", @r2.path(:test) 17 | assert_equal "/foo/haha/bar", @r2.path(:test, :bar => "haha") 18 | end 19 | 20 | def test_multiple_values 21 | @r1.add("/foo/:bar/:baz", :default_values => {:bar => "bar"}).to(@r2) 22 | assert_equal "/foo/bar/baz/bar", @r2.path(:test, :baz => "baz") 23 | end 24 | 25 | def test_bubble_params 26 | route = @r1.add("/foo/:bar", :default_values => {:bar => 'baz'}) 27 | route.to(@r2) 28 | assert_equal "/foo/baz/bar?bang=ers", @r2.path(:test, :bang => "ers") 29 | assert_equal "/foo/haha/bar?bang=ers", @r2.path(:test, :bar => "haha", :bang => "ers") 30 | end 31 | 32 | def test_path_with_optional 33 | @r1.add("/foo(/:bar)").to(@r2) 34 | @r2.add("/hey(/:there)", :name => :test2).to{|env| [200, {}, []]} 35 | assert_equal "/foo/hey", @r2.path(:test2) 36 | assert_equal "/foo/bar/hey", @r2.path(:test2, :bar => "bar") 37 | assert_equal "/foo/bar/hey/there", @r2.path(:test2, :bar => "bar", :there => "there") 38 | end 39 | 40 | def test_nest3 41 | @r3 = HttpRouter.new 42 | @r1.add("/foo(/:bar)", :default_values => {:bar => 'barry'}).to(@r2) 43 | @r2.add("/hi", :name => :hi).to{|env| [200, {}, []]} 44 | @r2.add("/mounted").to(@r3) 45 | @r3.add("/endpoint", :name => :endpoint).to{|env| [200, {}, []]} 46 | 47 | assert_equal "/foo/barry/hi", @r2.path(:hi) 48 | assert_equal "/foo/barry/mounted/endpoint", @r3.path(:endpoint) 49 | assert_equal "/foo/flower/mounted/endpoint", @r3.path(:endpoint, :bar => "flower") 50 | end 51 | 52 | def test_with_default_host 53 | @r1.add("/mounted", :default_values => {:host => "example.com"}).to(@r2) 54 | assert_equal "http://example.com/mounted/bar", @r2.path(:test) 55 | end 56 | 57 | def test_with_host 58 | @r1.add("/mounted").to(@r2) 59 | assert_equal "/mounted/bar", @r2.path(:test) 60 | assert_equal "http://example.com/mounted/bar", @r2.path(:test, :host => "example.com") 61 | end 62 | 63 | def test_with_scheme 64 | @r1.add("/mounted").to(@r2) 65 | assert_equal "/mounted/bar", @r2.path(:test) 66 | assert_equal "https://example.com/mounted/bar", @r2.path(:test, :scheme => "https", :host => "example.com") 67 | end 68 | 69 | def test_clone 70 | @r3 = HttpRouter.new 71 | @r1.add("/first").to(@r2) 72 | @r2.add("/second").to(@r3) 73 | r1 = @r1.clone 74 | assert @r1.routes.first 75 | r2 = r1.routes.first.dest 76 | assert r2 77 | assert_equal @r1.routes.first.dest.object_id, @r2.object_id 78 | assert r2.object_id != @r2.object_id 79 | assert_equal 2, r2.routes.size 80 | r3 = r2.routes.last.dest 81 | assert_instance_of HttpRouter, r3 82 | assert r3.object_id != @r3.object_id 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/test_recognition.rb: -------------------------------------------------------------------------------- 1 | class TestRecognition < MiniTest::Unit::TestCase 2 | if //.respond_to?(:names) 3 | eval <<-EOT 4 | def test_match_path_with_groups 5 | r = router { add(%r{/(?\\d{4})/(?\\d{2})/(?\\d{2})/?}) } 6 | assert_route r, "/1234/23/56", {:year => "1234", :month => "23", :day => "56"} 7 | end 8 | EOT 9 | end 10 | 11 | def test_non_path_matching 12 | passed, working = router { 13 | add(:conditions => {:user_agent => /MSIE/}).to { |env| [200, {}, ['IE']] } 14 | add('/').to { |env| [200, {}, ['SOMETHING ELSE']] } 15 | } 16 | assert_body 'SOMETHING ELSE', router.call(Rack::MockRequest.env_for('/')) 17 | assert_body 'IE', router.call(Rack::MockRequest.env_for('/', 'HTTP_USER_AGENT' => 'THIS IS MSIE DAWG')) 18 | end 19 | 20 | def test_passing_with_cascade 21 | passed, working = router { 22 | add('/').to { |env| [200, {'X-Cascade' => 'pass'}, ['pass']] } 23 | add('/').to { |env| [200, {}, ['working']] } 24 | } 25 | assert_body 'working', router.call(Rack::MockRequest.env_for('/')) 26 | end 27 | 28 | def test_compiling_uncompiling 29 | @router = router 30 | root = @router.add('/').default_destination 31 | assert_route root, '/' 32 | test = @router.add('/test').default_destination 33 | assert_route root, '/' 34 | assert_route test, '/test' 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/test_trailing_slash.rb: -------------------------------------------------------------------------------- 1 | class TestVariable < MiniTest::Unit::TestCase 2 | def test_ignore_trailing_slash 3 | assert_route router.add('/test'), '/test/' 4 | end 5 | 6 | def test_ignore_trailing_slash_enabled 7 | router(:ignore_trailing_slash => false).add('/test/?') 8 | assert_route nil, '/test/' 9 | end 10 | 11 | def test_capture_with_trailing_slash 12 | assert_route router.add('/:test'), '/test/', {:test => 'test'} 13 | end 14 | 15 | def test_trailing_slash_confusion 16 | more_general, more_specific = router { 17 | add('foo') 18 | add('foo/:bar/:id') 19 | } 20 | assert_route more_general, '/foo' 21 | assert_route more_general, '/foo/' 22 | assert_route more_specific, '/foo/5/10', {:bar => '5', :id => '10'} 23 | end 24 | end --------------------------------------------------------------------------------