aboutsummaryrefslogtreecommitdiffstats
path: root/actionpack/lib/action_dispatch/routing/mapper.rb
diff options
context:
space:
mode:
Diffstat (limited to 'actionpack/lib/action_dispatch/routing/mapper.rb')
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb599
1 files changed, 307 insertions, 292 deletions
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index 400039353c..3b185c2576 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -1,221 +1,193 @@
module ActionDispatch
module Routing
class Mapper
- module Resources
- class Resource #:nodoc:
- attr_reader :plural, :singular
- attr_reader :path_prefix, :name_prefix
-
- def initialize(entities, options = {})
- entities = entities.to_s
-
- @plural = entities.pluralize
- @singular = entities.singularize
-
- @path_prefix = options[:path_prefix]
- @name_prefix = options[:name_prefix]
- end
-
- def name
- plural
- end
-
- def controller
- plural
+ class Constraints
+ def new(app, constraints = [])
+ if constraints.any?
+ super(app, constraints)
+ else
+ app
end
+ end
- def member_name
- if name_prefix
- "#{name_prefix}_#{singular}"
- else
- singular
- end
- end
+ def initialize(app, constraints = [])
+ @app, @constraints = app, constraints
+ end
- def collection_name
- if name_prefix
- "#{name_prefix}_#{plural}"
- else
- plural
- end
- end
+ def call(env)
+ req = Rack::Request.new(env)
- def new_name
- if name_prefix
- "new_#{name_prefix}_#{singular}"
- else
- "new_#{singular}"
+ @constraints.each { |constraint|
+ if constraint.respond_to?(:matches?) && !constraint.matches?(req)
+ return [417, {}, []]
+ elsif constraint.respond_to?(:call) && !constraint.call(req)
+ return [417, {}, []]
end
- end
+ }
- def edit_name
- if name_prefix
- "edit_#{name_prefix}_#{singular}"
- else
- "edit_#{singular}"
- end
- end
+ @app.call(env)
end
+ end
- class SingletonResource < Resource #:nodoc:
- def initialize(entity, options = {})
- super(entity)
- end
+ module Base
+ def initialize(set)
+ @set = set
+ end
- def name
- singular
- end
+ def root(options = {})
+ match '/', options.merge(:as => :root)
end
- def resource(*resources, &block)
- options = resources.extract_options!
+ def match(*args)
+ options = args.extract_options!
- if resources.length > 1
- raise ArgumentError if block_given?
- resources.each { |r| resource(r, options) }
- return self
- end
+ path = args.first
- name_prefix = @scope[:options][:name_prefix] if @scope[:options]
- resource = SingletonResource.new(resources.pop, :name_prefix => name_prefix)
+ conditions, defaults = {}, {}
- if @scope[:scope_level] == :resources
- parent_resource = @scope[:scope_level_options][:name]
- parent_named_prefix = @scope[:scope_level_options][:name_prefix]
- with_scope_level(:member) do
- scope(":#{parent_resource}_id", :name_prefix => parent_named_prefix) do
- resource(resource.name, options, &block)
- end
- end
- return self
- end
+ path = nil if path == ""
+ path = "#{@scope[:path]}#{path}" if @scope[:path]
+ path = Rack::Mount::Utils.normalize_path(path) if path
- controller(resource.controller) do
- namespace(resource.name) do
- with_scope_level(:resource, :name => resource.singular, :name_prefix => resource.member_name) do
- yield if block_given?
+ raise ArgumentError, "path is required" unless path
- get "", :to => :show, :as => resource.member_name
- post "", :to => :create
- put "", :to => :update
- delete "", :to => :destroy
- get "new", :to => :new, :as => resource.new_name
- get "edit", :to => :edit, :as => resource.edit_name
- end
- end
+ constraints = options[:constraints] || {}
+ unless constraints.is_a?(Hash)
+ block, constraints = constraints, {}
end
+ blocks = ((@scope[:blocks] || []) + [block]).compact
+ constraints = (@scope[:constraints] || {}).merge(constraints)
+ options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) }
- self
- end
+ conditions[:path_info] = path
+ requirements = constraints.dup
- def resources(*resources, &block)
- options = resources.extract_options!
+ path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS)
+ segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names
+ constraints.reject! { |k, v| segment_keys.include?(k.to_s) }
+ conditions.merge!(constraints)
- if resources.length > 1
- raise ArgumentError if block_given?
- resources.each { |r| resources(r, options) }
- return self
- end
-
- name_prefix = @scope[:options][:name_prefix] if @scope[:options]
- resource = Resource.new(resources.pop, :name_prefix => name_prefix)
+ requirements[:controller] ||= @set.controller_constraints
- if @scope[:scope_level] == :resources
- parent_resource = @scope[:scope_level_options][:name]
- parent_named_prefix = @scope[:scope_level_options][:name_prefix]
- with_scope_level(:member) do
- scope(":#{parent_resource}_id", :name_prefix => parent_named_prefix) do
- resources(resource.name, options, &block)
- end
- end
- return self
+ if via = options[:via]
+ via = Array(via).map { |m| m.to_s.upcase }
+ conditions[:request_method] = Regexp.union(*via)
end
- controller(resource.controller) do
- namespace(resource.name) do
- with_scope_level(:resources, :name => resource.singular, :name_prefix => resource.member_name) do
- yield if block_given?
+ defaults[:controller] ||= @scope[:controller].to_s if @scope[:controller]
- collection do
- get "", :to => :index, :as => resource.collection_name
- post "", :to => :create
- get "new", :to => :new, :as => resource.new_name
- end
+ app = initialize_app_endpoint(options, defaults)
+ validate_defaults!(app, defaults, segment_keys)
+ app = Constraints.new(app, blocks)
- member do
- get "", :to => :show, :as => resource.member_name
- put "", :to => :update
- delete "", :to => :destroy
- get "edit", :to => :edit, :as => resource.edit_name
- end
- end
- end
- end
+ @set.add_route(app, conditions, requirements, defaults, options[:as])
self
end
- def collection
- unless @scope[:scope_level] == :resources
- raise ArgumentError, "can't use collection outside resources scope"
- end
+ private
+ def initialize_app_endpoint(options, defaults)
+ app = nil
+
+ if options[:to].respond_to?(:call)
+ app = options[:to]
+ defaults.delete(:controller)
+ defaults.delete(:action)
+ elsif options[:to].is_a?(String)
+ defaults[:controller], defaults[:action] = options[:to].split('#')
+ elsif options[:to].is_a?(Symbol)
+ defaults[:action] = options[:to].to_s
+ end
- with_scope_level(:collection) do
- yield
+ app || Routing::RouteSet::Dispatcher.new(:defaults => defaults)
end
- end
- def member
- unless @scope[:scope_level] == :resources
- raise ArgumentError, "can't use member outside resources scope"
- end
+ def validate_defaults!(app, defaults, segment_keys)
+ return unless app.is_a?(Routing::RouteSet::Dispatcher)
- with_scope_level(:member) do
- scope(":id") do
- yield
+ unless defaults.include?(:controller) || segment_keys.include?("controller")
+ raise ArgumentError, "missing :controller"
+ end
+
+ unless defaults.include?(:action) || segment_keys.include?("action")
+ raise ArgumentError, "missing :action"
end
end
+ end
+
+ module HttpHelpers
+ def get(*args, &block)
+ map_method(:get, *args, &block)
end
- def match(*args)
- options = args.extract_options!
- args.push(options)
+ def post(*args, &block)
+ map_method(:post, *args, &block)
+ end
- case options.delete(:on)
- when :collection
- return collection { match(*args) }
- when :member
- return member { match(*args) }
- end
+ def put(*args, &block)
+ map_method(:put, *args, &block)
+ end
- if @scope[:scope_level] == :resources
- raise ArgumentError, "can't define route directly in resources scope"
- end
+ def delete(*args, &block)
+ map_method(:delete, *args, &block)
+ end
- super
+ def redirect(path, options = {})
+ status = options[:status] || 301
+ lambda { |env|
+ req = Rack::Request.new(env)
+ url = req.scheme + '://' + req.host + path
+ [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']]
+ }
end
private
- def with_scope_level(kind, options = {})
- old, @scope[:scope_level] = @scope[:scope_level], kind
- old_options, @scope[:scope_level_options] = @scope[:scope_level_options], options
- yield
- ensure
- @scope[:scope_level] = old
- @scope[:scope_level_options] = old_options
+ def map_method(method, *args, &block)
+ options = args.extract_options!
+ options[:via] = method
+ args.push(options)
+ match(*args, &block)
+ self
end
end
module Scoping
- def self.extended(object)
- object.instance_eval do
- @scope = {}
- end
+ def initialize(*args)
+ @scope = {}
+ super
end
def scope(*args)
options = args.extract_options!
+ case args.first
+ when String
+ options[:path] = args.first
+ when Symbol
+ options[:controller] = args.first
+ end
+
+ if path = options.delete(:path)
+ path_set = true
+ path, @scope[:path] = @scope[:path], Rack::Mount::Utils.normalize_path(@scope[:path].to_s + path.to_s)
+ else
+ path_set = false
+ end
+
+ if name_prefix = options.delete(:name_prefix)
+ name_prefix_set = true
+ name_prefix, @scope[:name_prefix] = @scope[:name_prefix], (@scope[:name_prefix] ? "#{@scope[:name_prefix]}_#{name_prefix}" : name_prefix)
+ else
+ name_prefix_set = false
+ end
+
+ if controller = options.delete(:controller)
+ controller_set = true
+ controller, @scope[:controller] = @scope[:controller], controller
+ else
+ controller_set = false
+ end
+
constraints = options.delete(:constraints) || {}
unless constraints.is_a?(Hash)
block, constraints = constraints, {}
@@ -225,24 +197,12 @@ module ActionDispatch
options, @scope[:options] = @scope[:options], (@scope[:options] || {}).merge(options)
- path_set = controller_set = false
-
- case args.first
- when String
- path_set = true
- path = args.first
- path, @scope[:path] = @scope[:path], "#{@scope[:path]}#{Rack::Mount::Utils.normalize_path(path)}"
- when Symbol
- controller_set = true
- controller = args.first
- controller, @scope[:controller] = @scope[:controller], controller
- end
-
yield
self
ensure
@scope[:path] = path if path_set
+ @scope[:name_prefix] = name_prefix if name_prefix_set
@scope[:controller] = controller if controller_set
@scope[:options] = options
@scope[:blocks] = blocks
@@ -254,7 +214,7 @@ module ActionDispatch
end
def namespace(path)
- scope(path.to_s) { yield }
+ scope("/#{path}") { yield }
end
def constraints(constraints = {})
@@ -263,172 +223,227 @@ module ActionDispatch
def match(*args)
options = args.extract_options!
+
options = (@scope[:options] || {}).merge(options)
+
+ if @scope[:name_prefix] && !options[:as].blank?
+ options[:as] = "#{@scope[:name_prefix]}_#{options[:as]}"
+ elsif @scope[:name_prefix] && options[:as] == ""
+ options[:as] = @scope[:name_prefix].to_s
+ end
+
args.push(options)
super(*args)
end
end
- module HttpHelpers
- def get(*args, &block)
- map_method(:get, *args, &block)
- end
+ module Resources
+ class Resource #:nodoc:
+ attr_reader :plural, :singular
- def post(*args, &block)
- map_method(:post, *args, &block)
- end
+ def initialize(entities, options = {})
+ entities = entities.to_s
- def put(*args, &block)
- map_method(:put, *args, &block)
- end
+ @plural = entities.pluralize
+ @singular = entities.singularize
+ end
- def delete(*args, &block)
- map_method(:delete, *args, &block)
- end
+ def name
+ plural
+ end
- def redirect(path, options = {})
- status = options[:status] || 301
- lambda { |env|
- req = Rack::Request.new(env)
- url = req.scheme + '://' + req.host + path
- [status, {'Location' => url, 'Content-Type' => 'text/html'}, ['Moved Permanently']]
- }
- end
+ def controller
+ plural
+ end
- private
- def map_method(method, *args, &block)
- options = args.extract_options!
- options[:via] = method
- args.push(options)
- match(*args, &block)
- self
+ def member_name
+ singular
end
- end
- class Constraints
- def new(app, constraints = [])
- if constraints.any?
- super(app, constraints)
- else
- app
+ def collection_name
+ plural
+ end
+
+ def id_segment
+ ":#{singular}_id"
end
end
- def initialize(app, constraints = [])
- @app, @constraints = app, constraints
+ class SingletonResource < Resource #:nodoc:
+ def initialize(entity, options = {})
+ super
+ end
+
+ def name
+ singular
+ end
end
- def call(env)
- req = Rack::Request.new(env)
+ def resource(*resources, &block)
+ options = resources.extract_options!
- @constraints.each { |constraint|
- if constraint.respond_to?(:matches?) && !constraint.matches?(req)
- return [417, {}, []]
- elsif constraint.respond_to?(:call) && !constraint.call(req)
- return [417, {}, []]
+ if resources.length > 1
+ raise ArgumentError if block_given?
+ resources.each { |r| resource(r, options) }
+ return self
+ end
+
+ resource = SingletonResource.new(resources.pop)
+
+ if @scope[:scope_level] == :resources
+ nested do
+ resource(resource.name, options, &block)
end
- }
+ return self
+ end
- @app.call(env)
- end
- end
+ scope(:path => "/#{resource.name}", :controller => resource.controller) do
+ with_scope_level(:resource, resource) do
+ yield if block_given?
- def initialize(set)
- @set = set
+ get "(.:format)", :to => :show, :as => resource.member_name
+ post "(.:format)", :to => :create
+ put "(.:format)", :to => :update
+ delete "(.:format)", :to => :destroy
+ get "/new(.:format)", :to => :new, :as => "new_#{resource.singular}"
+ get "/edit(.:format)", :to => :edit, :as => "edit_#{resource.singular}"
+ end
+ end
- extend HttpHelpers
- extend Scoping
- extend Resources
- end
+ self
+ end
- def root(options = {})
- match '/', options.merge(:as => :root)
- end
+ def resources(*resources, &block)
+ options = resources.extract_options!
- def match(*args)
- options = args.extract_options!
+ if resources.length > 1
+ raise ArgumentError if block_given?
+ resources.each { |r| resources(r, options) }
+ return self
+ end
- if args.length > 1
- args.each { |path| match(path, options) }
- return self
- end
+ resource = Resource.new(resources.pop)
- if args.first.is_a?(Symbol)
- return match(args.first.to_s, options.merge(:to => args.first.to_sym))
- end
+ if @scope[:scope_level] == :resources
+ nested do
+ resources(resource.name, options, &block)
+ end
+ return self
+ end
- path = args.first
+ scope(:path => "/#{resource.name}", :controller => resource.controller) do
+ with_scope_level(:resources, resource) do
+ yield if block_given?
- conditions, defaults = {}, {}
+ with_scope_level(:collection) do
+ get "(.:format)", :to => :index, :as => resource.collection_name
+ post "(.:format)", :to => :create
+ get "/new(.:format)", :to => :new, :as => "new_#{resource.singular}"
+ end
- path = nil if path == ""
- path = Rack::Mount::Utils.normalize_path(path) if path
- path = "#{@scope[:path]}#{path}" if @scope[:path]
+ with_scope_level(:member) do
+ scope("/:id") do
+ get "(.:format)", :to => :show, :as => resource.member_name
+ put "(.:format)", :to => :update
+ delete "(.:format)", :to => :destroy
+ get "/edit(.:format)", :to => :edit, :as => "edit_#{resource.singular}"
+ end
+ end
+ end
+ end
+
+ self
+ end
- raise ArgumentError, "path is required" unless path
+ def collection
+ unless @scope[:scope_level] == :resources
+ raise ArgumentError, "can't use collection outside resources scope"
+ end
- constraints = options[:constraints] || {}
- unless constraints.is_a?(Hash)
- block, constraints = constraints, {}
+ with_scope_level(:collection) do
+ scope(:name_prefix => parent_resource.collection_name, :as => "") do
+ yield
+ end
+ end
end
- blocks = ((@scope[:blocks] || []) + [block]).compact
- constraints = (@scope[:constraints] || {}).merge(constraints)
- options.each { |k, v| constraints[k] = v if v.is_a?(Regexp) }
- conditions[:path_info] = path
- requirements = constraints.dup
+ def member
+ unless @scope[:scope_level] == :resources
+ raise ArgumentError, "can't use member outside resources scope"
+ end
- path_regexp = Rack::Mount::Strexp.compile(path, constraints, SEPARATORS)
- segment_keys = Rack::Mount::RegexpWithNamedGroups.new(path_regexp).names
- constraints.reject! { |k, v| segment_keys.include?(k.to_s) }
- conditions.merge!(constraints)
+ with_scope_level(:member) do
+ scope("/:id", :name_prefix => parent_resource.member_name, :as => "") do
+ yield
+ end
+ end
+ end
- requirements[:controller] ||= @set.controller_constraints
+ def nested
+ unless @scope[:scope_level] == :resources
+ raise ArgumentError, "can't use nested outside resources scope"
+ end
- if via = options[:via]
- via = Array(via).map { |m| m.to_s.upcase }
- conditions[:request_method] = Regexp.union(*via)
+ with_scope_level(:nested) do
+ scope("/#{parent_resource.id_segment}", :name_prefix => parent_resource.member_name) do
+ yield
+ end
+ end
end
- defaults[:controller] ||= @scope[:controller].to_s if @scope[:controller]
+ def match(*args)
+ options = args.extract_options!
- app = initialize_app_endpoint(options, defaults)
- validate_defaults!(app, defaults, segment_keys)
- app = Constraints.new(app, blocks)
+ if args.length > 1
+ args.each { |path| match(path, options) }
+ return self
+ end
- @set.add_route(app, conditions, requirements, defaults, options[:as])
+ if args.first.is_a?(Symbol)
+ begin
+ old_name_prefix, @scope[:name_prefix] = @scope[:name_prefix], "#{args.first}_#{@scope[:name_prefix]}"
+ return match("/#{args.first}(.:format)", options.merge(:to => args.first.to_sym))
+ ensure
+ @scope[:name_prefix] = old_name_prefix
+ end
+ end
- self
- end
+ args.push(options)
- private
- def initialize_app_endpoint(options, defaults)
- app = nil
+ case options.delete(:on)
+ when :collection
+ return collection { match(*args) }
+ when :member
+ return member { match(*args) }
+ end
- if options[:to].respond_to?(:call)
- app = options[:to]
- defaults.delete(:controller)
- defaults.delete(:action)
- elsif options[:to].is_a?(String)
- defaults[:controller], defaults[:action] = options[:to].split('#')
- elsif options[:to].is_a?(Symbol)
- defaults[:action] = options[:to].to_s
+ if @scope[:scope_level] == :resources
+ raise ArgumentError, "can't define route directly in resources scope"
end
- app || Routing::RouteSet::Dispatcher.new(:defaults => defaults)
+ super
end
- def validate_defaults!(app, defaults, segment_keys)
- return unless app.is_a?(Routing::RouteSet::Dispatcher)
-
- unless defaults.include?(:controller) || segment_keys.include?("controller")
- raise ArgumentError, "missing :controller"
+ protected
+ def parent_resource
+ @scope[:scope_level_resource]
end
- unless defaults.include?(:action) || segment_keys.include?("action")
- raise ArgumentError, "missing :action"
+ private
+ def with_scope_level(kind, resource = parent_resource)
+ old, @scope[:scope_level] = @scope[:scope_level], kind
+ old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
+ yield
+ ensure
+ @scope[:scope_level] = old
+ @scope[:scope_level_resource] = old_resource
end
- end
+ end
+
+ include Base
+ include HttpHelpers
+ include Scoping
+ include Resources
end
end
end