Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
Showing
45 changed files
with
3,970 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
require 'action_dispatch/journey/router' | ||
require 'action_dispatch/journey/gtg/builder' | ||
require 'action_dispatch/journey/gtg/simulator' | ||
require 'action_dispatch/journey/nfa/builder' | ||
require 'action_dispatch/journey/nfa/simulator' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module Rack | ||
Mount = ActionDispatch::Journey::Router | ||
Mount::RouteSet = ActionDispatch::Journey::Router | ||
Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# :stopdoc: | ||
if RUBY_VERSION < '1.9' | ||
class Hash | ||
def keep_if | ||
each do |k,v| | ||
delete(k) unless yield(k,v) | ||
end | ||
end | ||
end | ||
end | ||
# :startdoc: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
module ActionDispatch | ||
module Journey | ||
### | ||
# The Formatter class is used for formatting URLs. For example, parameters | ||
# passed to +url_for+ in rails will eventually call Formatter#generate | ||
class Formatter | ||
attr_reader :routes | ||
|
||
def initialize routes | ||
@routes = routes | ||
@cache = nil | ||
end | ||
|
||
def generate type, name, options, recall = {}, parameterize = nil | ||
constraints = recall.merge options | ||
missing_keys = [] | ||
|
||
match_route(name, constraints) do |route| | ||
parameterized_parts = extract_parameterized_parts route, options, recall, parameterize | ||
next if !name && route.requirements.empty? && route.parts.empty? | ||
|
||
missing_keys = missing_keys(route, parameterized_parts) | ||
next unless missing_keys.empty? | ||
params = options.dup.delete_if do |key, _| | ||
parameterized_parts.key?(key) || route.defaults.key?(key) | ||
end | ||
|
||
return [route.format(parameterized_parts), params] | ||
end | ||
|
||
raise Router::RoutingError.new "missing required keys: #{missing_keys}" | ||
end | ||
|
||
def clear | ||
@cache = nil | ||
end | ||
|
||
private | ||
def extract_parameterized_parts route, options, recall, parameterize = nil | ||
constraints = recall.merge options | ||
data = constraints.dup | ||
|
||
keys_to_keep = route.parts.reverse.drop_while { |part| | ||
!options.key?(part) || (options[part] || recall[part]).nil? | ||
} | route.required_parts | ||
|
||
(data.keys - keys_to_keep).each do |bad_key| | ||
data.delete bad_key | ||
end | ||
|
||
parameterized_parts = data.dup | ||
|
||
if parameterize | ||
parameterized_parts.each do |k,v| | ||
parameterized_parts[k] = parameterize.call(k, v) | ||
end | ||
end | ||
|
||
parameterized_parts.keep_if { |_,v| v } | ||
parameterized_parts | ||
end | ||
|
||
def named_routes | ||
routes.named_routes | ||
end | ||
|
||
def match_route name, options | ||
if named_routes.key? name | ||
yield named_routes[name] | ||
else | ||
#routes = possibles(@cache, options.to_a) | ||
routes = non_recursive(cache, options.to_a) | ||
|
||
hash = routes.group_by { |_, r| | ||
r.score options | ||
} | ||
|
||
hash.keys.sort.reverse_each do |score| | ||
next if score < 0 | ||
|
||
hash[score].sort_by { |i,_| i }.each do |_,route| | ||
yield route | ||
end | ||
end | ||
end | ||
end | ||
|
||
def non_recursive cache, options | ||
routes = [] | ||
stack = [cache] | ||
|
||
while stack.any? | ||
c = stack.shift | ||
routes.concat c[:___routes] if c.key? :___routes | ||
|
||
options.each do |pair| | ||
stack << c[pair] if c.key? pair | ||
end | ||
end | ||
|
||
routes | ||
end | ||
|
||
# returns an array populated with missing keys if any are present | ||
def missing_keys route, parts | ||
missing_keys = [] | ||
tests = route.path.requirements | ||
route.required_parts.each { |key| | ||
if tests.key? key | ||
missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key] | ||
else | ||
missing_keys << key unless parts[key] | ||
end | ||
} | ||
missing_keys | ||
end | ||
|
||
def possibles cache, options, depth = 0 | ||
cache.fetch(:___routes) { [] } + options.find_all { |pair| | ||
cache.key? pair | ||
}.map { |pair| | ||
possibles(cache[pair], options, depth + 1) | ||
}.flatten(1) | ||
end | ||
|
||
# returns boolean, true if no missing keys are present | ||
def verify_required_parts! route, parts | ||
missing_keys(route, parts).empty? | ||
end | ||
|
||
def build_cache | ||
root = { :___routes => [] } | ||
routes.each_with_index do |route, i| | ||
leaf = route.required_defaults.inject(root) do |h, tuple| | ||
h[tuple] ||= {} | ||
end | ||
(leaf[:___routes] ||= []) << [i, route] | ||
end | ||
root | ||
end | ||
|
||
def cache | ||
@cache ||= build_cache | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
require 'action_dispatch/journey/gtg/transition_table' | ||
|
||
module ActionDispatch | ||
module Journey | ||
module GTG | ||
class Builder | ||
DUMMY = Nodes::Dummy.new # :nodoc: | ||
|
||
attr_reader :root, :ast, :endpoints | ||
|
||
def initialize root | ||
@root = root | ||
@ast = Nodes::Cat.new root, DUMMY | ||
@followpos = nil | ||
end | ||
|
||
def transition_table | ||
dtrans = TransitionTable.new | ||
marked = {} | ||
state_id = Hash.new { |h,k| h[k] = h.length } | ||
|
||
start = firstpos(root) | ||
dstates = [start] | ||
until dstates.empty? | ||
s = dstates.shift | ||
next if marked[s] | ||
marked[s] = true # mark s | ||
|
||
s.group_by { |state| symbol(state) }.each do |sym, ps| | ||
u = ps.map { |l| followpos(l) }.flatten | ||
next if u.empty? | ||
|
||
if u.uniq == [DUMMY] | ||
from = state_id[s] | ||
to = state_id[Object.new] | ||
dtrans[from, to] = sym | ||
|
||
dtrans.add_accepting to | ||
ps.each { |state| dtrans.add_memo to, state.memo } | ||
else | ||
dtrans[state_id[s], state_id[u]] = sym | ||
|
||
if u.include? DUMMY | ||
to = state_id[u] | ||
|
||
accepting = ps.find_all { |l| followpos(l).include? DUMMY } | ||
|
||
accepting.each { |accepting_state| | ||
dtrans.add_memo to, accepting_state.memo | ||
} | ||
|
||
dtrans.add_accepting state_id[u] | ||
end | ||
end | ||
|
||
dstates << u | ||
end | ||
end | ||
|
||
dtrans | ||
end | ||
|
||
def nullable? node | ||
case node | ||
when Nodes::Group | ||
true | ||
when Nodes::Star | ||
true | ||
when Nodes::Or | ||
node.children.any? { |c| nullable?(c) } | ||
when Nodes::Cat | ||
nullable?(node.left) && nullable?(node.right) | ||
when Nodes::Terminal | ||
!node.left | ||
when Nodes::Unary | ||
nullable? node.left | ||
else | ||
raise ArgumentError, 'unknown nullable: %s' % node.class.name | ||
end | ||
end | ||
|
||
def firstpos node | ||
case node | ||
when Nodes::Star | ||
firstpos(node.left) | ||
when Nodes::Cat | ||
if nullable? node.left | ||
firstpos(node.left) | firstpos(node.right) | ||
else | ||
firstpos(node.left) | ||
end | ||
when Nodes::Or | ||
node.children.map { |c| firstpos(c) }.flatten.uniq | ||
when Nodes::Unary | ||
firstpos(node.left) | ||
when Nodes::Terminal | ||
nullable?(node) ? [] : [node] | ||
else | ||
raise ArgumentError, 'unknown firstpos: %s' % node.class.name | ||
end | ||
end | ||
|
||
def lastpos node | ||
case node | ||
when Nodes::Star | ||
firstpos(node.left) | ||
when Nodes::Or | ||
node.children.map { |c| lastpos(c) }.flatten.uniq | ||
when Nodes::Cat | ||
if nullable? node.right | ||
lastpos(node.left) | lastpos(node.right) | ||
else | ||
lastpos(node.right) | ||
end | ||
when Nodes::Terminal | ||
nullable?(node) ? [] : [node] | ||
when Nodes::Unary | ||
lastpos(node.left) | ||
else | ||
raise ArgumentError, 'unknown lastpos: %s' % node.class.name | ||
end | ||
end | ||
|
||
def followpos node | ||
followpos_table[node] | ||
end | ||
|
||
private | ||
def followpos_table | ||
@followpos ||= build_followpos | ||
end | ||
|
||
def build_followpos | ||
table = Hash.new { |h,k| h[k] = [] } | ||
@ast.each do |n| | ||
case n | ||
when Nodes::Cat | ||
lastpos(n.left).each do |i| | ||
table[i] += firstpos(n.right) | ||
end | ||
when Nodes::Star | ||
lastpos(n).each do |i| | ||
table[i] += firstpos(n) | ||
end | ||
end | ||
end | ||
table | ||
end | ||
|
||
def symbol edge | ||
case edge | ||
when Journey::Nodes::Symbol | ||
edge.regexp | ||
else | ||
edge.left | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Oops, something went wrong.
81bcfba
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@tenderlove what do you think?
81bcfba
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@pixeltrix great! ❤️