usher 0.4.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +35 -0
  3. data/README.rdoc +126 -0
  4. data/Rakefile +72 -0
  5. data/VERSION.yml +4 -0
  6. data/lib/usher/exceptions.rb +5 -0
  7. data/lib/usher/generate.rb +131 -0
  8. data/lib/usher/grapher.rb +65 -0
  9. data/lib/usher/interface/email_interface.rb +27 -0
  10. data/lib/usher/interface/merb_interface.rb +61 -0
  11. data/lib/usher/interface/rack_interface/mapper.rb +0 -0
  12. data/lib/usher/interface/rack_interface/route.rb +9 -0
  13. data/lib/usher/interface/rack_interface.rb +37 -0
  14. data/lib/usher/interface/rails2_2_interface/mapper.rb +44 -0
  15. data/lib/usher/interface/rails2_2_interface.rb +135 -0
  16. data/lib/usher/interface/rails2_3_interface.rb +135 -0
  17. data/lib/usher/interface.rb +27 -0
  18. data/lib/usher/node.rb +138 -0
  19. data/lib/usher/route/path.rb +24 -0
  20. data/lib/usher/route/request_method.rb +22 -0
  21. data/lib/usher/route/variable.rb +37 -0
  22. data/lib/usher/route.rb +58 -0
  23. data/lib/usher/splitter.rb +159 -0
  24. data/lib/usher.rb +184 -0
  25. data/rails/init.rb +8 -0
  26. data/spec/private/email/recognize_spec.rb +38 -0
  27. data/spec/private/generate_spec.rb +141 -0
  28. data/spec/private/grapher_spec.rb +41 -0
  29. data/spec/private/path_spec.rb +68 -0
  30. data/spec/private/rack/dispatch_spec.rb +29 -0
  31. data/spec/private/rails2_2/compat.rb +1 -0
  32. data/spec/private/rails2_2/generate_spec.rb +28 -0
  33. data/spec/private/rails2_2/path_spec.rb +16 -0
  34. data/spec/private/rails2_2/recognize_spec.rb +79 -0
  35. data/spec/private/rails2_3/compat.rb +1 -0
  36. data/spec/private/rails2_3/generate_spec.rb +28 -0
  37. data/spec/private/rails2_3/path_spec.rb +16 -0
  38. data/spec/private/rails2_3/recognize_spec.rb +79 -0
  39. data/spec/private/recognize_spec.rb +178 -0
  40. data/spec/private/request_method_spec.rb +15 -0
  41. data/spec/private/split_spec.rb +76 -0
  42. data/spec/spec.opts +7 -0
  43. metadata +120 -0
