Skip to content

Commit

Permalink
Merge branch 'mapper'
Browse files Browse the repository at this point in the history
* mapper: (34 commits)
  no more is_a checks on instantiation
  Path::Pattern is instantiated internally, so make the contructor require a strexp object
  Strexp#names is only used in a test, so rm
  pass the parsed path from mapper to the Strexp
  add an alternate constructor to Strexp that takes a string
  ask the strexp for the ast
  remove dead code
  disconnect path from the instance
  reuse the ast we already made
  use a parser to extract the group parts from the path
  pass the parsed parameters through the methods so we don't reparse or require caching code
  "controllers" should be a valid path name
  controllers with slash names are also not supported, so we can reuse the message
  only validate controllers
  golf down a bit
  only error handling between controller and action is the same
  add a test for controllers without colons
  move nil check to a method that yields to a block if the value is not nil
  translate action / controller to the desired object
  only one nil check on the action variable
  ...
  • Loading branch information
tenderlove committed May 29, 2014
2 parents aecf76f + da2cf93 commit c767fbf
Show file tree
Hide file tree
Showing 12 changed files with 220 additions and 210 deletions.
4 changes: 4 additions & 0 deletions actionpack/lib/action_dispatch/journey/nodes/node.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ def type; :GROUP; end


class Star < Unary # :nodoc: class Star < Unary # :nodoc:
def type; :STAR; end def type; :STAR; end

def name
left.name.tr '*:', ''
end
end end


class Binary < Node # :nodoc: class Binary < Node # :nodoc:
Expand Down
27 changes: 10 additions & 17 deletions actionpack/lib/action_dispatch/journey/path/pattern.rb
Original file line number Original file line Diff line number Diff line change
@@ -1,27 +1,20 @@
require 'action_dispatch/journey/router/strexp'

module ActionDispatch module ActionDispatch
module Journey # :nodoc: module Journey # :nodoc:
module Path # :nodoc: module Path # :nodoc:
class Pattern # :nodoc: class Pattern # :nodoc:
attr_reader :spec, :requirements, :anchored attr_reader :spec, :requirements, :anchored


def self.from_string string
new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
end

def initialize(strexp) def initialize(strexp)
parser = Journey::Parser.new @spec = strexp.ast

@requirements = strexp.requirements
@anchored = true @separators = strexp.separators.join

@anchored = strexp.anchor
case strexp
when String
@spec = parser.parse(strexp)
@requirements = {}
@separators = "/.?"
when Router::Strexp
@spec = parser.parse(strexp.path)
@requirements = strexp.requirements
@separators = strexp.separators.join
@anchored = strexp.anchor
else
raise ArgumentError, "Bad expression: #{strexp}"
end


@names = nil @names = nil
@optional_names = nil @optional_names = nil
Expand Down
15 changes: 9 additions & 6 deletions actionpack/lib/action_dispatch/journey/router/strexp.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -6,18 +6,21 @@ class << self
alias :compile :new alias :compile :new
end end


attr_reader :path, :requirements, :separators, :anchor attr_reader :path, :requirements, :separators, :anchor, :ast


def initialize(path, requirements, separators, anchor = true) def self.build(path, requirements, separators, anchor = true)
parser = Journey::Parser.new
ast = parser.parse path
new ast, path, requirements, separators, anchor
end

def initialize(ast, path, requirements, separators, anchor = true)
@ast = ast
@path = path @path = path
@requirements = requirements @requirements = requirements
@separators = separators @separators = separators
@anchor = anchor @anchor = anchor
end end

def names
@path.scan(/:\w+/).map { |s| s.tr(':', '') }
end
end end
end end
end end
Expand Down
165 changes: 86 additions & 79 deletions actionpack/lib/action_dispatch/routing/mapper.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -62,24 +62,25 @@ def constraint_args(constraint, request)
class Mapping #:nodoc: class Mapping #:nodoc:
IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format] IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix, :format]
ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z} ANCHOR_CHARACTERS_REGEX = %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
WILDCARD_PATH = %r{\*([^/\)]+)\)?$}


attr_reader :scope, :path, :options, :requirements, :conditions, :defaults attr_reader :scope, :options, :requirements, :conditions, :defaults
attr_reader :to, :default_controller, :default_action attr_reader :to, :default_controller, :default_action


def initialize(set, scope, path, options) def initialize(set, scope, path, options)
@set, @scope, @path = set, scope, path @set, @scope = set, scope
@requirements, @conditions, @defaults = {}, {}, {} @requirements, @conditions, @defaults = {}, {}, {}


