journey 1.0.0.rc1

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 (51) hide show
  1. data/.autotest +8 -0
  2. data/.gemtest +0 -0
  3. data/CHANGELOG.rdoc +6 -0
  4. data/Gemfile +11 -0
  5. data/Manifest.txt +50 -0
  6. data/README.rdoc +48 -0
  7. data/Rakefile +31 -0
  8. data/journey.gemspec +42 -0
  9. data/lib/journey.rb +5 -0
  10. data/lib/journey/backwards.rb +5 -0
  11. data/lib/journey/core-ext/hash.rb +11 -0
  12. data/lib/journey/formatter.rb +129 -0
  13. data/lib/journey/gtg/builder.rb +159 -0
  14. data/lib/journey/gtg/simulator.rb +44 -0
  15. data/lib/journey/gtg/transition_table.rb +152 -0
  16. data/lib/journey/nfa/builder.rb +74 -0
  17. data/lib/journey/nfa/dot.rb +34 -0
  18. data/lib/journey/nfa/simulator.rb +45 -0
  19. data/lib/journey/nfa/transition_table.rb +164 -0
  20. data/lib/journey/nodes/node.rb +104 -0
  21. data/lib/journey/parser.rb +204 -0
  22. data/lib/journey/parser.y +47 -0
  23. data/lib/journey/parser_extras.rb +21 -0
  24. data/lib/journey/path/pattern.rb +190 -0
  25. data/lib/journey/route.rb +92 -0
  26. data/lib/journey/router.rb +138 -0
  27. data/lib/journey/router/strexp.rb +22 -0
  28. data/lib/journey/router/utils.rb +57 -0
  29. data/lib/journey/routes.rb +74 -0
  30. data/lib/journey/scanner.rb +58 -0
  31. data/lib/journey/visitors.rb +186 -0
  32. data/lib/journey/visualizer/d3.min.js +2 -0
  33. data/lib/journey/visualizer/fsm.css +34 -0
  34. data/lib/journey/visualizer/fsm.js +134 -0
  35. data/lib/journey/visualizer/index.html.erb +50 -0
  36. data/lib/journey/visualizer/reset.css +48 -0
  37. data/test/gtg/test_builder.rb +77 -0
  38. data/test/gtg/test_transition_table.rb +113 -0
  39. data/test/helper.rb +4 -0
  40. data/test/nfa/test_simulator.rb +96 -0
  41. data/test/nfa/test_transition_table.rb +70 -0
  42. data/test/nodes/test_symbol.rb +15 -0
  43. data/test/path/test_pattern.rb +260 -0
  44. data/test/route/definition/test_parser.rb +108 -0
  45. data/test/route/definition/test_scanner.rb +52 -0
  46. data/test/router/test_strexp.rb +30 -0
  47. data/test/router/test_utils.rb +19 -0
  48. data/test/test_route.rb +95 -0
  49. data/test/test_router.rb +464 -0
  50. data/test/test_routes.rb +51 -0
  51. metadata +168 -0
