usher 0.7.1 → 0.7.2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # Generated from /Users/joshua/Development/usher/usher.gemspec
2
+ source :gemcutter
3
+ gem "fuzzyhash", ">= 0.0.11"
4
+
5
+ group :development do
6
+ gem "yard", ">= 0"
7
+ gem "rspec", ">= 0"
8
+ gem "code_stats", ">= 0"
9
+ gem "rake", ">= 0"
10
+ end
data/README.markdown ADDED
@@ -0,0 +1,158 @@
1
+ # Usher
2
+
3
+ Tree-based router library. Useful for (specifically) for Rails and Rack, but probably generally useful for anyone interested in doing routing. Based on Ilya Grigorik suggestion, turns out looking up in a hash and following a tree is faster than Krauter's massive regex approach.
4
+
5
+ ## Features
6
+
7
+ * Understands single and path-globbing variables
8
+ * Understands arbitrary regex variables
9
+ * Arbitrary HTTP header requirements
10
+ * No optimization phase, so routes are always alterable after the fact
11
+ * Understands Proc and Regex transformations, validations
12
+ * Really, really fast
13
+ * Relatively light and happy code-base, should be easy and fun to alter (it hovers around 1,000 LOC, 800 for the core)
14
+ * Interface and implementation are separate, encouraging cross-pollination
15
+ * Works in 1.9!
16
+
17
+ ## Projects using or other references to Usher
18
+
19
+ * [http://github.com/Tass/CRUDtree](http://github.com/Tass/CRUDtree) - RESTful resource mapper
20
+ * [http://github.com/padrino/padrino-framework](http://github.com/padrino/padrino-framework) - Web framework
21
+ * [http://github.com/botanicus/rango](http://github.com/botanicus/rango) - Web framework
22
+ * [http://github.com/hassox/pancake](http://github.com/hassox/pancake) - Web framework
23
+ * [http://github.com/eddanger/junior](http://github.com/eddanger/junior) - Web framework
24
+ * [http://github.com/lifo/cramp](http://github.com/lifo/cramp) - Web framework
25
+ * [http://yehudakatz.com/2009/08/26/how-to-build-sinatra-on-rails-3/](http://yehudakatz.com/2009/08/26/how-to-build-sinatra-on-rails-3/) - How to Build Sinatra on Rails 3
26
+
27
+ Any probably more!
28
+
29
+ ## Route format
30
+
31
+ From the rdoc:
32
+
33
+ Creates a route from +path+ and +options+
34
+
35
+ ### `path`
36
+ A path consists a mix of dynamic and static parts delimited by `/`
37
+
38
+ #### Dynamic
39
+ Dynamic parts are prefixed with either `:`, `*`. :variable matches only one part of the path, whereas `*variable` can match one or
40
+ more parts.
41
+
42
+ <b>Example:</b>
43
+ `/path/:variable/path` would match
44
+
45
+ * `/path/test/path`
46
+ * `/path/something_else/path`
47
+ * `/path/one_more/path`
48
+
49
+ In the above examples, `test`, `something_else` and `one_more` respectively would be bound to the key `:variable`.
50
+ However, `/path/test/one_more/path` would not be matched.
51
+
52
+ <b>Example:</b>
53
+ `/path/*variable/path` would match
54
+
55
+ * `/path/one/two/three/path`
56
+ * `/path/four/five/path`
57
+
58
+ In the above examples, `['one', 'two', 'three']` and `['four', 'five']` respectively would be bound to the key `:variable`.
59
+
60
+ As well, variables can have a regex matcher.
61
+
62
+ <b>Example:</b>
63
+ `/product/{:id,\d+}` would match
64
+
65
+ * `/product/123`
66
+ * `/product/4521`
67
+
68
+ But not
69
+ * `/product/AE-35`
70
+
71
+ As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
72
+ actually be bound to the variable
73
+
74
+ Variables can also have a greedy regex matcher. These matchers ignore all delimiters, and continue matching for as long as much as their
75
+ regex allows.
76
+
77
+ <b>Example:</b>
78
+ `/product/{!id,hello/world|hello}` would match
79
+
80
+ * `/product/hello/world`
81
+ * `/product/hello`
82
+
83
+
84
+ #### Static
85
+
86
+ Static parts of literal character sequences. For instance, `/path/something.html` would match only the same path.
87
+ As well, static parts can have a regex pattern in them as well, such as `/path/something.{html|xml}` which would match only
88
+ `/path/something.html` and `/path/something.xml`
89
+
90
+ #### Optional sections
91
+
92
+ Sections of a route can be marked as optional by surrounding it with brackets. For instance, in the above static example, `/path/something(.html)` would match both `/path/something` and `/path/something.html`.
93
+
94
+ #### One and only one sections
95
+
96
+ Sections of a route can be marked as "one and only one" by surrounding it with brackets and separating parts of the route with pipes.
97
+ For instance, the path, `/path/something(.xml|.html)` would only match `/path/something.xml` and
98
+ `/path/something.html`. Generally its more efficent to use one and only sections over using regex.
99
+
100
+ ### `options`
101
+ * `requirements` - After transformation, tests the condition using #===. If it returns false, it raises an `Usher::ValidationException`
102
+ * `conditions` - Accepts any of the `request_methods` specificied in the construction of Usher. This can be either a `string` or a regular expression.
103
+ * `default_values` - Provides values for variables in your route for generation. If you're using URL generation, then any values supplied here that aren't included in your path will be appended to the query string.
104
+ * `priority` - If there are two routes which equally match, the route with the highest priority will match first.
105
+ * Any other key is interpreted as a requirement for the variable of its name.
106
+
107
+ ## Rails
108
+
109
+ script/plugin install git://github.com/joshbuddy/usher.git
110
+
111
+ In your config/initializers/usher.rb (create if it doesn't exist) add:
112
+
113
+ Usher::Util::Rails.activate
114
+
115
+ ## Rack
116
+
117
+ ### `config.ru`
118
+
119
+ require 'usher'
120
+ app # proc do |env|
121
+ body # "Hi there #{env['usher.params'][:name]}"
122
+ [
123
+ 200, # Status code
124
+ { # Response headers
125
+ 'Content-Type' #> 'text/plain',
126
+ 'Content-Length' #> body.size.to_s,
127
+ },
128
+ [body] # Response body
129
+ ]
130
+ end
131
+
132
+ routes # Usher::Interface.for(:rack) do
133
+ add('/hello/:name').to(app)
134
+ end
135
+
136
+ run routes
137
+
138
+ ------------
139
+
140
+ >> curl http://127.0.0.1:3000/hello/samueltanders
141
+ << Hi there samueltanders
142
+
143
+
144
+ ## Sinatra
145
+
146
+ In Sinatra, you get the extra method, +generate+, which lets you generate a url. Name your routes with `:name` when you define them.
147
+
148
+ require 'rubygems'
149
+ require 'usher'
150
+ require 'sinatra'
151
+
152
+ Usher::Interface.for(:sinatra)
153
+
154
+ get '/hi', :name #> :hi do
155
+ "Hello World! #{generate(:hi)}"
156
+ end
157
+
158
+ (Let me show you to your request)
data/Rakefile CHANGED
@@ -2,7 +2,14 @@
2
2
 
3
3
  require 'spec'
4
4
  require 'spec/rake/spectask'
5
- task :spec => ['spec:private', 'spec:rails2_2', 'spec:rails2_3']
5
+ require 'yard'
6
+
7
+ YARD::Rake::YardocTask.new do |t|
8
+ t.files = ['lib/**/*.rb'] # optional
9
+ t.options = ['--markup=markdown'] # optional
10
+ end
11
+
12
+ task :spec => ['spec:private', 'spec:rails2_2:cleanup', 'spec:rails2_3:cleanup']
6
13
  namespace(:spec) do
7
14
  Spec::Rake::SpecTask.new(:private) do |t|
8
15
  t.spec_opts ||= []
@@ -11,20 +18,48 @@ namespace(:spec) do
11
18
  t.spec_files = FileList['spec/private/**/*_spec.rb']
12
19
  end
13
20
 
14
- Spec::Rake::SpecTask.new(:rails2_2) do |t|
15
- t.spec_opts ||= []
16
- t.spec_opts << "-rubygems"
17
- t.spec_opts << "--options" << "spec/spec.opts"
18
- t.spec_files = FileList['spec/rails2_2/**/*_spec.rb']
19
- end
21
+ namespace(:rails2_2) do
22
+ task :unzip do
23
+ sh('rm -rf spec/rails2_2/vendor')
24
+ sh('unzip -qq spec/rails2_2/vendor.zip -dspec/rails2_2')
25
+ end
20
26
 
21
- Spec::Rake::SpecTask.new(:rails2_3) do |t|
22
- t.spec_opts ||= []
23
- t.spec_opts << "-rubygems"
24
- t.spec_opts << "--options" << "spec/spec.opts"
25
- t.spec_files = FileList['spec/rails2_3/**/*_spec.rb']
27
+ Spec::Rake::SpecTask.new(:spec) do |t|
28
+ t.spec_opts ||= []
29
+ t.spec_opts << "-rubygems"
30
+ t.spec_opts << "--options" << "spec/spec.opts"
31
+ t.spec_files = FileList['spec/rails2_2/**/*_spec.rb']
32
+ end
33
+
34
+ task :cleanup do
35
+ sh('rm -rf spec/rails2_2/vendor')
36
+ end
37
+
38
+ task :spec => :unzip
39
+ task :cleanup => :spec
26
40
  end
27
41
 
42
+ namespace(:rails2_3) do
43
+ task :unzip do
44
+ sh('rm -rf spec/rails2_3/vendor')
45
+ sh('unzip -qq spec/rails2_3/vendor.zip -dspec/rails2_3')
46
+ end
47
+
48
+ Spec::Rake::SpecTask.new(:spec) do |t|
49
+ t.spec_opts ||= []
50
+ t.spec_opts << "-rubygems"
51
+ t.spec_opts << "--options" << "spec/spec.opts"
52
+ t.spec_files = FileList['spec/rails2_3/**/*_spec.rb']
53
+ end
54
+ task :cleanup do
55
+ sh('rm -rf spec/rails2_3/vendor')
56
+ end
57
+
58
+ task :spec => :unzip
59
+ task :cleanup => :spec
60
+ end
61
+
62
+
28
63
  end
29
64
 
30
65
  desc "Run all examples with RCov"
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rbench'
3
+ require '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('/simple/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)
26
+ end
27
+
28
+ report "4 levels, static" do
29
+ u.call(simple2_env)
30
+ end
31
+
32
+ report "8 levels, static" do
33
+ u.call(simple3_env)
34
+ end
35
+
36
+ report "4 levels, 1 dynamic" do
37
+ u.call(simple_and_dynamic_env)
38
+ end
39
+
40
+ report "8 levels, 3 dynamic" do
41
+ u.call(simple_and_dynamic_env1)
42
+ end
43
+
44
+ report "4 levels, 1 greedy" do
45
+ u.call(simple_and_dynamic_env2)
46
+ end
47
+
48
+ end
@@ -1,8 +1,11 @@
1
1
  class Usher
2
+ # Array of delimiters with convenience methods.
2
3
  class Delimiters < Array
3
4
 
4
5
  attr_reader :unescaped
5
-
6
+
7
+ # Creates a list of delimiters
8
+ # @param arr [Array<String>] delimters to use
6
9
  def initialize(ary)
7
10
  super ary
8
11
  @unescaped = self.map do |delimiter|
@@ -10,15 +13,24 @@ class Usher
10
13
  end
11
14
  end
12
15
 
16
+ # Finds the first occurrance of a delimiter in an array
17
+ # @param array [Array<String>] Array to search through
18
+ # @return [nil, String] The delimiter matched, or nil if none was found.
13
19
  def first_in(array)
14
- # TODO: should we optimize this O(n*m)? hash or modified or KNP or at leaset sort + b-search. But they are so short
15
-
16
- array.each do |element|
17
- return element if self.unescaped.any? { |delimiter| delimiter == element }
18
- end
19
- nil
20
+ array.find { |e| e if unescaped.any? { |delimiter| delimiter == e } }
20
21
  end
21
22
 
22
- # TODO: Delimiters#regex and so on
23
+ # The regular expression to find the delimiters.
24
+ # @return [Regexp] The regular expression
25
+ def regexp
26
+ @regexp ||= Regexp.new("(#{unescaped.collect{|d| Regexp.quote(d)}.join('|')})")
27
+ end
28
+
29
+ # The regular expression expressed as a character class.
30
+ # @return [String] The regular expression as a string.
31
+ def regexp_char_class
32
+ @regexp_char_class ||= collect{|d| Regexp.quote(d)}.join
33
+ end
34
+
23
35
  end
24
36
  end
@@ -1,6 +1,10 @@
1
1
  class Usher
2
+ # Exception raised when generation is attempted and the route cannot be determined
2
3
  class UnrecognizedException < RuntimeError; end
4
+ # Raised when a validator fails during recognition
3
5
  class ValidationException < RuntimeError; end
6
+ # Raised when generation attempts to create a route and a parameter is missing
4
7
  class MissingParameterException < RuntimeError; end
8
+ # Raised when a route is added with identical variable names and allow_identical_variable_names? is false
5
9
  class MultipleParameterException < RuntimeError; end
6
10
  end
data/lib/usher/grapher.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  class Usher
2
+ # Find nearest matching routes based on parameter keys.
2
3
  class Grapher
3
4
 
4
5
  attr_reader :routes, :router, :orders, :key_count, :cache
@@ -8,20 +9,39 @@ class Usher
8
9
  reset!
9
10
  end
10
11
 
11
- def reset!
12
- @significant_keys = nil
13
- @orders = Hash.new{|h,k| h[k] = Hash.new{|h2, k2| h2[k2] = []}}
14
- @key_count = Hash.new(0)
15
- @cache = {}
16
- @routes = []
17
- end
18
-
12
+ # Add route for matching
19
13
  def add_route(route)
14
+ reset! if @processed
20
15
  routes << route
21
16
  end
22
17
 
18
+ # Finds a matching path based on params hash
19
+ def find_matching_path(params)
20
+ unless params.empty?
21
+ process_routes
22
+ set = params.keys & significant_keys
23
+ if cached = cache[set]
24
+ return cached
25
+ end
26
+ set.size.downto(1) do |o|
27
+ set.each do |k|
28
+ orders[o][k].each do |r|
29
+ if r.can_generate_from_keys?(set)
30
+ cache[set] = r
31
+ return r
32
+ elsif router.consider_destination_keys? && r.can_generate_from_params?(params)
33
+ return r
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ nil
40
+ end
41
+
42
+ private
23
43
  def process_routes
24
- return if @processed
44
+ return if @processed
25
45
  routes.each do |route|
26
46
  route.paths.each do |path|
27
47
  if path.dynamic?
@@ -57,33 +77,19 @@ class Usher
57
77
  end
58
78
  @processed = true
59
79
  end
60
-
80
+
61
81
  def significant_keys
62
82
  @significant_keys ||= key_count.keys.uniq
63
83
  end
64
84
 
65
- def find_matching_path(params)
66
- unless params.empty?
67
- process_routes
68
- set = params.keys & significant_keys
69
- if cached = cache[set]
70
- return cached
71
- end
72
- set.size.downto(1) do |o|
73
- set.each do |k|
74
- orders[o][k].each do |r|
75
- if r.can_generate_from_keys?(set)
76
- cache[set] = r
77
- return r
78
- elsif router.consider_destination_keys? && r.can_generate_from_params?(params)
79
- return r
80
- end
81
- end
82
- end
83
- end
84
- end
85
- nil
85
+ def reset!
86
+ @significant_keys = nil
87
+ @orders = Hash.new{|h,k| h[k] = Hash.new{|h2, k2| h2[k2] = []}}
88
+ @key_count = Hash.new(0)
89
+ @cache = {}
90
+ @routes = []
91
+ @processed = false
86
92
  end
87
-
93
+
88
94
  end
89
95
  end
@@ -0,0 +1,39 @@
1
+ class Usher
2
+ module Interface
3
+ class Rack
4
+ # Replacement for <tt>Rack::Builder</tt> which using Usher to map requests instead of a simple Hash.
5
+ # As well, add convenience methods for the request methods.
6
+ #
7
+ class Builder < ::Rack::Builder
8
+ def initialize(&block)
9
+ @usher = Usher::Interface::Rack.new
10
+ super
11
+ end
12
+
13
+ def map(path, options = nil, &block)
14
+ @usher.add(path, options).to(&block)
15
+ @ins << @usher unless @ins.last == @usher
16
+ end
17
+
18
+ # it returns route, and because you may want to work with the route,
19
+ # for example give it a name, we returns the route with GET request
20
+ def get(path, options = nil, &block)
21
+ self.map(path, options.merge!(:conditions => {:request_method => "HEAD"}), &block)
22
+ self.map(path, options.merge!(:conditions => {:request_method => "GET"}), &block)
23
+ end
24
+
25
+ def post(path, options = nil, &block)
26
+ self.map(path, options.merge!(:conditions => {:request_method => "POST"}), &block)
27
+ end
28
+
29
+ def put(path, options = nil, &block)
30
+ self.map(path, options.merge!(:conditions => {:request_method => "PUT"}), &block)
31
+ end
32
+
33
+ def delete(path, options = nil, &block)
34
+ self.map(path, options.merge!(:conditions => {:request_method => "DELETE"}), &block)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,22 @@
1
+ class Usher
2
+ module Interface
3
+ class Rack
4
+ # Middleware for using Usher's rack interface to recognize the request, then, pass on to the next application.
5
+ # Values are stored in <tt>env</tt> normally.
6
+ #
7
+ class Middleware
8
+
9
+ def initialize(app, router)
10
+ @app = app
11
+ @router = router
12
+ end
13
+
14
+ def call(env)
15
+ @router.call(env)
16
+ @app.call(env)
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,64 +1,15 @@
1
- require "rack"
1
+ require 'rack'
2
2
  require File.join('usher', 'interface', 'rack', 'route')
3
+ require File.join('usher', 'interface', 'rack', 'middleware')
4
+ require File.join('usher', 'interface', 'rack', 'builder')
3
5
 
4
6
  class Usher
5
7
  module Interface
6
8
  class Rack
7
9
 
8
- ENV_KEY_RESPONSE = 'usher.response'
9
- ENV_KEY_PARAMS = 'usher.params'
10
- ENV_KEY_DEFAULT_ROUTER = 'usher.router'
11
-
12
- # Middleware for using Usher's rack interface to recognize the request, then, pass on to the next application.
13
- # Values are stored in <tt>env</tt> normally.
14
- #
15
- class Middleware
16
-
17
- def initialize(app, router)
18
- @app = app
19
- @router = router
20
- end
21
-
22
- def call(env)
23
- @router.call(env)
24
- @app.call(env)
25
- end
26
-
27
- end
28
-
29
- # Replacement for <tt>Rack::Builder</tt> which using Usher to map requests instead of a simple Hash.
30
- # As well, add convenience methods for the request methods.
31
- #
32
- class Builder < ::Rack::Builder
33
- def initialize(&block)
34
- @usher = Usher::Interface::Rack.new
35
- super
36
- end
37
-
38
- def map(path, options = nil, &block)
39
- @usher.add(path, options).to(&block)
40
- @ins << @usher unless @ins.last == @usher
41
- end
42
-
43
- # it returns route, and because you may want to work with the route,
44
- # for example give it a name, we returns the route with GET request
45
- def get(path, options = nil, &block)
46
- self.map(path, options.merge!(:conditions => {:request_method => "HEAD"}), &block)
47
- self.map(path, options.merge!(:conditions => {:request_method => "GET"}), &block)
48
- end
49
-
50
- def post(path, options = nil, &block)
51
- self.map(path, options.merge!(:conditions => {:request_method => "POST"}), &block)
52
- end
53
-
54
- def put(path, options = nil, &block)
55
- self.map(path, options.merge!(:conditions => {:request_method => "PUT"}), &block)
56
- end
57
-
58
- def delete(path, options = nil, &block)
59
- self.map(path, options.merge!(:conditions => {:request_method => "DELETE"}), &block)
60
- end
61
- end
10
+ ENV_KEY_RESPONSE = 'usher.response'.freeze
11
+ ENV_KEY_PARAMS = 'usher.params'.freeze
12
+ ENV_KEY_DEFAULT_ROUTER = 'usher.router'.freeze
62
13
 
63
14
  attr_reader :router, :router_key
64
15
  attr_accessor :redirect_on_trailing_delimiters