usher 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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'