journey 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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