Skip to content

Commit 56fee39

Browse files
committed
Integrate Journey into Action Dispatch
Move the Journey code underneath the ActionDispatch namespace so that we don't pollute the global namespace with names that may be used for models. Fixes rails/journey#49.
1 parent b225693 commit 56fee39

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+3970
-6
lines changed

Gemfile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ gem 'turbolinks'
1313
gem 'coffee-rails', github: 'rails/coffee-rails'
1414

1515
gem 'thread_safe', '~> 0.1'
16-
gem 'journey', github: 'rails/journey', branch: 'master'
1716

1817
gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders', branch: 'master'
1918

19+
# Needed for compiling the ActionDispatch::Journey parser
20+
gem 'racc', '>=1.4.6', require: false
21+
2022
# This needs to be with require false to avoid
2123
# it being automatically loaded by sprockets
2224
gem 'uglifier', require: false

actionpack/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
## Rails 4.0.0 (unreleased) ##
22

3+
* Integrate the Journey gem into Action Dispatch so that the global namespace
4+
is not polluted with names that may be used as models.
5+
6+
*Andrew White*
7+
38
* Extract support for email address obfuscation via `:encode`, `:replace_at`, and `replace_dot`
49
options from the `mail_to` helper into the `actionview-encoded_mail_to` gem.
510

actionpack/Rakefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Rake::TestTask.new(:test_action_pack) do |t|
1515

1616
# make sure we include the tests in alphabetical order as on some systems
1717
# this will not happen automatically and the tests (as a whole) will error
18-
t.test_files = Dir.glob('test/{abstract,controller,dispatch,template,assertions}/**/*_test.rb').sort
18+
t.test_files = Dir.glob('test/{abstract,controller,dispatch,template,assertions,journey}/**/*_test.rb').sort
1919

2020
t.warning = true
2121
t.verbose = true
@@ -75,3 +75,9 @@ task :lines do
7575

7676
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
7777
end
78+
79+
rule '.rb' => '.y' do |t|
80+
sh "racc -l -o #{t.name} #{t.source}"
81+
end
82+
83+
task :compile => 'lib/action_dispatch/journey/parser.rb'

actionpack/actionpack.gemspec

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ Gem::Specification.new do |s|
2323
s.add_dependency 'builder', '~> 3.1.0'
2424
s.add_dependency 'rack', '~> 1.4.1'
2525
s.add_dependency 'rack-test', '~> 0.6.1'
26-
s.add_dependency 'journey', '~> 2.0.0'
2726
s.add_dependency 'erubis', '~> 2.7.0'
2827

2928
s.add_development_dependency 'activemodel', version

actionpack/lib/action_dispatch.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class IllegalStateError < StandardError
6363
autoload :Static
6464
end
6565