@@ -0,0 +1,159 @@
1
+ require 'strscan'
2
+
3
+ class Usher
4
+ class Splitter
5
+
6
+ def self.for_delimiters(delimiters, valid_regex)
7
+ delimiters_regex = delimiters.collect{|d| Regexp.quote(d)} * '|'
8
+ SplitterInstance.new(
9
+ delimiters,
10
+ Regexp.new('((:|\*)?' + valid_regex + '|' + delimiters_regex + '|\(|\)|\||\{)'),
11
+ Regexp.new("[#{delimiters.collect{|d| Regexp.quote(d)}}]|[^#{delimiters.collect{|d| Regexp.quote(d)}}]+")
12
+ )
13
+ end
14
+
15
+ attr_reader :paths
16
+
17
+ class SplitterInstance
18
+
19
+ attr_reader :delimiter_chars
20
+
21
+ def initialize(delimiters, split_regex, url_split_regex)
22
+ @delimiters = delimiters
23
+ @delimiter_chars = delimiters.collect{|d| d[0]}
24
+ @split_regex = split_regex
25
+ @url_split_regex = url_split_regex
26
+ end
27
+
28
+ def url_split(path)
29
+ path.scan(@url_split_regex)
30
+ end
31
+
32
+ def split(path, requirements = nil, default_values = nil)
33
+ parts = Group.new(:all, nil)
34
+ ss = StringScanner.new(path)
35
+ current_group = parts
36
+ while !ss.eos?
37
+ part = ss.scan(@split_regex)
38
+ case part[0]
39
+ when ?*, ?:
40
+ type = part.slice!(0).chr.to_sym
41
+ current_group << Usher::Route::Variable.new(type, part, requirements && requirements[part.to_sym])
42
+ when ?{
43
+ pattern = ''
44
+ count = 1
45
+ variable = ss.scan(/[:\*]([^,]+),/)
46
+ until count.zero?
47
+ regex_part = ss.scan(/\{|\}|[^\{\}]+/)
48
+ case regex_part[0]
49
+ when ?{
50
+ count += 1
51
+ when ?}
52
+ count -= 1
53
+ end
54
+ pattern << regex_part
55
+ end
56
+ pattern.slice!(pattern.length - 1)
57
+ regex = Regexp.new(pattern)
58
+ if variable
59
+ variable_type = variable.slice!(0).chr.to_sym
60
+ variable_name = variable[0, variable.size - 1].to_sym
61
+ current_group << Usher::Route::Variable.new(variable_type, variable_name, requirements && requirements[variable_name], regex)
62
+ else
63
+ current_group << regex
64
+ end
65
+ when ?(
66
+ new_group = Group.new(:any, current_group)
67
+ current_group << new_group
68
+ current_group = new_group
69
+ when ?)
70
+ current_group = current_group.parent.type == :one ? current_group.parent.parent : current_group.parent
71
+ when ?|
72
+ unless current_group.parent.type == :one
73
+ detached_group = current_group.parent.pop
74
+ new_group = Group.new(:one, detached_group.parent)
75
+ detached_group.parent = new_group
76
+ detached_group.type = :all
77
+ new_group << detached_group
78
+ new_group.parent << new_group
79
+ end
80
+ current_group.parent << Group.new(:all, current_group.parent)
81
+ current_group = current_group.parent.last
82
+ else
83
+ current_group << part
84
+ end
85
+ end unless !path || path.empty?
86
+ paths = calc_paths(parts)
87
+ paths.each do |path|
88
+ path.each_with_index do |part, index|
89
+ if part.is_a?(Usher::Route::Variable)
90
+ part.default_value = default_values[part.name] if default_values
91
+
92
+ case part.type
93
+ when :*
94
+ part.look_ahead = path[index + 1, path.size].find{|p| !p.is_a?(Usher::Route::Variable) && !@delimiter_chars.include?(p[0])} || nil
95
+ when :':'
96
+ part.look_ahead = path[index + 1, path.size].find{|p| @delimiter_chars.include?(p[0])} || @delimiters.first
97
+ end
98
+ end
99
+ end
100
+ end
101
+ paths
102
+ end
103
+
104
+ private
105
+
106
+ def cartesian_product!(lval, rval)
107
+ product = []
108
+ (lval.size * rval.size).times do |index|
109
+ val = []
110
+ val.push(*lval[index % lval.size])
111
+ val.push(*rval[index % rval.size])
112
+ product << val
113
+ end
114
+ lval.replace(product)
115
+ end
116
+
117
+ def calc_paths(parts)
118
+ if parts.is_a?(Group)
119
+ paths = [[]]
120
+ case parts.type
121
+ when :all
122
+ parts.each do |p|
123
+ cartesian_product!(paths, calc_paths(p))
124
+ end
125
+ when :any
126
+ parts.each do |p|
127
+ cartesian_product!(paths, calc_paths(p))
128
+ end
129
+ paths.unshift([])
130
+ when :one
131
+ cartesian_product!(paths, parts.collect do |p|
132
+ calc_paths(p)
133
+ end)
134
+ end
135
+ paths.each{|p| p.compact!; p.flatten! }
136
+ paths
137
+ else
138
+ [[parts]]
139
+ end
140
+
141
+ end
142
+ end
143
+
144
+ class Group < Array
145
+ attr_accessor :type
146
+ attr_accessor :parent
147
+
148
+ def inspect
149
+ "#{type}->#{super}"
150
+ end
151
+
152
+ def initialize(type, parent)
153
+ @type = type
154
+ @parent = parent
155
+ end
156
+ end
157
+
158
+ end
159
+ end
data/lib/usher.rb ADDED
@@ -0,0 +1,184 @@
1
+ require File.join(File.dirname(__FILE__), 'usher', 'node')
2
+ require File.join(File.dirname(__FILE__), 'usher', 'route')
3
+ require File.join(File.dirname(__FILE__), 'usher', 'grapher')
4
+ require File.join(File.dirname(__FILE__), 'usher', 'interface')
5
+ require File.join(File.dirname(__FILE__), 'usher', 'splitter')
6
+ require File.join(File.dirname(__FILE__), 'usher', 'exceptions')
7
+
8
+ class Usher
9
+
10
+ autoload :Generators, File.join(File.dirname(__FILE__), 'usher', 'generate')
11
+
12
+ attr_reader :tree, :named_routes, :route_count, :routes, :splitter, :delimiters
13
+
14
+ SymbolArraySorter = proc {|a,b| a.hash <=> b.hash} #:nodoc:
15
+
16
+ # Returns whether the route set is empty
17
+ #
18
+ # set = Usher.new
19
+ # set.empty? => true
20
+ # set.add_route('/test')
21
+ # set.empty? => false
22
+ def empty?
23
+ @route_count.zero?
24
+ end
25
+
26
+ # Resets the route set back to its initial state
27
+ #
28
+ # set = Usher.new
29
+ # set.add_route('/test')
30
+ # set.empty? => false
31
+ # set.reset!
32
+ # set.empty? => true
33
+ def reset!
34
+ @tree = Node.root(self, @request_methods, @globs_capture_separators)
35
+ @named_routes = {}
36
+ @routes = []
37
+ @route_count = 0
38
+ @grapher = Grapher.new
39
+ end
40
+ alias clear! reset!
41
+
42
+ # Creates a route set, with options
43
+ #
44
+ # <tt>:globs_capture_separators</tt>: +true+ or +false+. (default +false+) Specifies whether glob matching will also include separators
45
+ # that are matched.
46
+ #
47
+ # <tt>:delimiters</tt>: Array of Strings. (default <tt>['/', '.']</tt>). Delimiters used in path separation. Array must be single character strings.
48
+ #
49
+ # <tt>:valid_regex</tt>: String. (default <tt>'[0-9A-Za-z\$\-_\+!\*\',]+'</tt>). String that can be interpolated into regex to match
50
+ # valid character sequences within path.
51
+ #
52
+ # <tt>:request_methods</tt>: Array of Symbols. (default <tt>[:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]</tt>)
53
+ # Array of methods called against the request object for the purposes of matching route requirements.
54
+ def initialize(options = nil)
55
+ @globs_capture_separators = options && options.key?(:globs_capture_separators) ? options.delete(:globs_capture_separators) : false
56
+ @delimiters = options && options.delete(:delimiters) || ['/', '.']
57
+ @valid_regex = options && options.delete(:valid_regex) || '[0-9A-Za-z\$\-_\+!\*\',]+'
58
+ @request_methods = options && options.delete(:request_methods) || [:protocol, :domain, :port, :query_string, :remote_ip, :user_agent, :referer, :method, :subdomains]
59
+ @splitter = Splitter.for_delimiters(@delimiters, @valid_regex)
60
+ reset!
61
+ end
62
+
63
+ # Adds a route referencable by +name+. Sett add_route for format +path+ and +options+.
64
+ #
65
+ # set = Usher.new
66
+ # set.add_named_route(:test_route, '/test')
67
+ def add_named_route(name, path, options = nil)
68
+ add_route(path, options).name(name)
69
+ end
70
+
71
+ # Attaches a +route+ to a +name+
72
+ #
73
+ # set = Usher.new
74
+ # route = set.add_route('/test')
75
+ # set.name(:test, route)
76
+ def name(name, route)
77
+ @named_routes[name] = route
78
+ route
79
+ end
80
+
81
+ # Creates a route from +path+ and +options+
82
+ #
83
+ # === +path+
84
+ # A path consists a mix of dynamic and static parts delimited by <tt>/</tt>
85
+ #
86
+ # ==== Dynamic
87
+ # Dynamic parts are prefixed with either :, *. :variable matches only one part of the path, whereas *variable can match one or
88
+ # more parts.
89
+ #
90
+ # <b>Example:</b>
91
+ # <tt>/path/:variable/path</tt> would match
92
+ #
93
+ # * <tt>/path/test/path</tt>
94
+ # * <tt>/path/something_else/path</tt>
95
+ # * <tt>/path/one_more/path</tt>
96
+ #
97
+ # In the above examples, 'test', 'something_else' and 'one_more' respectively would be bound to the key <tt>:variable</tt>.
98
+ # However, <tt>/path/test/one_more/path</tt> would not be matched.
99
+ #
100
+ # <b>Example:</b>
101
+ # <tt>/path/*variable/path</tt> would match
102
+ #
103
+ # * <tt>/path/one/two/three/path</tt>
104
+ # * <tt>/path/four/five/path</tt>
105
+ #
106
+ # In the above examples, ['one', 'two', 'three'] and ['four', 'five'] respectively would be bound to the key :variable.
107
+ #
108
+ # As well, variables can have a regex matcher.
109
+ #
110
+ # <b>Example:</b>
111
+ # <tt>/product/{:id,\d+}</tt> would match
112
+ #
113
+ # * <tt>/product/123</tt>
114
+ # * <tt>/product/4521</tt>
115
+ #
116
+ # But not
117
+ # * <tt>/product/AE-35</tt>
118
+ #
119
+ # As well, the same logic applies for * variables as well, where only parts matchable by the supplied regex will
120
+ # actually be bound to the variable
121
+ #
122
+ # ==== Static
123
+ #
124
+ # Static parts of literal character sequences. For instance, <tt>/path/something.html</tt> would match only the same path.
125
+ # As well, static parts can have a regex pattern in them as well, such as <tt>/path/something.{html|xml}</tt> which would match only
126
+ # <tt>/path/something.html</tt> and <tt>/path/something.xml</tt>
127
+ #
128
+ # ==== Optional sections
129
+ #
130
+ # Sections of a route can be marked as optional by surrounding it with brackets. For instance, in the above static example, <tt>/path/something(.html)</tt> would match both <tt>/path/something</tt> and <tt>/path/something.html</tt>.
131
+ #
132
+ # ==== One and only one sections
133
+ #
134
+ # 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.
135
+ # For instance, the path, <tt>/path/something(.xml|.html)</tt> would only match <tt>/path/something.xml</tt> and
136
+ # <tt>/path/something.html</tt>. Generally its more efficent to use one and only sections over using regex.
137
+ #
138
+ # === +options+
139
+ # * +requirements+ - After transformation, tests the condition using ===. If it returns false, it raises an <tt>Usher::ValidationException</tt>
140
+ # * +conditions+ - Accepts any of the +request_methods+ specificied in the construction of Usher. This can be either a <tt>string</tt> or a regular expression.
141
+ # * Any other key is interpreted as a requirement for the variable of its name.
142
+ def add_route(path, options = nil)
143
+ conditions = options && options.delete(:conditions) || nil
144
+ requirements = options && options.delete(:requirements) || nil
145
+ default_values = options && options.delete(:default_values) || nil
146
+ generate_with = options && options.delete(:generate_with) || nil
147
+ if options
148
+ options.delete_if do |k, v|
149
+ if v.is_a?(Regexp) || v.is_a?(Proc)
150
+ (requirements ||= {})[k] = v
151
+ true
152
+ end
153
+ end
154
+ end
155
+ route = Route.new(path, self, conditions, requirements, default_values, generate_with)
156
+ route.to(options) if options && !options.empty?
157
+
158
+ @tree.add(route)
159
+ @routes << route
160
+ @grapher.add_route(route)
161
+ @route_count += 1
162
+ route
163
+ end
164
+
165
+ # Recognizes a +request+ and returns +nil+ or an Usher::Node::Response, which is a struct containing a Usher::Route::Path and an array of arrays containing the extracted parameters.
166
+ #
167
+ # Request = Struct.new(:path)
168
+ # set = Usher.new
169
+ # route = set.add_route('/test')
170
+ # set.recognize(Request.new('/test')).path.route == route => true
171
+ def recognize(request, path = request.path)
172
+ @tree.find(self, request, @splitter.url_split(path))
173
+ end
174
+
175
+ # Recognizes a set of +parameters+ and gets the closest matching Usher::Route::Path or +nil+ if no route exists.
176
+ #
177
+ # set = Usher.new
178
+ # route = set.add_route('/:controller/:action')
179
+ # set.path_for_options({:controller => 'test', :action => 'action'}) == path.route => true
180
+ def path_for_options(options)
181
+ @grapher.find_matching_path(options)
182
+ end
183
+
184
+ end
data/rails/init.rb ADDED
@@ -0,0 +1,8 @@
1
+ if Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR == 3
2
+ ActionController::Routing.module_eval "remove_const(:Routes); Routes = Usher::Interface.for(:rails2_3)"
3
+ elsif Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR >= 2
4
+ class Usher::Interface::Rails2_2Interface::Mapper
5
+ include ActionController::Resources
6
+ end
7
+ ActionController::Routing.module_eval "remove_const(:Routes); Routes = Usher::Interface.for(:rails2_2)"
8
+ end
@@ -0,0 +1,38 @@
1
+ require 'lib/usher'
2
+
3
+
4
+
5
+ def build_email_mock(email)
6
+ request = mock "Request"
7
+ request.should_receive(:email).any_number_of_times.and_return(email)
8
+ request
9
+ end
10
+
11
+ describe "Usher (for email) route recognition" do
12
+
13
+ before(:each) do
14
+ @route_set = Usher::Interface.for(:email)
15
+ end
16
+
17
+ it "should recognize a simple request" do
18
+ receiver = mock('receiver')
19
+ receiver.should_receive(:action).with({}).exactly(1)
20
+ @route_set.for('joshbuddy@gmail.com') { |params| receiver.action(params) }
21
+ @route_set.act('joshbuddy@gmail.com')
22
+ end
23
+
24
+ it "should recognize a wildcard domain" do
25
+ receiver = mock('receiver')
26
+ receiver.should_receive(:action).with({:domain => 'gmail.com'}).exactly(1)
27
+ @route_set.for('joshbuddy@*domain') { |params| receiver.action(params) }
28
+ @route_set.act('joshbuddy@gmail.com')
29
+ end
30
+
31
+ it "should recognize a complex email" do
32
+ receiver = mock('receiver')
33
+ receiver.should_receive(:action).with({:subject => 'sub+ect', :id => '123', :sid => '456', :tok => 'sdqwe123ae', :domain => 'mydomain.org'}).exactly(1)
34
+ @route_set.for(':subject.{:id,^\d+$}-{:sid,^\d+$}-{:tok,^\w+$}@*domain') { |params| receiver.action(params) }
35
+ @route_set.act('sub+ect.123-456-sdqwe123ae@mydomain.org')
36
+ end
37
+
38
+ end
@@ -0,0 +1,141 @@
1
+ require 'lib/usher'
2
+ require 'rack'
3
+
4
+ describe "Usher URL generation" do
5
+
6
+ before(:each) do
7
+ @route_set = Usher.new
8
+ @route_set.reset!
9
+ @url_generator = Usher::Generators::URL.new(@route_set)
10
+ end
11
+
12
+ it "should generate a simple URL" do
13
+ @route_set.add_named_route(:sample, '/sample', :controller => 'sample', :action => 'action')
14
+ @url_generator.generate(:sample, {}).should == '/sample'
15
+ end
16
+
17
+ it "should generate a simple URL with a single variable" do
18
+ @route_set.add_named_route(:sample, '/sample/:action', :controller => 'sample')
19
+ @url_generator.generate(:sample, {:action => 'action'}).should == '/sample/action'
20
+ end
21
+
22
+ it "should generate a simple URL with a single variable (and escape)" do
23
+ @route_set.add_named_route(:sample, '/sample/:action', :controller => 'sample')
24
+ @url_generator.generate(:sample, {:action => 'action time'}).should == '/sample/action%20time'
25
+ end
26
+
27
+ it "should generate a simple URL with a single variable (thats not a string)" do
28
+ @route_set.add_named_route(:sample, '/sample/:action/:id', :controller => 'sample')
29
+ @url_generator.generate(:sample, {:action => 'action', :id => 123}).should == '/sample/action/123'
30
+ end
31
+
32
+ it "should generate a simple URL with a glob variable" do
33
+ @route_set.add_named_route(:sample, '/sample/*action', :controller => 'sample')
34
+ @url_generator.generate(:sample, {:action => ['foo', 'baz']}).should == '/sample/foo/baz'
35
+ end
36
+
37
+ it "should generate a mutliple vairable URL from a hash" do
38
+ @route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
39
+ @url_generator.generate(:sample, {:first => 'zoo', :second => 'maz'}).should == '/sample/zoo/maz'
40
+ end
41
+
42
+ it "should generate a mutliple vairable URL from an array" do
43
+ @route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
44
+ @url_generator.generate(:sample, ['maz', 'zoo']).should == '/sample/maz/zoo'
45
+ end
46
+
47
+ it "should generate append extra hash variables to the end" do
48
+ @route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
49
+ @url_generator.generate(:sample, {:first => 'maz', :second => 'zoo', :third => 'zanz'}).should == '/sample/maz/zoo?third=zanz'
50
+ end
51
+
52
+ it "should generate append extra hash variables to the end (when the first parts are an array)" do
53
+ @route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
54
+ ['/sample/maz/zoo?four=jane&third=zanz', '/sample/maz/zoo?third=zanz&four=jane'].include?(@url_generator.generate(:sample, ['maz', 'zoo', {:third => 'zanz', :four => 'jane'}])).should == true
55
+ end
56
+
57
+ it "should generate append extra hash variables to the end using [] syntax if its an array" do
58
+ @route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
59
+ @url_generator.generate(:sample, {:first => 'maz', :second => 'zoo', :third => ['zanz', 'susie']}).should == '/sample/maz/zoo?third%5B%5D=zanz&third%5B%5D=susie'
60
+ end
61
+
62
+ it "should generate a mutliple vairable URL from an array" do
63
+ @route_set.add_named_route(:sample, '/sample/:first/:second', :controller => 'sample')
64
+ @url_generator.generate(:sample, ['maz', 'zoo']).should == '/sample/maz/zoo'
65
+ end
66
+
67
+ it "should generate a simple URL with a format" do
68
+ @route_set.add_named_route(:sample, '/sample/:action.:format', :controller => 'sample')
69
+ @url_generator.generate(:sample, {:action => 'action', :format => 'html'}).should == '/sample/action.html'
70
+ end
71
+
72
+ it "should generate from parameters" do
73
+ caf = @route_set.add_route('/:controller/:action.:format')
74
+ ca = @route_set.add_route('/:controller/:action')
75
+ @url_generator.generate(nil, {:controller => 'controller', :action => 'action'}).should == '/controller/action'
76
+ @url_generator.generate(nil, {:controller => 'controller', :action => 'action', :format => 'html'}).should == '/controller/action.html'
77
+ end
78
+
79
+ it "should use the first route when generating a URL from two ambiguous routes" do
80
+ @route_set.add_route('/:controller/:action')
81
+ @route_set.add_route('/:action/:controller')
82
+ @url_generator.generate(nil, {:controller => 'controller', :action => 'action'}).should == '/controller/action'
83
+ end
84
+
85
+ it "should accept an array of parameters" do
86
+ caf = @route_set.add_named_route(:name, '/:controller/:action.:format')
87
+ @url_generator.generate(:name, ['controller', 'action', 'html']).should == '/controller/action.html'
88
+ end
89
+
90
+ it "should generate a route with a specific host" do
91
+ caf = @route_set.add_named_route(:name, '/:controller/:action.:format', :generate_with => {:host => 'www.slashdot.org', :port => 80})
92
+ @url_generator.generate_full(:name, Rack::Request.new(Rack::MockRequest.env_for("http://localhost:8080")), ['controller', 'action', 'html']).should == 'http://www.slashdot.org/controller/action.html'
93
+ end
94
+
95
+ it "should require all the parameters (hash) to generate a route" do
96
+ proc {@url_generator.generate(@route_set.add_route('/:controller/:action'), {:controller => 'controller'})}.should raise_error Usher::MissingParameterException
97
+ end
98
+
99
+ it "should generate from a route" do
100
+ @url_generator.generate(@route_set.add_route('/:controller/:action'), {:controller => 'controller', :action => 'action'}).should == '/controller/action'
101
+ end
102
+
103
+ it "should require all the parameters (array) to generate a route" do
104
+ @route_set.add_named_route(:name, '/:controller/:action.:format')
105
+ proc {@url_generator.generate(:name, ['controller', 'action'])}.should raise_error Usher::MissingParameterException
106
+ end
107
+
108
+ it "should generate a route when only one parameter is given" do
109
+ @route_set.add_named_route(:name, '/:controller')
110
+ @url_generator.generate(:name, 'controller').should == '/controller'
111
+ end
112
+
113
+ it "should generate the correct route from a route containing optional parts" do
114
+ @route_set.add_named_route(:name, '/:controller(/:action(/:id))')
115
+ @url_generator.generate(:name, {:controller => 'controller'}).should == '/controller'
116
+ @url_generator.generate(:name, {:controller => 'controller', :action => 'action'}).should == '/controller/action'
117
+ @url_generator.generate(:name, {:controller => 'controller', :action => 'action', :id => 'id'}).should == '/controller/action/id'
118
+ end
119
+
120
+ it "should generate a route using defaults for everything but the first parameter" do
121
+ @route_set.add_named_route(:name, '/:one/:two/:three', {:default_values => {:one => 'one', :two => 'two', :three => 'three'}})
122
+ @url_generator.generate(:name, {:one => "1"}).should == '/1/two/three'
123
+ end
124
+
125
+ it "should generate a route using defaults for everything" do
126
+ @route_set.add_named_route(:name, '/:one/:two/:three', {:default_values => {:one => 'one', :two => 'two', :three => 'three'}})
127
+ @url_generator.generate(:name).should == '/one/two/three'
128
+ end
129
+
130
+ it "should generate a route using defaults and optionals using the last parameter" do
131
+ @route_set.add_named_route(:opts_with_defaults, '/:one(/:two(/:three))', {:default_values => {:one => '1', :two => '2', :three => '3'}})
132
+ @url_generator.generate(:opts_with_defaults, {:three => 'three'}).should == '/1/2/three'
133
+ end
134
+
135
+ it "should generate a route with optional segments given two nested optional parameters" do
136
+ @route_set.add_named_route(:optionals, '/:controller(/:action(/:id))(.:format)')
137
+ @url_generator.generate(:optionals, {:controller => "foo", :action => "bar"}).should == '/foo/bar'
138
+ end
139
+
140
+
141
+ end
@@ -0,0 +1,41 @@
1
+ require 'lib/usher'
2
+
3
+
4
+ describe "Usher grapher" do
5
+
6
+ before(:each) do
7
+ @route_set = Usher.new
8
+ @route_set.reset!
9
+ @url_generator = Usher::Generators::URL.new(@route_set)
10
+ end
11
+
12
+ it "should find a simple path" do
13
+ @route_set.add_route('/:a/:b/:c')
14
+ @url_generator.generate(nil, {:a => 'A', :b => 'B', :c => 'C'}).should == '/A/B/C'
15
+ end
16
+
17
+ it "should pick a more specific route" do
18
+ @route_set.add_route('/:a/:b')
19
+ @route_set.add_route('/:a/:b/:c')
20
+ @url_generator.generate(nil, {:a => 'A', :b => 'B', :c => 'C'}).should == '/A/B/C'
21
+ end
22
+
23
+ it "should fail to generate a route when none matches" do
24
+ @route_set.add_route('/:a/:b')
25
+ proc {@url_generator.generate(nil, {:c => 'C', :d => 'D'}) }.should raise_error Usher::UnrecognizedException
26
+ end
27
+
28
+ it "should find the most specific route and append extra parts on as a query string" do
29
+ @route_set.add_route('/:a/:b/:c')
30
+ @route_set.add_route('/:a/:b')
31
+ @url_generator.generate(nil, {:a => 'A', :b => 'B', :d => 'C'}).should == '/A/B?d=C'
32
+ end
33
+
34
+ # FIXME
35
+ #it "should do a validity check against the incoming variables when asked to" do
36
+ # route_set.add_route('/:a/:b', :b => /\d+/)
37
+ # route_set.generate_url(nil, {:a => 'A', :b => 'B'}).should == '/A/B'
38
+ # proc{ route_set.generate_url(nil, {:a => 'A', :b => 'B'})}.should raise_error Usher::ValidationException
39
+ #end
40
+
41
+ end
@@ -0,0 +1,68 @@
1
+ require 'lib/usher'
2
+
3
+ route_set = Usher.new
4
+
5
+ describe "Usher route adding" do
6
+
7
+ before(:each) do
8
+ route_set.reset!
9
+ end
10
+
11
+ it "should be empty after a reset" do
12
+ route_set.add_route('/sample', :controller => 'sample')
13
+ route_set.empty?.should == false
14
+ route_set.reset!
15
+ route_set.empty?.should == true
16
+ end
17
+
18
+ it "shouldn't care about routes without a controller" do
19
+ proc { route_set.add_route('/bad/route') }.should_not raise_error
20
+ end
21
+
22
+ it "should add every kind of optional route possible" do
23
+ route_set.add_route('/a/b(/c)(/d(/e))')
24
+ route_set.routes.first.paths.collect{|a| a.parts }.should == [
25
+ ['/', "a", '/', "b"],
26
+ ['/', "a", '/', "b", '/', "c", '/', "d"],
27
+ ['/', "a", '/', "b", '/', "d", '/', "e"],
28
+ ['/', "a", '/', "b", '/', "c"],
29
+ ['/', "a", '/', "b", '/', "d"],
30
+ ['/', "a", '/', "b", '/', "c", '/', "d", '/', "e"]
31
+ ]
32
+
33
+ end
34
+
35
+ it "should allow named routes to be added" do
36
+ route_set.add_named_route(:route, '/bad/route', :controller => 'sample').should == route_set.named_routes[:route]
37
+ end
38
+
39
+ it "should calculate depths for nodes" do
40
+ route_set.add_named_route(:route, '/bad/route/three/four')
41
+ route_set.tree.depth.should == 0
42
+ route_set.tree.lookup['/'].depth.should == 1
43
+ end
44
+
45
+ it "should pp for nodes" do
46
+ route_set.add_named_route(:route, '/bad/route/three/four')
47
+ route_set.tree.depth.should == 0
48
+ old_out = $stdout
49
+ $stdout = (output = StringIO.new)
50
+ route_set.tree.lookup['/'].lookup['bad'].lookup['/'].pp
51
+ $stdout = old_out
52
+ output.rewind
53
+ output.read.should == <<-HEREDOC
54
+ 3: "/" false
55
+ route ==>
56
+ 4: "route" false
57
+ / ==>
58
+ 5: "/" false
59
+ three ==>
60
+ 6: "three" false
61
+ / ==>
62
+ 7: "/" false
63
+ four ==>
64
+ 8: "four" true
65
+ HEREDOC
66
+ end
67
+
68
+ end
@@ -0,0 +1,29 @@
1
+ require 'lib/usher'
2
+
3
+ require 'rack'
4
+
5
+ route_set = Usher::Interface.for(:rack)
6
+
7
+ describe "Usher (for rack) route dispatching" do
8
+
9
+ before(:each) do
10
+ route_set.reset!
11
+ end
12
+
13
+ it "should dispatch a simple request" do
14
+ app = mock 'app'
15
+ app.should_receive(:call).once.with {|v| v['usher.params'].should == {} }
16
+ route_set.add('/sample').to(app)
17
+ route_set.call(Rack::MockRequest.env_for("/sample", :method => 'GET'))
18
+ end
19
+
20
+ it "should dispatch a POST request" do
21
+ bad_app = mock 'bad_app'
22
+ app = mock 'app'
23
+ app.should_receive(:call).once.with {|v| v['usher.params'].should == {} }
24
+ route_set.add('/sample').to(bad_app)
25
+ route_set.add('/sample', :requirements => {:request_method => 'POST'}).to(app)
26
+ route_set.call(Rack::MockRequest.env_for("/sample", :request_method => 'POST'))
27
+ end
28
+
29
+ end
@@ -0,0 +1 @@
1
+ require 'activesupport'