andyjeffries-journey 1.0.0

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 +49 -0
  6. data/README.rdoc +48 -0
  7. data/Rakefile +31 -0
  8. data/journey.gemspec +42 -0
  9. data/lib/journey/backwards.rb +5 -0
  10. data/lib/journey/core-ext/hash.rb +11 -0
  11. data/lib/journey/formatter.rb +129 -0
  12. data/lib/journey/gtg/builder.rb +159 -0
  13. data/lib/journey/gtg/simulator.rb +44 -0
  14. data/lib/journey/gtg/transition_table.rb +152 -0
  15. data/lib/journey/nfa/builder.rb +74 -0
  16. data/lib/journey/nfa/dot.rb +34 -0
  17. data/lib/journey/nfa/simulator.rb +45 -0
  18. data/lib/journey/nfa/transition_table.rb +164 -0
  19. data/lib/journey/nodes/node.rb +104 -0
  20. data/lib/journey/parser.rb +204 -0
  21. data/lib/journey/parser.y +47 -0
  22. data/lib/journey/parser_extras.rb +21 -0
  23. data/lib/journey/path/pattern.rb +190 -0
  24. data/lib/journey/route.rb +92 -0
  25. data/lib/journey/router/strexp.rb +22 -0
  26. data/lib/journey/router/utils.rb +57 -0
  27. data/lib/journey/router.rb +138 -0
  28. data/lib/journey/routes.rb +74 -0
  29. data/lib/journey/scanner.rb +58 -0
  30. data/lib/journey/visitors.rb +186 -0
  31. data/lib/journey/visualizer/d3.min.js +2 -0
  32. data/lib/journey/visualizer/fsm.css +34 -0
  33. data/lib/journey/visualizer/fsm.js +134 -0
  34. data/lib/journey/visualizer/index.html.erb +50 -0
  35. data/lib/journey/visualizer/reset.css +48 -0
  36. data/lib/journey.rb +5 -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 +38 -0
  51. metadata +171 -0