66+
autoload :Journey
6667
autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
6768
autoload :Routing
6869

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
require 'action_dispatch/journey/router'
2+
require 'action_dispatch/journey/gtg/builder'
3+
require 'action_dispatch/journey/gtg/simulator'
4+
require 'action_dispatch/journey/nfa/builder'
5+
require 'action_dispatch/journey/nfa/simulator'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module Rack
2+
Mount = ActionDispatch::Journey::Router
3+
Mount::RouteSet = ActionDispatch::Journey::Router
4+
Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern
5+
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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:
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
module ActionDispatch
2+
module Journey
3+
###
4+
# The Formatter class is used for formatting URLs. For example, parameters
5+
# passed to +url_for+ in rails will eventually call Formatter#generate
6+
class Formatter
7+
attr_reader :routes
8+
9+
def initialize routes
10+
@routes = routes
11+
@cache = nil
12+
end
13+
14+
def generate type, name, options, recall = {}, parameterize = nil
15+
constraints = recall.merge options
16+
missing_keys = []
17+
18+
match_route(name, constraints) do |route|
19+
parameterized_parts = extract_parameterized_parts route, options, recall, parameterize
20+
next if !name && route.requirements.empty? && route.parts.empty?
21+
22+
missing_keys = missing_keys(route, parameterized_parts)
23+
next unless missing_keys.empty?
24+
params = options.dup.delete_if do |key, _|
25+
parameterized_parts.key?(key) || route.defaults.key?(key)
26+
end
27+
28+
return [route.format(parameterized_parts), params]
29+
end
30+
31+
raise Router::RoutingError.new "missing required keys: #{missing_keys}"
32+
end
33+
34+
def clear
35+
@cache = nil
36+
end
37+
38+
private
39+
def extract_parameterized_parts route, options, recall, parameterize = nil
40+
constraints = recall.merge options
41+
data = constraints.dup
42+
43+
keys_to_keep = route.parts.reverse.drop_while { |part|
44+
!options.key?(part) || (options[part] || recall[part]).nil?
45+
} | route.required_parts
46+
47+
(data.keys - keys_to_keep).each do |bad_key|
48+
data.delete bad_key
49+
end
50+
51+
parameterized_parts = data.dup
52+
53+
if parameterize
54+
parameterized_parts.each do |k,v|
55+
parameterized_parts[k] = parameterize.call(k, v)
56+
end
57+
end
58+
59+
parameterized_parts.keep_if { |_,v| v }
60+
parameterized_parts
61+
end
62+
63+
def named_routes
64+
routes.named_routes
65+
end
66+
67+
def match_route name, options
68+
if named_routes.key? name
69+
yield named_routes[name]
70+
else
71+
#routes = possibles(@cache, options.to_a)
72+
routes = non_recursive(cache, options.to_a)
73+
74+
hash = routes.group_by { |_, r|
75+
r.score options
76+
}
77+
78+
hash.keys.sort.reverse_each do |score|
79+
next if score < 0
80+
81+
hash[score].sort_by { |i,_| i }.each do |_,route|
82+
yield route
83+
end
84+
end
85+
end
86+
end
87+
88+
def non_recursive cache, options
89+
routes = []
90+
stack = [cache]
91+
92+
while stack.any?
93+
c = stack.shift
94+
routes.concat c[:___routes] if c.key? :___routes
95+
96+
options.each do |pair|
97+
stack << c[pair] if c.key? pair
98+
end
99+
end
100+
101+
routes
102+
end
103+
104+
# returns an array populated with missing keys if any are present
105+
def missing_keys route, parts
106+
missing_keys = []
107+
tests = route.path.requirements
108+
route.required_parts.each { |key|
109+
if tests.key? key
110+
missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
111+
else
112+
missing_keys << key unless parts[key]
113+
end
114+
}
115+
missing_keys
116+
end
117+
118+
def possibles cache, options, depth = 0
119+
cache.fetch(:___routes) { [] } + options.find_all { |pair|
120+
cache.key? pair
121+
}.map { |pair|
122+
possibles(cache[pair], options, depth + 1)
123+
}.flatten(1)
124+
end
125+
126+
# returns boolean, true if no missing keys are present
127+
def verify_required_parts! route, parts
128+
missing_keys(route, parts).empty?
129+
end
130+
131+
def build_cache
132+
root = { :___routes => [] }
133+
routes.each_with_index do |route, i|
134+
leaf = route.required_defaults.inject(root) do |h, tuple|
135+
h[tuple] ||= {}
136+
end
137+
(leaf[:___routes] ||= []) << [i, route]
138+
end
139+
root
140+
end
141+
142+
def cache
143+
@cache ||= build_cache
144+
end
145+
end
146+
end
147+
end
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
require 'action_dispatch/journey/gtg/transition_table'
2+
3+
module ActionDispatch
4+
module Journey
5+
module GTG
6+
class Builder
7+
DUMMY = Nodes::Dummy.new # :nodoc:
8+
9+
attr_reader :root, :ast, :endpoints
10+
11+
def initialize root
12+
@root = root
13+
@ast = Nodes::Cat.new root, DUMMY
14+
@followpos = nil
15+
end
16+
17+
def transition_table
18+
dtrans = TransitionTable.new
19+
marked = {}
20+
state_id = Hash.new { |h,k| h[k] = h.length }
21+
22+
start = firstpos(root)
23+
dstates = [start]
24+
until dstates.empty?
25+
s = dstates.shift
26+
next if marked[s]
27+
marked[s] = true # mark s
28+
29+
s.group_by { |state| symbol(state) }.each do |sym, ps|
30+
u = ps.map { |l| followpos(l) }.flatten
31+
next if u.empty?
32+
33+
if u.uniq == [DUMMY]
34+
from = state_id[s]
35+
to = state_id[Object.new]
36+
dtrans[from, to] = sym
37+
38+
dtrans.add_accepting to
39+
ps.each { |state| dtrans.add_memo to, state.memo }
40+
else
41+
dtrans[state_id[s], state_id[u]] = sym
42+
43+
if u.include? DUMMY
44+
to = state_id[u]
45+
46+
accepting = ps.find_all { |l| followpos(l).include? DUMMY }
47+
48+
accepting.each { |accepting_state|
49+
dtrans.add_memo to, accepting_state.memo
50+
}
51+
52+
dtrans.add_accepting state_id[u]
53+
end
54+
end
55+
56+
dstates << u
57+
end
58+
end
59+
60+
dtrans
61+
end
62+
63+
def nullable? node
64+
case node
65+
when Nodes::Group
66+
true
67+
when Nodes::Star
68+
true
69+
when Nodes::Or
70+
node.children.any? { |c| nullable?(c) }
71+
when Nodes::Cat
72+
nullable?(node.left) && nullable?(node.right)
73+
when Nodes::Terminal
74+
!node.left
75+
when Nodes::Unary
76+
nullable? node.left
77+
else
78+
raise ArgumentError, 'unknown nullable: %s' % node.class.name
79+
end
80+
end
81+
82+
def firstpos node
83+
case node
84+
when Nodes::Star
85+
firstpos(node.left)
86+
when Nodes::Cat
87+
if nullable? node.left
88+
firstpos(node.left) | firstpos(node.right)
89+
else
90+
firstpos(node.left)
91+
end
92+
when Nodes::Or
93+
node.children.map { |c| firstpos(c) }.flatten.uniq
94+
when Nodes::Unary
95+
firstpos(node.left)
96+
when Nodes::Terminal
97+
nullable?(node) ? [] : [node]
98+
else
99+
raise ArgumentError, 'unknown firstpos: %s' % node.class.name
100+
end
101+
end
102+
103+
def lastpos node
104+
case node
105+
when Nodes::Star
106+
firstpos(node.left)
107+
when Nodes::Or
108+
node.children.map { |c| lastpos(c) }.flatten.uniq
109+
when Nodes::Cat
110+
if nullable? node.right
111+
lastpos(node.left) | lastpos(node.right)
112+
else
113+
lastpos(node.right)
114+
end
115+
when Nodes::Terminal
116+
nullable?(node) ? [] : [node]
117+
when Nodes::Unary
118+
lastpos(node.left)
119+
else
120+
raise ArgumentError, 'unknown lastpos: %s' % node.class.name
121+
end
122+
end
123+
124+
def followpos node
125+
followpos_table[node]
126+
end
127+
128+
private
129+
def followpos_table
130+
@followpos ||= build_followpos
131+
end
132+
133+
def build_followpos
134+
table = Hash.new { |h,k| h[k] = [] }
135+
@ast.each do |n|
136+
case n
137+
when Nodes::Cat
138+
lastpos(n.left).each do |i|
139+
table[i] += firstpos(n.right)
140+
end
141+
when Nodes::Star
142+
lastpos(n).each do |i|
143+
table[i] += firstpos(n)
144+
end
145+
end
146+
end
147+
table
148+
end
149+
150+
def symbol edge
151+
case edge
152+
when Journey::Nodes::Symbol
153+
edge.regexp
154+
else
155+
edge.left
156+
end
157+
end
158+
end
159+
end
160+
end
161+
end

0 commit comments

Comments
 (0)