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
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,50 @@
1
+ .autotest
2
+ .gemtest
3
+ CHANGELOG.rdoc
4
+ Gemfile
5
+ Manifest.txt
6
+ README.rdoc
7
+ Rakefile
8
+ journey.gemspec
9
+ lib/journey.rb
10
+ lib/journey/backwards.rb
11
+ lib/journey/core-ext/hash.rb
12
+ lib/journey/formatter.rb
13
+ lib/journey/gtg/builder.rb
14
+ lib/journey/gtg/simulator.rb
15
+ lib/journey/gtg/transition_table.rb
16
+ lib/journey/nfa/builder.rb
17
+ lib/journey/nfa/dot.rb
18
+ lib/journey/nfa/simulator.rb
19
+ lib/journey/nfa/transition_table.rb
20
+ lib/journey/nodes/node.rb
21
+ lib/journey/parser.rb
22
+ lib/journey/parser.y
23
+ lib/journey/parser_extras.rb
24
+ lib/journey/path/pattern.rb
25
+ lib/journey/route.rb
26
+ lib/journey/router.rb
27
+ lib/journey/router/strexp.rb
28
+ lib/journey/router/utils.rb
29
+ lib/journey/routes.rb
30
+ lib/journey/scanner.rb
31
+ lib/journey/visitors.rb
32
+ lib/journey/visualizer/d3.min.js
33
+ lib/journey/visualizer/fsm.css
34
+ lib/journey/visualizer/fsm.js
35
+ lib/journey/visualizer/index.html.erb
36
+ lib/journey/visualizer/reset.css
37
+ test/gtg/test_builder.rb
38
+ test/gtg/test_transition_table.rb
39
+ test/helper.rb
40
+ test/nfa/test_simulator.rb
41
+ test/nfa/test_transition_table.rb
42
+ test/nodes/test_symbol.rb
43
+ test/path/test_pattern.rb
44
+ test/route/definition/test_parser.rb
45
+ test/route/definition/test_scanner.rb
46
+ test/router/test_strexp.rb
47
+ test/router/test_utils.rb
48
+ test/test_route.rb
49
+ test/test_router.rb
50
+ 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 '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 = "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"]
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
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,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