@@ -0,0 +1,77 @@
1
+ require 'helper'
2
+
3
+ module Journey
4
+ module GTG
5
+ class TestBuilder < MiniTest::Unit::TestCase
6
+ def test_following_states_multi
7
+ table = tt ['a|a']
8
+ assert_equal 1, table.move(0, 'a').length
9
+ end
10
+
11
+ def test_following_states_multi_regexp
12
+ table = tt [':a|b']
13
+ assert_equal 1, table.move(0, 'fooo').length
14
+ assert_equal 2, table.move(0, 'b').length
15
+ end
16
+
17
+ def test_multi_path
18
+ table = tt ['/:a/d', '/b/c']
19
+
20
+ [
21
+ [1, '/'],
22
+ [2, 'b'],
23
+ [2, '/'],
24
+ [1, 'c'],
25
+ ].inject(0) { |state, (exp, sym)|
26
+ new = table.move(state, sym)
27
+ assert_equal exp, new.length
28
+ new
29
+ }
30
+ end
31
+
32
+ def test_match_data_ambiguous
33
+ table = tt %w{
34
+ /articles(.:format)
35
+ /articles/new(.:format)
36
+ /articles/:id/edit(.:format)
37
+ /articles/:id(.:format)
38
+ }
39
+
40
+ sim = NFA::Simulator.new table
41
+
42
+ match = sim.match '/articles/new'
43
+ assert_equal 2, match.memos.length
44
+ end
45
+
46
+ ##
47
+ # Identical Routes may have different restrictions.
48
+ def test_match_same_paths
49
+ table = tt %w{
50
+ /articles/new(.:format)
51
+ /articles/new(.:format)
52
+ }
53
+
54
+ sim = NFA::Simulator.new table
55
+
56
+ match = sim.match '/articles/new'
57
+ assert_equal 2, match.memos.length
58
+ end
59
+
60
+ private
61
+ def ast strings
62
+ parser = Journey::Parser.new
63
+ asts = strings.map { |string|
64
+ memo = Object.new
65
+ ast = parser.parse string
66
+ ast.each { |n| n.memo = memo }
67
+ ast
68
+ }
69
+ Nodes::Or.new asts
70
+ end
71
+
72
+ def tt strings
73
+ Builder.new(ast(strings)).transition_table
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,113 @@
1
+ require 'helper'
2
+ require 'json'
3
+
4
+ module Journey
5
+ module GTG
6
+ class TestGeneralizedTable < MiniTest::Unit::TestCase
7
+ def test_to_json
8
+ table = tt %w{
9
+ /articles(.:format)
10
+ /articles/new(.:format)
11
+ /articles/:id/edit(.:format)
12
+ /articles/:id(.:format)
13
+ }
14
+
15
+ json = JSON.load table.to_json
16
+ assert json['regexp_states']
17
+ assert json['string_states']
18
+ assert json['accepting']
19
+ end
20
+
21
+ if system("dot -V 2>/dev/null")
22
+ def test_to_svg
23
+ table = tt %w{
24
+ /articles(.:format)
25
+ /articles/new(.:format)
26
+ /articles/:id/edit(.:format)
27
+ /articles/:id(.:format)
28
+ }
29
+ svg = table.to_svg
30
+ assert svg
31
+ refute_match(/DOCTYPE/, svg)
32
+ end
33
+ end
34
+
35
+ def test_simulate_gt
36
+ sim = simulator_for ['/foo', '/bar']
37
+ assert_match sim, '/foo'
38
+ end
39
+
40
+ def test_simulate_gt_regexp
41
+ sim = simulator_for [':foo']
42
+ assert_match sim, 'foo'
43
+ end
44
+
45
+ def test_simulate_gt_regexp_mix
46
+ sim = simulator_for ['/get', '/:method/foo']
47
+ assert_match sim, '/get'
48
+ assert_match sim, '/get/foo'
49
+ end
50
+
51
+ def test_simulate_optional
52
+ sim = simulator_for ['/foo(/bar)']
53
+ assert_match sim, '/foo'
54
+ assert_match sim, '/foo/bar'
55
+ refute_match sim, '/foo/'
56
+ end
57
+
58
+ def test_match_data
59
+ path_asts = asts %w{ /get /:method/foo }
60
+ paths = path_asts.dup
61
+
62
+ builder = GTG::Builder.new Nodes::Or.new path_asts
63
+ tt = builder.transition_table
64
+
65
+ sim = GTG::Simulator.new tt
66
+
67
+ match = sim.match '/get'
68
+ assert_equal [paths.first], match.memos
69
+
70
+ match = sim.match '/get/foo'
71
+ assert_equal [paths.last], match.memos
72
+ end
73
+
74
+ def test_match_data_ambiguous
75
+ path_asts = asts %w{
76
+ /articles(.:format)
77
+ /articles/new(.:format)
78
+ /articles/:id/edit(.:format)
79
+ /articles/:id(.:format)
80
+ }
81
+
82
+ paths = path_asts.dup
83
+ ast = Nodes::Or.new path_asts
84
+
85
+ builder = GTG::Builder.new ast
86
+ sim = GTG::Simulator.new builder.transition_table
87
+
88
+ match = sim.match '/articles/new'
89
+ assert_equal [paths[1], paths[3]], match.memos
90
+ end
91
+
92
+ private
93
+ def asts paths
94
+ parser = Journey::Parser.new
95
+ paths.map { |x|
96
+ ast = parser.parse x
97
+ ast.each { |n| n.memo = ast}
98
+ ast
99
+ }
100
+ end
101
+
102
+ def tt paths
103
+ x = asts paths
104
+ builder = GTG::Builder.new Nodes::Or.new x
105
+ builder.transition_table
106
+ end
107
+
108
+ def simulator_for paths
109
+ GTG::Simulator.new tt(paths)
110
+ end
111
+ end
112
+ end
113
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'minitest/autorun'
3
+ require 'journey'
4
+ require 'stringio'
@@ -0,0 +1,96 @@
1
+ require 'helper'
2
+
3
+ module Journey
4
+ module NFA
5
+ class TestSimulator < MiniTest::Unit::TestCase
6
+ def test_simulate_simple
7
+ sim = simulator_for ['/foo']
8
+ assert_match sim, '/foo'
9
+ end
10
+
11
+ def test_simulate_simple_no_match
12
+ sim = simulator_for ['/foo']
13
+ refute_match sim, 'foo'
14
+ end
15
+
16
+ def test_simulate_simple_no_match_too_long
17
+ sim = simulator_for ['/foo']
18
+ refute_match sim, '/foo/bar'
19
+ end
20
+
21
+ def test_simulate_simple_no_match_wrong_string
22
+ sim = simulator_for ['/foo']
23
+ refute_match sim, '/bar'
24
+ end
25
+
26
+ def test_simulate_regex
27
+ sim = simulator_for ['/:foo/bar']
28
+ assert_match sim, '/bar/bar'
29
+ assert_match sim, '/foo/bar'
30
+ end
31
+
32
+ def test_simulate_or
33
+ sim = simulator_for ['/foo', '/bar']
34
+ assert_match sim, '/bar'
35
+ assert_match sim, '/foo'
36
+ refute_match sim, '/baz'
37
+ end
38
+
39
+ def test_simulate_optional
40
+ sim = simulator_for ['/foo(/bar)']
41
+ assert_match sim, '/foo'
42
+ assert_match sim, '/foo/bar'
43
+ refute_match sim, '/foo/'
44
+ end
45
+
46
+ def test_matchdata_has_memos
47
+ paths = %w{ /foo /bar }
48
+ parser = Journey::Parser.new
49
+ asts = paths.map { |x|
50
+ ast = parser.parse x
51
+ ast.each { |n| n.memo = ast}
52
+ ast
53
+ }
54
+
55
+ expected = asts.first
56
+
57
+ builder = Builder.new Nodes::Or.new asts
58
+
59
+ sim = Simulator.new builder.transition_table
60
+
61
+ md = sim.match '/foo'
62
+ assert_equal [expected], md.memos
63
+ end
64
+
65
+ def test_matchdata_memos_on_merge
66
+ parser = Journey::Parser.new
67
+ routes = [
68
+ '/articles(.:format)',
69
+ '/articles/new(.:format)',
70
+ '/articles/:id/edit(.:format)',
71
+ '/articles/:id(.:format)',
72
+ ].map { |path|
73
+ ast = parser.parse path
74
+ ast.each { |n| n.memo = ast }
75
+ ast
76
+ }
77
+
78
+ asts = routes.dup
79
+
80
+ ast = Nodes::Or.new routes
81
+
82
+ nfa = Journey::NFA::Builder.new ast
83
+ sim = Simulator.new nfa.transition_table
84
+ md = sim.match '/articles'
85
+ assert_equal [asts.first], md.memos
86
+ end
87
+
88
+ def simulator_for paths
89
+ parser = Journey::Parser.new
90
+ asts = paths.map { |x| parser.parse x }
91
+ builder = Builder.new Nodes::Or.new asts
92
+ Simulator.new builder.transition_table
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,70 @@
1
+ require 'helper'
2
+
3
+ module Journey
4
+ module NFA
5
+ class TestTransitionTable < MiniTest::Unit::TestCase
6
+ def setup
7
+ @parser = Journey::Parser.new
8
+ end
9
+
10
+ def test_eclosure
11
+ table = tt '/'
12
+ assert_equal [0], table.eclosure(0)
13
+
14
+ table = tt ':a|:b'
15
+ assert_equal 3, table.eclosure(0).length
16
+
17
+ table = tt '(:a|:b)'
18
+ assert_equal 5, table.eclosure(0).length
19
+ assert_equal 5, table.eclosure([0]).length
20
+ end
21
+
22
+ def test_following_states_one
23
+ table = tt '/'
24
+
25
+ assert_equal [1], table.following_states(0, '/')
26
+ assert_equal [1], table.following_states([0], '/')
27
+ end
28
+
29
+ def test_following_states_group
30
+ table = tt 'a|b'
31
+ states = table.eclosure 0
32
+
33
+ assert_equal 1, table.following_states(states, 'a').length
34
+ assert_equal 1, table.following_states(states, 'b').length
35
+ end
36
+
37
+ def test_following_states_multi
38
+ table = tt 'a|a'
39
+ states = table.eclosure 0
40
+
41
+ assert_equal 2, table.following_states(states, 'a').length
42
+ assert_equal 0, table.following_states(states, 'b').length
43
+ end
44
+
45
+ def test_following_states_regexp
46
+ table = tt 'a|:a'
47
+ states = table.eclosure 0
48
+
49
+ assert_equal 1, table.following_states(states, 'a').length
50
+ assert_equal 1, table.following_states(states, /[^\.\/\?]+/).length
51
+ assert_equal 0, table.following_states(states, 'b').length
52
+ end
53
+
54
+ def test_alphabet
55
+ table = tt 'a|:a'
56
+ assert_equal ['a', /[^\.\/\?]+/], table.alphabet
57
+
58
+ table = tt 'a|a'
59
+ assert_equal ['a'], table.alphabet
60
+ end
61
+
62
+ private
63
+ def tt string
64
+ ast = @parser.parse string
65
+ builder = Builder.new ast
66
+ builder.transition_table
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,15 @@
1
+ require 'helper'
2
+
3
+ module Journey
4
+ module Nodes
5
+ class TestSymbol < MiniTest::Unit::TestCase
6
+ def test_default_regexp?
7
+ sym = Symbol.new nil
8
+ assert sym.default_regexp?
9
+
10
+ sym.regexp = nil
11
+ refute sym.default_regexp?
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,260 @@
1
+ require 'helper'
2
+
3
+ module Journey
4
+ module Path
5
+ class TestPattern < MiniTest::Unit::TestCase
6
+ x = /.+/
7
+ {
8
+ '/:controller(/:action)' => %r{\A/(#{x}?)(?:/([^/.?]+))?\Z},
9
+ '/:controller/foo' => %r{\A/(#{x}?)/foo\Z},
10
+ '/:controller/:action' => %r{\A/(#{x}?)/([^/.?]+)\Z},
11
+ '/:controller' => %r{\A/(#{x}?)\Z},
12
+ '/:controller(/:action(/:id))' => %r{\A/(#{x}?)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z},
13
+ '/:controller/:action.xml' => %r{\A/(#{x}?)/([^/.?]+)\.xml\Z},
14
+ '/:controller.:format' => %r{\A/(#{x}?)\.([^/.?]+)\Z},
15
+ '/:controller(.:format)' => %r{\A/(#{x}?)(?:\.([^/.?]+))?\Z},
16
+ '/:controller/*foo' => %r{\A/(#{x}?)/(.+)\Z},
17
+ '/:controller/*foo/bar' => %r{\A/(#{x}?)/(.+)/bar\Z},
18
+ }.each do |path, expected|
19
+ define_method(:"test_to_regexp_#{path}") do
20
+ strexp = Router::Strexp.new(
21
+ path,
22
+ { :controller => /.+/ },
23
+ ["/", ".", "?"]
24
+ )
25
+ path = Pattern.new strexp
26
+ assert_equal(expected, path.send(:to_regexp))
27
+ end
28
+ end
29
+
30
+ {
31
+ '/:controller(/:action)' => %r{\A/(#{x}?)(?:/([^/.?]+))?},
32
+ '/:controller/foo' => %r{\A/(#{x}?)/foo},
33
+ '/:controller/:action' => %r{\A/(#{x}?)/([^/.?]+)},
34
+ '/:controller' => %r{\A/(#{x}?)},
35
+ '/:controller(/:action(/:id))' => %r{\A/(#{x}?)(?:/([^/.?]+)(?:/([^/.?]+))?)?},
36
+ '/:controller/:action.xml' => %r{\A/(#{x}?)/([^/.?]+)\.xml},
37
+ '/:controller.:format' => %r{\A/(#{x}?)\.([^/.?]+)},
38
+ '/:controller(.:format)' => %r{\A/(#{x}?)(?:\.([^/.?]+))?},
39
+ '/:controller/*foo' => %r{\A/(#{x}?)/(.+)},
40
+ '/:controller/*foo/bar' => %r{\A/(#{x}?)/(.+)/bar},
41
+ }.each do |path, expected|
42
+ define_method(:"test_to_non_anchored_regexp_#{path}") do
43
+ strexp = Router::Strexp.new(
44
+ path,
45
+ { :controller => /.+/ },
46
+ ["/", ".", "?"],
47
+ false
48
+ )
49
+ path = Pattern.new strexp
50
+ assert_equal(expected, path.send(:to_regexp))
51
+ end
52
+ end
53
+
54
+ {
55
+ '/:controller(/:action)' => %w{ controller action },
56
+ '/:controller/foo' => %w{ controller },
57
+ '/:controller/:action' => %w{ controller action },
58
+ '/:controller' => %w{ controller },
59
+ '/:controller(/:action(/:id))' => %w{ controller action id },
60
+ '/:controller/:action.xml' => %w{ controller action },
61
+ '/:controller.:format' => %w{ controller format },
62
+ '/:controller(.:format)' => %w{ controller format },
63
+ '/:controller/*foo' => %w{ controller foo },
64
+ '/:controller/*foo/bar' => %w{ controller foo },
65
+ }.each do |path, expected|
66
+ define_method(:"test_names_#{path}") do
67
+ strexp = Router::Strexp.new(
68
+ path,
69
+ { :controller => /.+/ },
70
+ ["/", ".", "?"]
71
+ )
72
+ path = Pattern.new strexp
73
+ assert_equal(expected, path.names)
74
+ end
75
+ end
76
+
77
+ def test_to_regexp_with_extended_group
78
+ strexp = Router::Strexp.new(
79
+ '/page/:name',
80
+ { :name => /
81
+ #ROFL
82
+ (tender|love
83
+ #MAO
84
+ )/x },
85
+ ["/", ".", "?"]
86
+ )
87
+ path = Pattern.new strexp
88
+ assert_match('/page/tender', path)
89
+ assert_match('/page/love', path)
90
+ refute_match('/page/loving', path)
91
+ end
92
+
93
+ def test_optional_names
94
+ [
95
+ ['/:foo(/:bar(/:baz))', %w{ bar baz }],
96
+ ['/:foo(/:bar)', %w{ bar }],
97
+ ['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }],
98
+ ].each do |pattern, list|
99
+ path = Pattern.new pattern
100
+ assert_equal list.sort, path.optional_names.sort
101
+ end
102
+ end
103
+
104
+ def test_to_regexp_with_group
105
+ strexp = Router::Strexp.new(
106
+ '/page/:name',
107
+ { :name => /(tender|love)/ },
108
+ ["/", ".", "?"]
109
+ )
110
+ path = Pattern.new strexp
111
+ assert_match('/page/tender', path)
112
+ assert_match('/page/love', path)
113
+ refute_match('/page/loving', path)
114
+ end
115
+
116
+ def test_ast_sets_regular_expressions
117
+ requirements = { :name => /(tender|love)/, :value => /./ }
118
+ strexp = Router::Strexp.new(
119
+ '/page/:name/:value',
120
+ requirements,
121
+ ["/", ".", "?"]
122
+ )
123
+
124
+ assert_equal requirements, strexp.requirements
125
+
126
+ path = Pattern.new strexp
127
+ nodes = path.ast.grep(Nodes::Symbol)
128
+ assert_equal 2, nodes.length
129
+ nodes.each do |node|
130
+ assert_equal requirements[node.to_sym], node.regexp
131
+ end
132
+ end
133
+
134
+ def test_match_data_with_group
135
+ strexp = Router::Strexp.new(
136
+ '/page/:name',
137
+ { :name => /(tender|love)/ },
138
+ ["/", ".", "?"]
139
+ )
140
+ path = Pattern.new strexp
141
+ match = path.match '/page/tender'
142
+ assert_equal 'tender', match[1]
143
+ assert_equal 2, match.length
144
+ end
145
+
146
+ def test_match_data_with_multi_group
147
+ strexp = Router::Strexp.new(
148
+ '/page/:name/:id',
149
+ { :name => /t(((ender|love)))()/ },
150
+ ["/", ".", "?"]
151
+ )
152
+ path = Pattern.new strexp
153
+ match = path.match '/page/tender/10'
154
+ assert_equal 'tender', match[1]
155
+ assert_equal '10', match[2]
156
+ assert_equal 3, match.length
157
+ assert_equal %w{ tender 10 }, match.captures
158
+ end
159
+
160
+ def test_insensitive_regexp_with_group
161
+ strexp = Router::Strexp.new(
162
+ '/page/:name/aaron',
163
+ { :name => /(tender|love)/i },
164
+ ["/", ".", "?"]
165
+ )
166
+ path = Pattern.new strexp
167
+ assert_match('/page/TENDER/aaron', path)
168
+ assert_match('/page/loVE/aaron', path)
169
+ refute_match('/page/loVE/AAron', path)
170
+ end
171
+
172
+ def test_to_regexp_with_strexp
173
+ strexp = Router::Strexp.new('/:controller', { }, ["/", ".", "?"])
174
+ path = Pattern.new strexp
175
+ x = %r{\A/([^/.?]+)\Z}
176
+
177
+ assert_equal(x.source, path.source)
178
+ end
179
+
180
+ def test_to_regexp_defaults
181
+ path = Pattern.new '/:controller(/:action(/:id))'
182
+ expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}
183
+ assert_equal expected, path.send(:to_regexp)
184
+ end
185
+
186
+ def test_failed_match
187
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
188
+ uri = 'content'
189
+
190
+ refute path =~ uri
191
+ end
192
+
193
+ def test_match_controller
194
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
195
+ uri = '/content'
196
+
197
+ match = path =~ uri
198
+ assert_equal %w{ controller action id format }, match.names
199
+ assert_equal 'content', match[1]
200
+ assert_nil match[2]
201
+ assert_nil match[3]
202
+ assert_nil match[4]
203
+ end
204
+
205
+ def test_match_controller_action
206
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
207
+ uri = '/content/list'
208
+
209
+ match = path =~ uri
210
+ assert_equal %w{ controller action id format }, match.names
211
+ assert_equal 'content', match[1]
212
+ assert_equal 'list', match[2]
213
+ assert_nil match[3]
214
+ assert_nil match[4]
215
+ end
216
+
217
+ def test_match_controller_action_id
218
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
219
+ uri = '/content/list/10'
220
+
221
+ match = path =~ uri
222
+ assert_equal %w{ controller action id format }, match.names
223
+ assert_equal 'content', match[1]
224
+ assert_equal 'list', match[2]
225
+ assert_equal '10', match[3]
226
+ assert_nil match[4]
227
+ end
228
+
229
+ def test_match_literal
230
+ path = Path::Pattern.new "/books(/:action(.:format))"
231
+
232
+ uri = '/books'
233
+ match = path =~ uri
234
+ assert_equal %w{ action format }, match.names
235
+ assert_nil match[1]
236
+ assert_nil match[2]
237
+ end
238
+
239
+ def test_match_literal_with_action
240
+ path = Path::Pattern.new "/books(/:action(.:format))"
241
+
242
+ uri = '/books/list'
243
+ match = path =~ uri
244
+ assert_equal %w{ action format }, match.names
245
+ assert_equal 'list', match[1]
246
+ assert_nil match[2]
247
+ end
248
+
249
+ def test_match_literal_with_action_and_format
250
+ path = Path::Pattern.new "/books(/:action(.:format))"
251
+
252
+ uri = '/books/list.rss'
253
+ match = path =~ uri
254
+ assert_equal %w{ action format }, match.names
255
+ assert_equal 'list', match[1]
256
+ assert_equal 'rss', match[2]
257
+ end
258
+ end
259
+ end
260
+ end