andyjeffries-journey 1.0.0

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 +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,204 @@
1
+ #
2
+ # DO NOT MODIFY!!!!
3
+ # This file is automatically generated by Racc 1.4.6
4
+ # from Racc grammer file "".
5
+ #
6
+
7
+ require 'racc/parser.rb'
8
+
9
+
10
+ require 'journey/parser_extras'
11
+ module Journey
12
+ class Parser < Racc::Parser
13
+ ##### State transition tables begin ###
14
+
15
+ racc_action_table = [
16
+ 17, 22, 13, 15, 14, 7, 15, 16, 8, 19,
17
+ 13, 15, 14, 7, 24, 16, 8, 19, 13, 15,
18
+ 14, 7, nil, 16, 8, 13, 15, 14, 7, nil,
19
+ 16, 8, 13, 15, 14, 7, nil, 16, 8 ]
20
+
21
+ racc_action_check = [
22
+ 1, 17, 1, 1, 1, 1, 8, 1, 1, 1,
23
+ 20, 20, 20, 20, 20, 20, 20, 20, 7, 7,
24
+ 7, 7, nil, 7, 7, 19, 19, 19, 19, nil,
25
+ 19, 19, 0, 0, 0, 0, nil, 0, 0 ]
26
+
27
+ racc_action_pointer = [
28
+ 30, 0, nil, nil, nil, nil, nil, 16, 3, nil,
29
+ nil, nil, nil, nil, nil, nil, nil, 1, nil, 23,
30
+ 8, nil, nil, nil, nil ]
31
+
32
+ racc_action_default = [
33
+ -18, -18, -2, -3, -4, -5, -6, -18, -18, -10,
34
+ -11, -12, -13, -14, -15, -16, -17, -18, -1, -18,
35
+ -18, -9, 25, -8, -7 ]
36
+
37
+ racc_goto_table = [
38
+ 18, 1, 21, nil, nil, nil, nil, nil, 20, nil,
39
+ nil, nil, nil, nil, nil, nil, nil, nil, 23, 18 ]
40
+
41
+ racc_goto_check = [
42
+ 2, 1, 7, nil, nil, nil, nil, nil, 1, nil,
43
+ nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ]
44
+
45
+ racc_goto_pointer = [
46
+ nil, 1, -1, nil, nil, nil, nil, -6, nil, nil,
47
+ nil ]
48
+
49
+ racc_goto_default = [
50
+ nil, nil, 2, 3, 4, 5, 6, 10, 9, 11,
51
+ 12 ]
52
+
53
+ racc_reduce_table = [
54
+ 0, 0, :racc_error,
55
+ 2, 11, :_reduce_1,
56
+ 1, 11, :_reduce_2,
57
+ 1, 11, :_reduce_none,
58
+ 1, 12, :_reduce_none,
59
+ 1, 12, :_reduce_none,
60
+ 1, 12, :_reduce_none,
61
+ 3, 15, :_reduce_7,
62
+ 3, 13, :_reduce_8,
63
+ 2, 16, :_reduce_9,
64
+ 1, 14, :_reduce_none,
65
+ 1, 14, :_reduce_none,
66
+ 1, 14, :_reduce_none,
67
+ 1, 14, :_reduce_none,
68
+ 1, 19, :_reduce_14,
69
+ 1, 18, :_reduce_15,
70
+ 1, 17, :_reduce_16,
71
+ 1, 20, :_reduce_17 ]
72
+
73
+ racc_reduce_n = 18
74
+
75
+ racc_shift_n = 25
76
+
77
+ racc_token_table = {
78
+ false => 0,
79
+ :error => 1,
80
+ :SLASH => 2,
81
+ :LITERAL => 3,
82
+ :SYMBOL => 4,
83
+ :LPAREN => 5,
84
+ :RPAREN => 6,
85
+ :DOT => 7,
86
+ :STAR => 8,
87
+ :OR => 9 }
88
+
89
+ racc_nt_base = 10
90
+
91
+ racc_use_result_var = true
92
+
93
+ Racc_arg = [
94
+ racc_action_table,
95
+ racc_action_check,
96
+ racc_action_default,
97
+ racc_action_pointer,
98
+ racc_goto_table,
99
+ racc_goto_check,
100
+ racc_goto_default,
101
+ racc_goto_pointer,
102
+ racc_nt_base,
103
+ racc_reduce_table,
104
+ racc_token_table,
105
+ racc_shift_n,
106
+ racc_reduce_n,
107
+ racc_use_result_var ]
108
+
109
+ Racc_token_to_s_table = [
110
+ "$end",
111
+ "error",
112
+ "SLASH",
113
+ "LITERAL",
114
+ "SYMBOL",
115
+ "LPAREN",
116
+ "RPAREN",
117
+ "DOT",
118
+ "STAR",
119
+ "OR",
120
+ "$start",
121
+ "expressions",
122
+ "expression",
123
+ "or",
124
+ "terminal",
125
+ "group",
126
+ "star",
127
+ "literal",
128
+ "symbol",
129
+ "slash",
130
+ "dot" ]
131
+
132
+ Racc_debug_parser = false
133
+
134
+ ##### State transition tables end #####
135
+
136
+ # reduce 0 omitted
137
+
138
+ def _reduce_1(val, _values, result)
139
+ result = Cat.new(val.first, val.last)
140
+ result
141
+ end
142
+
143
+ def _reduce_2(val, _values, result)
144
+ result = val.first
145
+ result
146
+ end
147
+
148
+ # reduce 3 omitted
149
+
150
+ # reduce 4 omitted
151
+
152
+ # reduce 5 omitted
153
+
154
+ # reduce 6 omitted
155
+
156
+ def _reduce_7(val, _values, result)
157
+ result = Group.new(val[1])
158
+ result
159
+ end
160
+
161
+ def _reduce_8(val, _values, result)
162
+ result = Or.new([val.first, val.last])
163
+ result
164
+ end
165
+
166
+ def _reduce_9(val, _values, result)
167
+ result = Star.new(Symbol.new(val.last.left))
168
+ result
169
+ end
170
+
171
+ # reduce 10 omitted
172
+
173
+ # reduce 11 omitted
174
+
175
+ # reduce 12 omitted
176
+
177
+ # reduce 13 omitted
178
+
179
+ def _reduce_14(val, _values, result)
180
+ result = Slash.new('/')
181
+ result
182
+ end
183
+
184
+ def _reduce_15(val, _values, result)
185
+ result = Symbol.new(val.first)
186
+ result
187
+ end
188
+
189
+ def _reduce_16(val, _values, result)
190
+ result = Literal.new(val.first)
191
+ result
192
+ end
193
+
194
+ def _reduce_17(val, _values, result)
195
+ result = Dot.new(val.first)
196
+ result
197
+ end
198
+
199
+ def _reduce_none(val, _values, result)
200
+ val[0]
201
+ end
202
+
203
+ end # class Parser
204
+ end # module Journey
@@ -0,0 +1,47 @@
1
+ class Journey::Parser
2
+
3
+ token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
4
+
5
+ rule
6
+ expressions
7
+ : expressions expression { result = Cat.new(val.first, val.last) }
8
+ | expression { result = val.first }
9
+ | or
10
+ ;
11
+ expression
12
+ : terminal
13
+ | group
14
+ | star
15
+ ;
16
+ group
17
+ : LPAREN expressions RPAREN { result = Group.new(val[1]) }
18
+ ;
19
+ or
20
+ : expressions OR expression { result = Or.new([val.first, val.last]) }
21
+ ;
22
+ star
23
+ : STAR literal { result = Star.new(Symbol.new(val.last.left)) }
24
+ ;
25
+ terminal
26
+ : symbol
27
+ | literal
28
+ | slash
29
+ | dot
30
+ ;
31
+ slash
32
+ : SLASH { result = Slash.new('/') }
33
+ ;
34
+ symbol
35
+ : SYMBOL { result = Symbol.new(val.first) }
36
+ ;
37
+ literal
38
+ : LITERAL { result = Literal.new(val.first) }
39
+ dot
40
+ : DOT { result = Dot.new(val.first) }
41
+ ;
42
+
43
+ end
44
+
45
+ ---- header
46
+
47
+ require 'journey/parser_extras'
@@ -0,0 +1,21 @@
1
+ require 'journey/scanner'
2
+ require 'journey/nodes/node'
3
+
4
+ module Journey
5
+ class Parser < Racc::Parser
6
+ include Journey::Nodes
7
+
8
+ def initialize
9
+ @scanner = Scanner.new
10
+ end
11
+
12
+ def parse string
13
+ @scanner.scan_setup string
14
+ do_parse
15
+ end
16
+
17
+ def next_token
18
+ @scanner.next_token
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,190 @@
1
+ module Journey
2
+ module Path
3
+ class Pattern
4
+ attr_reader :spec, :requirements, :anchored
5
+
6
+ def initialize strexp
7
+ parser = Journey::Parser.new
8
+
9
+ @anchored = true
10
+
11
+ case strexp
12
+ when String
13
+ @spec = parser.parse strexp
14
+ @requirements = {}
15
+ @separators = "/.?"
16
+ when Router::Strexp
17
+ @spec = parser.parse strexp.path
18
+ @requirements = strexp.requirements
19
+ @separators = strexp.separators.join
20
+ @anchored = strexp.anchor
21
+ else
22
+ raise "wtf bro: #{strexp}"
23
+ end
24
+
25
+ @names = nil
26
+ @optional_names = nil
27
+ @required_names = nil
28
+ @re = nil
29
+ end
30
+
31
+ def ast
32
+ @spec.grep(Nodes::Symbol).each do |node|
33
+ re = @requirements[node.to_sym]
34
+ node.regexp = re if re
35
+ end
36
+
37
+ @spec.grep(Nodes::Star).each do |node|
38
+ node = node.left
39
+ node.regexp = @requirements[node.to_sym] || /(.+)/
40
+ end
41
+
42
+ @spec
43
+ end
44
+
45
+ def names
46
+ @names ||= spec.grep(Nodes::Symbol).map { |n| n.name }
47
+ end
48
+
49
+ def required_names
50
+ @required_names ||= names - optional_names
51
+ end
52
+
53
+ def optional_names
54
+ @optional_names ||= spec.grep(Nodes::Group).map { |group|
55
+ group.grep(Nodes::Symbol)
56
+ }.flatten.map { |n| n.name }.uniq
57
+ end
58
+
59
+ class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
60
+ attr_reader :offsets
61
+
62
+ def initialize matchers
63
+ @matchers = matchers
64
+ @capture_count = [0]
65
+ end
66
+
67
+ def visit node
68
+ super
69
+ @capture_count
70
+ end
71
+
72
+ def visit_SYMBOL node
73
+ node = node.to_sym
74
+
75
+ if @matchers.key? node
76
+ re = /#{@matchers[node]}|/
77
+ @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0))
78
+ else
79
+ @capture_count << (@capture_count.last || 0)
80
+ end
81
+ end
82
+ end
83
+
84
+ class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
85
+ def initialize separator, matchers
86
+ @separator = separator
87
+ @matchers = matchers
88
+ @separator_re = "([^#{separator}]+)"
89
+ super()
90
+ end
91
+
92
+ def accept node
93
+ %r{\A#{visit node}\Z}
94
+ end
95
+
96
+ def visit_CAT node
97
+ [visit(node.left), visit(node.right)].join
98
+ end
99
+
100
+ def visit_SYMBOL node
101
+ node = node.to_sym
102
+
103
+ return @separator_re unless @matchers.key? node
104
+
105
+ re = @matchers[node]
106
+ # FIXME: is the question mark needed?
107
+ "(#{re}?)"
108
+ end
109
+
110
+ def visit_GROUP node
111
+ "(?:#{visit node.left})?"
112
+ end
113
+
114
+ def visit_LITERAL node
115
+ Regexp.escape node.left
116
+ end
117
+ alias :visit_DOT :visit_LITERAL
118
+
119
+ def visit_SLASH node
120
+ node.left
121
+ end
122
+
123
+ def visit_STAR node
124
+ "(.+)"
125
+ end
126
+ end
127
+
128
+ class UnanchoredRegexp < AnchoredRegexp # :nodoc:
129
+ def accept node
130
+ %r{\A#{visit node}}
131
+ end
132
+ end
133
+
134
+ class MatchData
135
+ attr_reader :names
136
+
137
+ def initialize names, offsets, match
138
+ @names = names
139
+ @offsets = offsets
140
+ @match = match
141
+ end
142
+
143
+ def captures
144
+ (length - 1).times.map { |i| self[i + 1] }
145
+ end
146
+
147
+ def [] x
148
+ idx = @offsets[x - 1] + x
149
+ @match[idx]
150
+ end
151
+
152
+ def length
153
+ @offsets.length
154
+ end
155
+
156
+ def post_match
157
+ @match.post_match
158
+ end
159
+
160
+ def to_s
161
+ @match.to_s
162
+ end
163
+ end
164
+
165
+ def match other
166
+ return unless match = to_regexp.match(other)
167
+ MatchData.new names, offsets, match
168
+ end
169
+ alias :=~ :match
170
+
171
+ def source
172
+ to_regexp.source
173
+ end
174
+
175
+ private
176
+ def to_regexp
177
+ @re ||= regexp_visitor.new(@separators, @requirements).accept spec
178
+ end
179
+
180
+ def regexp_visitor
181
+ @anchored ? AnchoredRegexp : UnanchoredRegexp
182
+ end
183
+
184
+ def offsets
185
+ viz = RegexpOffsets.new @requirements
186
+ viz.accept spec
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,92 @@
1
+ module Journey
2
+ class Route
3
+ attr_reader :app, :path, :verb, :defaults, :ip, :name
4
+
5
+ attr_reader :constraints
6
+ alias :conditions :constraints
7
+
8
+ ##
9
+ # +path+ is a path constraint.
10
+ # +constraints+ is a hash of constraints to be applied to this route.
11
+ def initialize name, app, path, constraints, defaults = {}
12
+ constraints = constraints.dup
13
+ @name = name
14
+ @app = app
15
+ @path = path
16
+ @verb = constraints[:request_method] || //
17
+ @ip = constraints.delete(:ip) || //
18
+
19
+ @constraints = constraints
20
+ @constraints.keep_if { |_,v| Regexp === v || String === v }
21
+ @defaults = defaults
22
+ @required_defaults = nil
23
+ @required_parts = nil
24
+ @parts = nil
25
+ @decorated_ast = nil
26
+ end
27
+
28
+ def ast
29
+ return @decorated_ast if @decorated_ast
30
+
31
+ @decorated_ast = path.ast
32
+ @decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self }
33
+ @decorated_ast
34
+ end
35
+
36
+ def requirements # :nodoc:
37
+ # needed for rails `rake routes`
38
+ path.requirements.merge(@defaults).delete_if { |_,v|
39
+ /.+?/ == v
40
+ }
41
+ end
42
+
43
+ def segments
44
+ @path.names
45
+ end
46
+
47
+ def required_keys
48
+ path.required_names.map { |x| x.to_sym } + required_defaults.keys
49
+ end
50
+
51
+ def score constraints
52
+ required_keys = path.required_names
53
+ supplied_keys = constraints.map { |k,v| v && k.to_s }.compact
54
+
55
+ return -1 unless (required_keys - supplied_keys).empty?
56
+
57
+ score = (supplied_keys & path.names).length
58
+ score + (required_defaults.length * 2)
59
+ end
60
+
61
+ def parts
62
+ @parts ||= segments.map { |n| n.to_sym }
63
+ end
64
+ alias :segment_keys :parts
65
+
66
+ def format path_options
67
+ (defaults.keys - required_parts).each do |key|
68
+ path_options.delete key if defaults[key].to_s == path_options[key].to_s
69
+ end
70
+
71
+ formatter = Visitors::Formatter.new(path_options)
72
+
73
+ formatted_path = formatter.accept(path.spec)
74
+ formatted_path.gsub(/\/\x00/, '')
75
+ end
76
+
77
+ def optional_parts
78
+ path.optional_names.map { |n| n.to_sym }
79
+ end
80
+
81
+ def required_parts
82
+ @required_parts ||= path.required_names.map { |n| n.to_sym }
83
+ end
84
+
85
+ def required_defaults
86
+ @required_defaults ||= begin
87
+ matches = parts
88
+ @defaults.dup.delete_if { |k,_| matches.include? k }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,22 @@
1
+ module Journey
2
+ class Router
3
+ class Strexp
4
+ class << self
5
+ alias :compile :new
6
+ end
7
+
8
+ attr_reader :path, :requirements, :separators, :anchor
9
+
10
+ def initialize path, requirements, separators, anchor = true
11
+ @path = path
12
+ @requirements = requirements
13
+ @separators = separators
14
+ @anchor = anchor
15
+ end
16
+
17
+ def names
18
+ @path.scan(/:\w+/).map { |s| s.tr(':', '') }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ require 'uri'
2
+
3
+ module Journey
4
+ class Router
5
+ class Utils
6
+ # Normalizes URI path.
7
+ #
8
+ # Strips off trailing slash and ensures there is a leading slash.
9
+ #
10
+ # normalize_path("/foo") # => "/foo"
11
+ # normalize_path("/foo/") # => "/foo"
12
+ # normalize_path("foo") # => "/foo"
13
+ # normalize_path("") # => "/"
14
+ def self.normalize_path(path)
15
+ path = "/#{path}"
16
+ path.squeeze!('/')
17
+ path.sub!(%r{/+\Z}, '')
18
+ path = '/' if path == ''
19
+ path
20
+ end
21
+
22
+ # URI path and fragment escaping
23
+ # http://tools.ietf.org/html/rfc3986
24
+ module UriEscape
25
+ # Symbol captures can generate multiple path segments, so include /.
26
+ reserved_segment = '/'
27
+ reserved_fragment = '/?'
28
+ reserved_pchar = ':@&=+$,;%'
29
+
30
+ safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}"
31
+ safe_segment = "#{safe_pchar}#{reserved_segment}"
32
+ safe_fragment = "#{safe_pchar}#{reserved_fragment}"
33
+ if RUBY_VERSION >= '1.9'
34
+ UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze
35
+ UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze
36
+ else
37
+ UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false, 'N').freeze
38
+ UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false, 'N').freeze
39
+ end
40
+ end
41
+
42
+ Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
43
+
44
+ def self.escape_path(path)
45
+ Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT)
46
+ end
47
+
48
+ def self.escape_fragment(fragment)
49
+ Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT)
50
+ end
51
+
52
+ def self.unescape_uri(uri)
53
+ Parser.unescape(uri)
54
+ end
55
+ end
56
+ end
57
+ end