options = scope[:options].merge(options) if scope[:options] options = scope[:options].merge(options) if scope[:options]
@to = options[:to] @to = options[:to]
@default_controller = options[:controller] || scope[:controller] @default_controller = options[:controller] || scope[:controller]
@default_action = options[:action] || scope[:action] @default_action = options[:action] || scope[:action]


@options = normalize_options!(options) path = normalize_path! path, options[:format]
normalize_path! ast = path_ast path
normalize_requirements! path_params = path_params ast
normalize_conditions! @options = normalize_options!(options, path_params, ast)
normalize_requirements!(path_params)
normalize_conditions!(path_params, path, ast)
normalize_defaults! normalize_defaults!
end end


Expand All @@ -89,35 +90,33 @@ def to_route


private private


def normalize_path! def normalize_path!(path, format)
raise ArgumentError, "path is required" if @path.blank? raise ArgumentError, "path is required" if path.blank?
@path = Mapper.normalize_path(@path) path = Mapper.normalize_path(path)


if required_format? if format == true
@path = "#{@path}.:format" "#{path}.:format"
elsif optional_format? elsif optional_format?(path, format)
@path = "#{@path}(.:format)" "#{path}(.:format)"
else
path
end end
end end


def required_format? def optional_format?(path, format)
options[:format] == true format != false && !path.include?(':format') && !path.end_with?('/')
end

def optional_format?
options[:format] != false && !path.include?(':format') && !path.end_with?('/')
end end


def normalize_options!(options) def normalize_options!(options, path_params, path_ast)
path_without_format = path.sub(/\(\.:format\)$/, '')

# Add a constraint for wildcard route to make it non-greedy and match the # Add a constraint for wildcard route to make it non-greedy and match the
# optional format part of the route by default # optional format part of the route by default
if path_without_format.match(WILDCARD_PATH) && options[:format] != false if options[:format] != false
options[$1.to_sym] ||= /.+?/ path_ast.grep(Journey::Nodes::Star) do |node|
options[node.name.to_sym] ||= /.+?/
end
end end


if path_without_format.match(':controller') if path_params.include?(:controller)
raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module] raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]


# Add a default constraint for :controller path segments that matches namespaced # Add a default constraint for :controller path segments that matches namespaced
Expand All @@ -127,12 +126,16 @@ def normalize_options!(options)
options[:controller] ||= /.+?/ options[:controller] ||= /.+?/
end end


options.merge!(default_controller_and_action) if to.respond_to? :call
options
else
options.merge!(default_controller_and_action(path_params))
end
end end


def normalize_requirements! def normalize_requirements!(path_params)
constraints.each do |key, requirement| constraints.each do |key, requirement|
next unless segment_keys.include?(key) || key == :controller next unless path_params.include?(key) || key == :controller
verify_regexp_requirement(requirement) if requirement.is_a?(Regexp) verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
@requirements[key] = requirement @requirements[key] = requirement
end end
Expand Down Expand Up @@ -189,18 +192,19 @@ def verify_callable_constraint(callable_constraint)
end end
end end


def normalize_conditions! def normalize_conditions!(path_params, path, ast)
@conditions[:path_info] = path @conditions[:path_info] = path
@conditions[:parsed_path_info] = ast


constraints.each do |key, condition| constraints.each do |key, condition|
unless segment_keys.include?(key) || key == :controller unless path_params.include?(key) || key == :controller
@conditions[key] = condition @conditions[key] = condition
end end
end end


required_defaults = [] required_defaults = []
options.each do |key, required_default| options.each do |key, required_default|
unless segment_keys.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default unless path_params.include?(key) || IGNORE_OPTIONS.include?(key) || Regexp === required_default
required_defaults << key required_defaults << key
end end
end end
Expand Down Expand Up @@ -236,55 +240,61 @@ def app
end end
end end


def default_controller_and_action def default_controller_and_action(path_params)
if to.respond_to?(:call) controller, action = get_controller_and_action(default_controller,
{ } default_action,
else to,
if to.is_a?(String) @scope[:module]
controller, action = to.split('#') )
elsif to.is_a?(Symbol)
action = to.to_s
end

controller ||= default_controller
action ||= default_action


if @scope[:module] && !controller.is_a?(Regexp) hash = check_part(:controller, controller, path_params, {}) do |part|
if controller =~ %r{\A/} translate_controller(part) {
controller = controller[1..-1] message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
else message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
controller = [@scope[:module], controller].compact.join("/").presence
end
end


if controller.is_a?(String) && controller =~ %r{\A/} raise ArgumentError, message
raise ArgumentError, "controller name should not start with a slash" }
end end


