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
data/.autotest ADDED
@@ -0,0 +1,8 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ Autotest.add_hook :initialize do |at|
6
+ at.testlib = 'minitest/autorun'
7
+ at.find_directories = ARGV unless ARGV.empty?
8
+ end
data/.gemtest ADDED
File without changes
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2011-03-08
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Gemfile ADDED
@@ -0,0 +1,11 @@
1
+ # -*- ruby -*-
2
+
3
+ # DO NOT EDIT THIS FILE. Instead, edit Rakefile, and run `rake bundler:gemfile`.
4
+
5
+ source :gemcutter
6
+
7
+
8
+ gem "minitest", "~>2.5", :group => [:development, :test]
9
+ gem "hoe", "~>2.10", :group => [:development, :test]
10
+
11
+ # vim: syntax=ruby
data/Manifest.txt ADDED
@@ -0,0 +1,49 @@
1
+ .autotest
2
+ CHANGELOG.rdoc
3
+ Gemfile
4
+ Manifest.txt
5
+ README.rdoc
6
+ Rakefile
7
+ journey.gemspec
8
+ lib/journey.rb
9
+ lib/journey/backwards.rb
10
+ lib/journey/core-ext/hash.rb
11
+ lib/journey/formatter.rb
12
+ lib/journey/gtg/builder.rb
13
+ lib/journey/gtg/simulator.rb
14
+ lib/journey/gtg/transition_table.rb
15
+ lib/journey/nfa/builder.rb
16
+ lib/journey/nfa/dot.rb
17
+ lib/journey/nfa/simulator.rb
18
+ lib/journey/nfa/transition_table.rb
19
+ lib/journey/nodes/node.rb
20
+ lib/journey/parser.rb
21
+ lib/journey/parser.y
22
+ lib/journey/parser_extras.rb
23
+ lib/journey/path/pattern.rb
24
+ lib/journey/route.rb
25
+ lib/journey/router.rb
26
+ lib/journey/router/strexp.rb
27
+ lib/journey/router/utils.rb
28
+ lib/journey/routes.rb
29
+ lib/journey/scanner.rb
30
+ lib/journey/visitors.rb
31
+ lib/journey/visualizer/d3.min.js
32
+ lib/journey/visualizer/fsm.css
33
+ lib/journey/visualizer/fsm.js
34
+ lib/journey/visualizer/index.html.erb
35
+ lib/journey/visualizer/reset.css
36
+ test/gtg/test_builder.rb
37
+ test/gtg/test_transition_table.rb
38
+ test/helper.rb
39
+ test/nfa/test_simulator.rb
40
+ test/nfa/test_transition_table.rb
41
+ test/nodes/test_symbol.rb
42
+ test/path/test_pattern.rb
43
+ test/route/definition/test_parser.rb
44
+ test/route/definition/test_scanner.rb
45
+ test/router/test_strexp.rb
46
+ test/router/test_utils.rb
47
+ test/test_route.rb
48
+ test/test_router.rb
49
+ test/test_routes.rb
data/README.rdoc ADDED
@@ -0,0 +1,48 @@
1
+ = Journey
2
+
3
+ * http://github.com/tenderlove/journey
4
+
5
+ == DESCRIPTION:
6
+
7
+ Journey is a router. It routes requests.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Designed for rails
12
+
13
+ == SYNOPSIS:
14
+
15
+ Too complex right now. :(
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * None
20
+
21
+ == INSTALL:
22
+
23
+ * gem install journey
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2011 Aaron Patterson
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ Hoe.plugins.delete :rubyforge
7
+ Hoe.plugin :minitest
8
+ Hoe.plugin :gemspec # `gem install hoe-gemspec`
9
+ Hoe.plugin :git # `gem install hoe-git`
10
+ Hoe.plugin :bundler # `gem install hoe-bundler`
11
+
12
+ Hoe.spec 'andyjeffries-journey' do
13
+ developer('Aaron Patterson', 'aaron@tenderlovemaking.com')
14
+ self.readme_file = 'README.rdoc'
15
+ self.history_file = 'CHANGELOG.rdoc'
16
+ self.extra_rdoc_files = FileList['*.rdoc']
17
+ self.extra_dev_deps += [
18
+ ["racc", ">= 1.4.6"],
19
+ ["json"],
20
+ ]
21
+ end
22
+
23
+ rule '.rb' => '.y' do |t|
24
+ sh "racc -l -o #{t.name} #{t.source}"
25
+ end
26
+
27
+ task :compile => "lib/journey/parser.rb"
28
+
29
+ Rake::Task[:test].prerequisites.unshift "lib/journey/parser.rb"
30
+
31
+ # vim: syntax=ruby
data/journey.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "andyjeffries-journey"
5
+ s.version = "1.0.0.20111022124133"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Aaron Patterson", "Andy Jeffries"]
9
+ s.date = "2011-10-22"
10
+ s.description = "Journey is a router. It routes requests."
11
+ s.email = ["aaron@tenderlovemaking.com"]
12
+ s.extra_rdoc_files = ["Manifest.txt", "CHANGELOG.rdoc", "README.rdoc"]
13
+ s.files = [".autotest", "CHANGELOG.rdoc", "Gemfile", "Manifest.txt", "README.rdoc", "Rakefile", "journey.gemspec", "lib/journey.rb", "lib/journey/backwards.rb", "lib/journey/core-ext/hash.rb", "lib/journey/formatter.rb", "lib/journey/gtg/builder.rb", "lib/journey/gtg/simulator.rb", "lib/journey/gtg/transition_table.rb", "lib/journey/nfa/builder.rb", "lib/journey/nfa/dot.rb", "lib/journey/nfa/simulator.rb", "lib/journey/nfa/transition_table.rb", "lib/journey/nodes/node.rb", "lib/journey/parser.rb", "lib/journey/parser.y", "lib/journey/parser_extras.rb", "lib/journey/path/pattern.rb", "lib/journey/route.rb", "lib/journey/router.rb", "lib/journey/router/strexp.rb", "lib/journey/router/utils.rb", "lib/journey/routes.rb", "lib/journey/scanner.rb", "lib/journey/visitors.rb", "lib/journey/visualizer/d3.min.js", "lib/journey/visualizer/fsm.css", "lib/journey/visualizer/fsm.js", "lib/journey/visualizer/index.html.erb", "lib/journey/visualizer/reset.css", "test/gtg/test_builder.rb", "test/gtg/test_transition_table.rb", "test/helper.rb", "test/nfa/test_simulator.rb", "test/nfa/test_transition_table.rb", "test/nodes/test_symbol.rb", "test/path/test_pattern.rb", "test/route/definition/test_parser.rb", "test/route/definition/test_scanner.rb", "test/router/test_strexp.rb", "test/router/test_utils.rb", "test/test_route.rb", "test/test_router.rb", "test/test_routes.rb", ".gemtest"]
14
+ s.homepage = "http://github.com/tenderlove/journey"
15
+ s.rdoc_options = ["--main", "README.rdoc"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = "journey"
18
+ s.rubygems_version = "1.8.11"
19
+ s.summary = "Journey is a router"
20
+ s.test_files = ["test/gtg/test_builder.rb", "test/gtg/test_transition_table.rb", "test/nfa/test_simulator.rb", "test/nfa/test_transition_table.rb", "test/nodes/test_symbol.rb", "test/path/test_pattern.rb", "test/route/definition/test_parser.rb", "test/route/definition/test_scanner.rb", "test/router/test_strexp.rb", "test/router/test_utils.rb", "test/test_route.rb", "test/test_router.rb", "test/test_routes.rb"]
21
+
22
+ if s.respond_to? :specification_version then
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
26
+ s.add_development_dependency(%q<minitest>, ["~> 2.6"])
27
+ s.add_development_dependency(%q<racc>, [">= 1.4.6"])
28
+ s.add_development_dependency(%q<json>, [">= 0"])
29
+ s.add_development_dependency(%q<hoe>, ["~> 2.12"])
30
+ else
31
+ s.add_dependency(%q<minitest>, ["~> 2.6"])
32
+ s.add_dependency(%q<racc>, [">= 1.4.6"])
33
+ s.add_dependency(%q<json>, [">= 0"])
34
+ s.add_dependency(%q<hoe>, ["~> 2.12"])
35
+ end
36
+ else
37
+ s.add_dependency(%q<minitest>, ["~> 2.6"])
38
+ s.add_dependency(%q<racc>, [">= 1.4.6"])
39
+ s.add_dependency(%q<json>, [">= 0"])
40
+ s.add_dependency(%q<hoe>, ["~> 2.12"])
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ Mount = Journey::Router
3
+ Mount::RouteSet = Journey::Router
4
+ Mount::RegexpWithNamedGroups = Journey::Path::Pattern
5
+ end
@@ -0,0 +1,11 @@
1
+ # :stopdoc:
2
+ if RUBY_VERSION < '1.9'
3
+ class Hash
4
+ def keep_if
5
+ each do |k,v|
6
+ delete(k) unless yield(k,v)
7
+ end
8
+ end
9
+ end
10
+ end
11
+ # :startdoc:
@@ -0,0 +1,129 @@
1
+ module Journey
2
+ ###
3
+ # The Formatter class is used for formatting URLs. For example, parameters
4
+ # passed to +url_for+ in rails will eventually call Formatter#generate
5
+ class Formatter
6
+ attr_reader :routes
7
+
8
+ def initialize routes
9
+ @routes = routes
10
+ @cache = nil
11
+ end
12
+
13
+ def generate key, name, options, recall = {}, parameterize = nil
14
+ constraints = recall.merge options
15
+
16
+ match_route(name, constraints) do |route|
17
+ data = constraints.dup
18
+
19
+ keys_to_keep = route.parts.reverse.drop_while { |part|
20
+ !options.key?(part) || (options[part] || recall[part]).nil?
21
+ } | route.required_parts
22
+
23
+ (data.keys - keys_to_keep).each do |bad_key|
24
+ data.delete bad_key
25
+ end
26
+
27
+ parameterized_parts = data.dup
28
+
29
+ if parameterize
30
+ parameterized_parts.each do |k,v|
31
+ parameterized_parts[k] = parameterize.call(k, v)
32
+ end
33
+ end
34
+
35
+ parameterized_parts.keep_if { |_,v| v }
36
+
37
+ next unless verify_required_parts!(route, parameterized_parts)
38
+
39
+ z = Hash[options.to_a - data.to_a - route.defaults.to_a]
40
+
41
+ return [route.format(parameterized_parts), z]
42
+ end
43
+
44
+ raise Router::RoutingError
45
+ end
46
+
47
+ def clear
48
+ @cache = nil
49
+ end
50
+
51
+ private
52
+ def named_routes
53
+ routes.named_routes
54
+ end
55
+
56
+ def match_route name, options
57
+ if named_routes.key? name
58
+ yield named_routes[name]
59
+ else
60
+ #routes = possibles(@cache, options.to_a)
61
+ routes = non_recursive(cache, options.to_a)
62
+
63
+ hash = routes.group_by { |_, r|
64
+ r.score options
65
+ }
66
+
67
+ hash.keys.sort.reverse_each do |score|
68
+ next if score < 0
69
+
70
+ hash[score].sort_by { |i,_| i }.each do |_,route|
71
+ yield route
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def non_recursive cache, options
78
+ routes = []
79
+ stack = [cache]
80
+
81
+ while stack.any?
82
+ c = stack.shift
83
+ routes.concat c[:___routes] if c.key? :___routes
84
+
85
+ options.each do |pair|
86
+ stack << c[pair] if c.key? pair
87
+ end
88
+ end
89
+
90
+ routes
91
+ end
92
+
93
+ def possibles cache, options, depth = 0
94
+ cache.fetch(:___routes) { [] } + options.find_all { |pair|
95
+ cache.key? pair
96
+ }.map { |pair|
97
+ possibles(cache[pair], options, depth + 1)
98
+ }.flatten(1)
99
+ end
100
+
101
+ def verify_required_parts! route, parts
102
+ tests = route.path.requirements
103
+ route.required_parts.all? { |key|
104
+ if tests.key? key
105
+ /\A#{tests[key]}\Z/ === parts[key]
106
+ else
107
+ true
108
+ end
109
+ }
110
+ end
111
+
112
+ def build_cache
113
+ kash = {}
114
+ routes.each_with_index do |route, i|
115
+ money = kash
116
+ route.required_defaults.each do |tuple|
117
+ hash = (money[tuple] ||= {})
118
+ money = hash
119
+ end
120
+ (money[:___routes] ||= []) << [i, route]
121
+ end
122
+ kash
123
+ end
124
+
125
+ def cache
126
+ @cache ||= build_cache
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,159 @@
1
+ require 'journey/gtg/transition_table'
2
+
3
+ module Journey
4
+ module GTG
5
+ class Builder
6
+ DUMMY = Nodes::Literal.new Object.new # :nodoc:
7
+
8
+ attr_reader :root, :ast, :endpoints
9
+
10
+ def initialize root
11
+ @root = root
12
+ @ast = Nodes::Cat.new root, DUMMY
13
+ @followpos = nil
14
+ end
15
+
16
+ def transition_table
17
+ dtrans = TransitionTable.new
18
+ marked = {}
19
+ state_id = Hash.new { |h,k| h[k] = h.length }
20
+
21
+ start = firstpos(root)
22
+ dstates = [start]
23
+ until dstates.empty?
24
+ s = dstates.shift
25
+ next if marked[s]
26
+ marked[s] = true # mark s
27
+
28
+ s.group_by { |state| symbol(state) }.each do |sym, ps|
29
+ u = ps.map { |l| followpos(l) }.flatten
30
+ next if u.empty?
31
+
32
+ if u.uniq == [DUMMY]
33
+ from = state_id[s]
34
+ to = state_id[Object.new]
35
+ dtrans[from, to] = sym
36
+
37
+ dtrans.add_accepting to
38
+ ps.each { |state| dtrans.add_memo to, state.memo }
39
+ else
40
+ dtrans[state_id[s], state_id[u]] = sym
41
+
42
+ if u.include? DUMMY
43
+ to = state_id[u]
44
+
45
+ accepting = ps.find_all { |l| followpos(l).include? DUMMY }
46
+
47
+ accepting.each { |accepting_state|
48
+ dtrans.add_memo to, accepting_state.memo
49
+ }
50
+
51
+ dtrans.add_accepting state_id[u]
52
+ end
53
+ end
54
+
55
+ dstates << u
56
+ end
57
+ end
58
+
59
+ dtrans
60
+ end
61
+
62
+ def nullable? node
63
+ case node
64
+ when Nodes::Group
65
+ true
66
+ when Nodes::Star
67
+ true
68
+ when Nodes::Or
69
+ node.children.any? { |c| nullable?(c) }
70
+ when Nodes::Cat
71
+ nullable?(node.left) && nullable?(node.right)
72
+ when Nodes::Terminal
73
+ !node.left
74
+ when Nodes::Unary
75
+ nullable? node.left
76
+ else
77
+ raise ArgumentError, 'unknown nullable: %s' % node.class.name
78
+ end
79
+ end
80
+
81
+ def firstpos node
82
+ case node
83
+ when Nodes::Star
84
+ firstpos(node.left)
85
+ when Nodes::Cat
86
+ if nullable? node.left
87
+ firstpos(node.left) | firstpos(node.right)
88
+ else
89
+ firstpos(node.left)
90
+ end
91
+ when Nodes::Or
92
+ node.children.map { |c| firstpos(c) }.flatten.uniq
93
+ when Nodes::Unary
94
+ firstpos(node.left)
95
+ when Nodes::Terminal
96
+ nullable?(node) ? [] : [node]
97
+ else
98
+ raise ArgumentError, 'unknown firstpos: %s' % node.class.name
99
+ end
100
+ end
101
+
102
+ def lastpos node
103
+ case node
104
+ when Nodes::Star
105
+ firstpos(node.left)
106
+ when Nodes::Or
107
+ node.children.map { |c| lastpos(c) }.flatten.uniq
108
+ when Nodes::Cat
109
+ if nullable? node.right
110
+ lastpos(node.left) | lastpos(node.right)
111
+ else
112
+ lastpos(node.right)
113
+ end
114
+ when Nodes::Terminal
115
+ nullable?(node) ? [] : [node]
116
+ when Nodes::Unary
117
+ lastpos(node.left)
118
+ else
119
+ raise ArgumentError, 'unknown lastpos: %s' % node.class.name
120
+ end
121
+ end
122
+
123
+ def followpos node
124
+ followpos_table[node]
125
+ end
126
+
127
+ private
128
+ def followpos_table
129
+ @followpos ||= build_followpos
130
+ end
131
+
132
+ def build_followpos
133
+ table = Hash.new { |h,k| h[k] = [] }
134
+ @ast.each do |n|
135
+ case n
136
+ when Nodes::Cat
137
+ lastpos(n.left).each do |i|
138
+ table[i] += firstpos(n.right)
139
+ end
140
+ when Nodes::Star
141
+ lastpos(n).each do |i|
142
+ table[i] += firstpos(n)
143
+ end
144
+ end
145
+ end
146
+ table
147
+ end
148
+
149
+ def symbol edge
150
+ case edge
151
+ when Journey::Nodes::Symbol
152
+ edge.regexp
153
+ else
154
+ edge.left
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,44 @@
1
+ require 'strscan'
2
+
3
+ module Journey
4
+ module GTG
5
+ class MatchData
6
+ attr_reader :memos
7
+
8
+ def initialize memos
9
+ @memos = memos
10
+ end
11
+ end
12
+
13
+ class Simulator
14
+ attr_reader :tt
15
+
16
+ def initialize transition_table
17
+ @tt = transition_table
18
+ end
19
+
20
+ def simulate string
21
+ input = StringScanner.new string
22
+ state = [0]
23
+ until input.eos?
24
+ sym = input.scan(/[\/\.\?]|[^\/\.\?]+/)
25
+ state = tt.move(state, sym)
26
+ end
27
+
28
+ acceptance_states = state.find_all { |s|
29
+ tt.accepting? s
30
+ }
31
+
32
+ return if acceptance_states.empty?
33
+
34
+ memos = acceptance_states.map { |x| tt.memo x }.flatten.compact
35
+
36
+ MatchData.new memos
37
+ end
38
+
39
+ alias :=~ :simulate
40
+ alias :match :simulate
41
+ end
42
+ end
43
+ end
44
+