@@ -0,0 +1,134 @@
1
+ function tokenize(input, callback) {
2
+ while(input.length > 0) {
3
+ callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]);
4
+ input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, '');
5
+ }
6
+ }
7
+
8
+ var graph = d3.select("#chart-2 svg");
9
+ var svg_edges = {};
10
+ var svg_nodes = {};
11
+
12
+ graph.selectAll("g.edge").each(function() {
13
+ var node = d3.select(this);
14
+ var index = node.select("title").text().split("->");
15
+ var left = parseInt(index[0]);
16
+ var right = parseInt(index[1]);
17
+
18
+ if(!svg_edges[left]) { svg_edges[left] = {} }
19
+ svg_edges[left][right] = node;
20
+ });
21
+
22
+ graph.selectAll("g.node").each(function() {
23
+ var node = d3.select(this);
24
+ var index = parseInt(node.select("title").text());
25
+ svg_nodes[index] = node;
26
+ });
27
+
28
+ function reset_graph() {
29
+ for(var key in svg_edges) {
30
+ for(var mkey in svg_edges[key]) {
31
+ var node = svg_edges[key][mkey];
32
+ var path = node.select("path");
33
+ var arrow = node.select("polygon");
34
+ path.style("stroke", "black");
35
+ arrow.style("stroke", "black").style("fill", "black");
36
+ }
37
+ }
38
+
39
+ for(var key in svg_nodes) {
40
+ var node = svg_nodes[key];
41
+ node.select('ellipse').style("fill", "white");
42
+ node.select('polygon').style("fill", "white");
43
+ }
44
+ return false;
45
+ }
46
+
47
+ function highlight_edge(from, to) {
48
+ var node = svg_edges[from][to];
49
+ var path = node.select("path");
50
+ var arrow = node.select("polygon");
51
+
52
+ path
53
+ .transition().duration(500)
54
+ .style("stroke", "green");
55
+
56
+ arrow
57
+ .transition().duration(500)
58
+ .style("stroke", "green").style("fill", "green");
59
+ }
60
+
61
+ function highlight_state(index, color) {
62
+ if(!color) { color = "green"; }
63
+
64
+ svg_nodes[index].select('ellipse')
65
+ .style("fill", "white")
66
+ .transition().duration(500)
67
+ .style("fill", color);
68
+ }
69
+
70
+ function highlight_finish(index) {
71
+ svg_nodes[index].select('polygon')
72
+ .style("fill", "while")
73
+ .transition().duration(500)
74
+ .style("fill", "blue");
75
+ }
76
+
77
+ function match(input) {
78
+ reset_graph();
79
+ var table = tt();
80
+ var states = [0];
81
+ var regexp_states = table['regexp_states'];
82
+ var string_states = table['string_states'];
83
+ var accepting = table['accepting'];
84
+
85
+ highlight_state(0);
86
+
87
+ tokenize(input, function(token) {
88
+ var new_states = [];
89
+ for(var key in states) {
90
+ var state = states[key];
91
+
92
+ if(string_states[state] && string_states[state][token]) {
93
+ var new_state = string_states[state][token];
94
+ highlight_edge(state, new_state);
95
+ highlight_state(new_state);
96
+ new_states.push(new_state);
97
+ }
98
+
99
+ if(regexp_states[state]) {
100
+ for(var key in regexp_states[state]) {
101
+ var re = new RegExp("^" + key + "$");
102
+ if(re.test(token)) {
103
+ var new_state = regexp_states[state][key];
104
+ highlight_edge(state, new_state);
105
+ highlight_state(new_state);
106
+ new_states.push(new_state);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ if(new_states.length == 0) {
113
+ return;
114
+ }
115
+ states = new_states;
116
+ });
117
+
118
+ for(var key in states) {
119
+ var state = states[key];
120
+ if(accepting[state]) {
121
+ for(var mkey in svg_edges[state]) {
122
+ if(!regexp_states[mkey] && !string_states[mkey]) {
123
+ highlight_edge(state, mkey);
124
+ highlight_finish(mkey);
125
+ }
126
+ }
127
+ } else {
128
+ highlight_state(state, "red");
129
+ }
130
+ }
131
+
132
+ return false;
133
+ }
134
+
@@ -0,0 +1,50 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= title %></title>
5
+ <style>
6
+ <% stylesheets.each do |style| %>
7
+ <%= style %>
8
+ <% end %>
9
+ </style>
10
+ </head>
11
+ <body>
12
+ <div id="wrapper">
13
+ <h1>Routes FSM with NFA simulation</h1>
14
+ <div class="instruction form">
15
+ <p>
16
+ Type a route in to the box and click "simulate".
17
+ </p>
18
+ <form onsubmit="return match(this.route.value);">
19
+ <input type="text" size="30" name="route" value="/articles/new" />
20
+ <button>simulate</button>
21
+ <input type="reset" value="reset" onclick="return reset_graph();"/>
22
+ </form>
23
+ <p class="fun_routes">
24
+ Some fun routes to try:
25
+ <% fun_routes.each do |path| %>
26
+ <a href="#" onclick="document.forms[0].elements[0].value=this.text.replace(/^\s+|\s+$/g,''); return match(this.text.replace(/^\s+|\s+$/g,''));">
27
+ <%= path %>
28
+ </a>
29
+ <% end %>
30
+ </p>
31
+ </div>
32
+ <div class='chart' id='chart-2'>
33
+ <%= svg %>
34
+ </div>
35
+ <div class="instruction">
36
+ <p>
37
+ This is a FSM for a system that has the following routes:
38
+ </p>
39
+ <ul>
40
+ <% paths.each do |route| %>
41
+ <li><%= route %></li>
42
+ <% end %>
43
+ </ul>
44
+ </div>
45
+ </div>
46
+ <% javascripts.each do |js| %>
47
+ <script><%= js %></script>
48
+ <% end %>
49
+ </body>
50
+ </html>
@@ -0,0 +1,48 @@
1
+ /* http://meyerweb.com/eric/tools/css/reset/
2
+ v2.0 | 20110126
3
+ License: none (public domain)
4
+ */
5
+
6
+ html, body, div, span, applet, object, iframe,
7
+ h1, h2, h3, h4, h5, h6, p, blockquote, pre,
8
+ a, abbr, acronym, address, big, cite, code,
9
+ del, dfn, em, img, ins, kbd, q, s, samp,
10
+ small, strike, strong, sub, sup, tt, var,
11
+ b, u, i, center,
12
+ dl, dt, dd, ol, ul, li,
13
+ fieldset, form, label, legend,
14
+ table, caption, tbody, tfoot, thead, tr, th, td,
15
+ article, aside, canvas, details, embed,
16
+ figure, figcaption, footer, header, hgroup,
17
+ menu, nav, output, ruby, section, summary,
18
+ time, mark, audio, video {
19
+ margin: 0;
20
+ padding: 0;
21
+ border: 0;
22
+ font-size: 100%;
23
+ font: inherit;
24
+ vertical-align: baseline;
25
+ }
26
+ /* HTML5 display-role reset for older browsers */
27
+ article, aside, details, figcaption, figure,
28
+ footer, header, hgroup, menu, nav, section {
29
+ display: block;
30
+ }
31
+ body {
32
+ line-height: 1;
33
+ }
34
+ ol, ul {
35
+ list-style: none;
36
+ }
37
+ blockquote, q {
38
+ quotes: none;
39
+ }
40
+ blockquote:before, blockquote:after,
41
+ q:before, q:after {
42
+ content: '';
43
+ content: none;
44
+ }
45
+ table {
46
+ border-collapse: collapse;
47
+ border-spacing: 0;
48
+ }
data/lib/journey.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'journey/router'
2
+ require 'journey/gtg/builder'
3
+ require 'journey/gtg/simulator'
4
+ require 'journey/nfa/builder'
5
+ require 'journey/nfa/simulator'
@@ -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