controller = controller.to_s unless controller.is_a?(Regexp) check_part(:action, action, path_params, hash) { |part|
action = action.to_s unless action.is_a?(Regexp) part.is_a?(Regexp) ? part : part.to_s
}
end


if controller.blank? && segment_keys.exclude?(:controller) def check_part(name, part, path_params, hash)
message = "Missing :controller key on routes definition, please check your routes." if part
hash[name] = yield(part)
else
unless path_params.include?(name)
message = "Missing :#{name} key on routes definition, please check your routes."
raise ArgumentError, message raise ArgumentError, message
end end
end
hash
end


if action.blank? && segment_keys.exclude?(:action) def get_controller_and_action(controller, action, to, modyoule)
message = "Missing :action key on routes definition, please check your routes." case to
raise ArgumentError, message when Symbol then action = to.to_s
end when /#/ then controller, action = to.split('#')
when String then controller = to
end


if controller.is_a?(String) && controller !~ /\A[a-z_0-9\/]*\z/ if modyoule && !controller.is_a?(Regexp)
message = "'#{controller}' is not a supported controller name. This can lead to potential routing problems." if controller =~ %r{\A/}
message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use" controller = controller[1..-1]
raise ArgumentError, message else
controller = [modyoule, controller].compact.join("/")
end end

hash = {}
hash[:controller] = controller unless controller.blank?
hash[:action] = action unless action.blank?
hash
end end
[controller, action]
end

def translate_controller(controller)
return controller if Regexp === controller
return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/

yield
end end


def blocks def blocks
Expand All @@ -307,16 +317,13 @@ def constraints
end end
end end


def segment_keys def path_params(ast)
@segment_keys ||= path_pattern.names.map{ |s| s.to_sym } ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
end

def path_pattern
Journey::Path::Pattern.new(strexp)
end end


def strexp def path_ast(path)
Journey::Router::Strexp.compile(path, requirements, SEPARATORS) parser = Journey::Parser.new
parser.parse path
end end


def dispatcher def dispatcher
Expand Down
7 changes: 5 additions & 2 deletions actionpack/lib/action_dispatch/routing/route_set.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -418,16 +418,19 @@ def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil
"http://guides.rubyonrails.org/routing.html#restricting-the-routes-created" "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
end end


path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) path = conditions.delete :path_info
ast = conditions.delete :parsed_path_info
path = build_path(path, ast, requirements, SEPARATORS, anchor)
conditions = build_conditions(conditions, path.names.map { |x| x.to_sym }) conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })


route = @set.add_route(app, path, conditions, defaults, name) route = @set.add_route(app, path, conditions, defaults, name)
named_routes[name] = route if name named_routes[name] = route if name
route route
end end


def build_path(path, requirements, separators, anchor) def build_path(path, ast, requirements, separators, anchor)
strexp = Journey::Router::Strexp.new( strexp = Journey::Router::Strexp.new(
ast,
path, path,
requirements, requirements,
SEPARATORS, SEPARATORS,
Expand Down
4 changes: 2 additions & 2 deletions actionpack/test/dispatch/mapper_test.rb
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -72,15 +72,15 @@ def test_map_wildcard_with_other_element
mapper = Mapper.new fakeset mapper = Mapper.new fakeset
mapper.get '/*path/foo/:bar', :to => 'pages#show' mapper.get '/*path/foo/:bar', :to => 'pages#show'
assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info] assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info]
assert_nil fakeset.requirements.first[:path] assert_equal(/.+?/, fakeset.requirements.first[:path])
end end


def test_map_wildcard_with_multiple_wildcard def test_map_wildcard_with_multiple_wildcard
fakeset = FakeSet.new fakeset = FakeSet.new
mapper = Mapper.new fakeset mapper = Mapper.new fakeset
mapper.get '/*foo/*bar', :to => 'pages#show' mapper.get '/*foo/*bar', :to => 'pages#show'
assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info] assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info]
assert_nil fakeset.requirements.first[:foo] assert_equal(/.+?/, fakeset.requirements.first[:foo])
assert_equal(/.+?/, fakeset.requirements.first[:bar]) assert_equal(/.+?/, fakeset.requirements.first[:bar])
end end


Expand Down
Loading

0 comments on commit c767fbf

Please sign in to comment.