module Merb
class Router
# The Behavior class is an interim route-building class that ties
# pattern-matching +conditions+ to output parameters, +params+.
#---
# @public
class Behavior
attr_reader :placeholders, :conditions, :params
attr_accessor :parent
@@parent_resource = []
class << self
# ==== Parameters
# string<String>:: The string in which to count parentheses.
# pos<Fixnum>:: The last character for counting.
#
# ==== Returns
# Fixnum::
# The number of open parentheses in string, up to and including pos.
def count_parens_up_to(string, pos)
string[0..pos].gsub(/[^\(]/, '').size
end
# ==== Parameters
# string1<String>:: The string to concatenate with.
# string2<String>:: The string to concatenate.
#
# ==== Returns
# String:: the concatenated string with regexp end caps removed.
def concat_without_endcaps(string1, string2)
return nil if !string1 and !string2
return string1 if string2.nil?
return string2 if string1.nil?
s1 = string1[-1] == ?$ ? string1[0..-2] : string1
s2 = string2[0] == ?^ ? string2[1..-1] : string2
s1 + s2
end
# ==== Parameters
# arr<Array>:: The array to convert to a code string.
#
# ==== Returns
# String::
# The arr's elements converted to string and joined with " + ", with
# any string elements surrounded by quotes.
def array_to_code(arr)
code = ''
arr.each_with_index do |part, i|
code << ' + ' if i > 0
case part
when Symbol
code << part.to_s
when String
code << %{"#{part}"}
else
raise "Don't know how to compile array part: #{part.class} [#{i}]"
end
end
code
end
end # class << self
# ==== Parameters
# conditions<Hash>::
# Conditions to be met for this behavior to take effect.
# params<Hash>::
# Hash describing the course action to take (Behavior) when the
# conditions match. The values of the +params+ keys must be Strings.
# parent<Behavior, Nil>::
# The parent of this Behavior. Defaults to nil.
def initialize(conditions = {}, params = {}, parent = nil)
# Must wait until after deducing placeholders to set @params !
@conditions, @params, @parent = conditions, {}, parent
@placeholders = {}
stringify_conditions
copy_original_conditions
deduce_placeholders
@params.merge! params
end
# Register a new route.
#
# ==== Parameters
# path<String, Regex>:: The url path to match
# params<Hash>:: The parameters the new routes maps to.
#
# ==== Returns
# Route:: The resulting Route.
#---
# @public
def add(path, params = {})
match(path).to(params)
end
# Matches a +path+ and any number of optional request methods as
# conditions of a route. Alternatively, +path+ can be a hash of
# conditions, in which case +conditions+ ignored.
#
# ==== Parameters
#
# path<String, Regexp>::
# When passing a string as +path+ you're defining a literal definition
# for your route. Using a colon, ex.: ":login", defines both a capture
# and a named param.
# When passing a regular expression you can define captures explicitly
# within the regular expression syntax.
# +path+ is optional.
# conditions<Hash>::
# Addational conditions that the request must meet in order to match.
# the keys must be methods that the Merb::Request instance will respond
# to. The value is the string or regexp that matched the returned value.
# Conditions are inherited by child routes.
# &block::
# Passes a new instance of a Behavior object into the optional block so
# that sub-matching and routes nesting may occur.
#
# ==== Returns
# Behavior::
# A new instance of Behavior with the specified path and conditions.
#
# +Tip+: When nesting always make sure the most inner sub-match registers
# a Route and doesn't just returns new Behaviors.
#
# ==== Examples
#
# # registers /foo/bar to controller => "foo", :action => "bar"
# # and /foo/baz to controller => "foo", :action => "baz"
# r.match "/foo" do |f|
# f.params[:controller] = 'foo'
# f.match("/bar").to(:action => "bar")
# f.match("/baz").to(:action => "caz")
# end
#
# # match also takes regular expressions
# r.match(%r[/account/([a-z]{4,6})]).to(:controller => "account",
# :action => "show", :id => "[1]")
#---
# @public
def match(path = '', conditions = {}, &block)
if path.is_a? Hash
conditions = path
else
conditions[:path] = path
end
match_without_path(conditions, &block)
end
# Generates a new child behavior without the path if the path matches
# an empty string. Yields the new behavior to a block.
#
# ==== Parameters
# conditions<Hash>:: Optional conditions to pass to the new route.
#
# ==== Block parameters
# new_behavior<Behavior>:: The child behavior.
#
# ==== Returns
# Behavior:: The new behavior.
def match_without_path(conditions = {})
new_behavior = self.class.new(conditions, {}, self)
conditions.delete :path if ['', '^$'].include?(conditions[:path])
yield new_behavior if block_given?
new_behavior
end
# ==== Parameters
# params<Hash>:: Optional additional parameters for generating the route.
# &conditional_block:: A conditional block to be passed to Route.new.
#
# ==== Returns
# Route:: A new route based on this behavior.
def to_route(params = {}, &conditional_block)
@params.merge! params
Route.new compiled_conditions, compiled_params, self, &conditional_block
end
# Combines common case of match being used with
# to({}).
#
# ==== Returns
# <Route>:: route that uses params from named path segments.
#
# ==== Examples
# r.match!("/api/:token/:controller/:action/:id")
#
# is the same thing as
#
# r.match!("/api/:token/:controller/:action/:id").to({})
def match!(path = '', conditions = {}, &block)
self.match(path, conditions, &block).to({})
end
# Creates a Route from one or more Behavior objects, unless a +block+ is
# passed in.
#
# ==== Parameters
# params<Hash>:: The parameters the route maps to.
# &block::
# Optional block. A new Behavior object is yielded and further #to
# operations may be called in the block.
#
# ==== Block parameters
# new_behavior<Behavior>:: The child behavior.
#
# ==== Returns
# Route:: It registers a new route and returns it.
#
# ==== Examples
# r.match('/:controller/:id).to(:action => 'show')
#
# r.to :controller => 'simple' do |s|
# s.match('/test').to(:action => 'index')
# s.match('/other').to(:action => 'other')
# end
#---
# @public
def to(params = {}, &block)
if block_given?
new_behavior = self.class.new({}, params, self)
yield new_behavior if block_given?
new_behavior
else
to_route(params).register
end
end
# Takes a block and stores it for deferred conditional routes. The block
# takes the +request+ object and the +params+ hash as parameters.
#
# ==== Parameters
# params<Hash>:: Parameters and conditions associated with this behavior.
# &conditional_block::
# A block with the conditions to be met for the behavior to take
# effect.
#
# ==== Returns
# Route :: The default route.
#
# ==== Examples
# r.defer_to do |request, params|
# params.merge :controller => 'here',
# :action => 'there' if request.xhr?
# end
#---
# @public
def defer_to(params = {}, &conditional_block)
Router.routes << (route = to_route(params, &conditional_block))
route
end
# Creates the most common routes /:controller/:action/:id.format when
# called with no arguments.
# You can pass a hash or a block to add parameters or override the default
# behavior.
#
# ==== Parameters
# params<Hash>::
# This optional hash can be used to augment the default settings
# &block::
# When passing a block a new behavior is yielded and more refinement is
# possible.
#
# ==== Returns
# Route:: the default route
#
# ==== Examples
#
# # Passing an extra parameter "mode" to all matches
# r.default_routes :mode => "default"
#
# # specifying exceptions within a block
# r.default_routes do |nr|
# nr.defer_to do |request, params|
# nr.match(:protocol => "http://").to(:controller => "login",
# :action => "new") if request.env["REQUEST_URI"] =~ /\/private\//
# end
# end
#---
# @public
def default_routes(params = {}, &block)
match(%r{/:controller(/:action(/:id)?)?(\.:format)?}).to(params, &block)
end
# Creates a namespace for a route. This way you can have logical
# separation to your routes.
#
# ==== Parameters
# name_or_path<String, Symbol>:: The name or path of the namespace.
# options<Hash>:: Optional hash, set :path if you want to override what appears on the url
# &block::
# A new Behavior instance is yielded in the block for nested resources.
#
# ==== Block parameters
# r<Behavior>:: The namespace behavior object.
#
# ==== Examples
# r.namespace :admin do |admin|
# admin.resources :accounts
# admin.resource :email
# end
#
# # /super_admin/accounts
# r.namespace(:admin, :path=>"super_admin") do |admin|
# admin.resources :accounts
# end
#---
# @public
def namespace(name_or_path, options={}, &block)
path = options[:path] || name_or_path.to_s
(path.empty? ? self : match("/#{path}")).to(:namespace => name_or_path.to_s) do |r|
yield r
end
end
# Behavior#+resources+ is a route helper for defining a collection of
# RESTful resources. It yields to a block for child routes.
#
# ==== Parameters
# name<String, Symbol>:: The name of the resources
# options<Hash>::
# Ovverides and parameters to be associated with the route
#
# ==== Options (options)
# :namespace<~to_s>: The namespace for this route.
# :name_prefix<~to_s>:
# A prefix for the named routes. If a namespace is passed and there
# isn't a name prefix, the namespace will become the prefix.
# :controller<~to_s>: The controller for this route
# :collection<~to_s>: Special settings for the collections routes
# :member<Hash>:
# Special settings and resources related to a specific member of this
# resource.
# :keys<Array>:
# A list of the keys to be used instead of :id with the resource in the order of the url.
#
# ==== Block parameters
# next_level<Behavior>:: The child behavior.
#
# ==== Returns
# Array::
# Routes which will define the specified RESTful collection of resources
#
# ==== Examples
#
# r.resources :posts # will result in the typical RESTful CRUD
# # GET /posts/?(\.:format)? :action => "index"
# # GET /posts/index(\.:format)? :action => "index"
# # GET /posts/new :action => "new"
# # POST /posts/?(\.:format)?, :action => "create"
# # GET /posts/:id(\.:format)? :action => "show"
# # GET /posts/:id[;/]edit :action => "edit"
# # PUT /posts/:id(\.:format)? :action => "update"
# # GET /posts/:id[;/]delete :action => "delete"
# # DELETE /posts/:id(\.:format)? :action => "destroy"
#
# # Nesting resources
# r.resources :posts do |posts|
# posts.resources :comments
# end
#---
# @public
def resources(name, options = {})
namespace = options[:namespace] || merged_params[:namespace]
next_level = match "/#{name}"
name_prefix = options.delete :name_prefix
matched_keys = options[:keys] ? options.delete(:keys).map{|k| ":#{k}"}.join("/") : ":id"
if name_prefix.nil? && !namespace.nil?
name_prefix = namespace_to_name_prefix namespace
end
unless @@parent_resource.empty?
parent_resource = namespace_to_name_prefix @@parent_resource.join('_')
end
options[:controller] ||= merged_params[:controller] || name.to_s
singular = name.to_s.singularize
route_plural_name = "#{name_prefix}#{parent_resource}#{name}"
route_singular_name = "#{name_prefix}#{parent_resource}#{singular}"
behaviors = []
if member = options.delete(:member)
member.each_pair do |action, methods|
behaviors << Behavior.new(
{ :path => %r{^/#{matched_keys}[/;]#{action}(\.:format)?$}, :method => /^(#{[methods].flatten * '|'})$/ },
{ :action => action.to_s }, next_level
)
next_level.match("/#{matched_keys}/#{action}").to_route.name(:"#{action}_#{route_singular_name}")
end
end
if collection = options.delete(:collection)
collection.each_pair do |action, methods|
behaviors << Behavior.new(
{ :path => %r{^[/;]#{action}(\.:format)?$}, :method => /^(#{[methods].flatten * '|'})$/ },
{ :action => action.to_s }, next_level
)
next_level.match("/#{action}").to_route.name(:"#{action}_#{route_plural_name}")
end
end
routes = many_behaviors_to(behaviors + next_level.send(:resources_behaviors, matched_keys), options)
# Add names to some routes
[['', :"#{route_plural_name}"],
["/#{matched_keys}", :"#{route_singular_name}"],
['/new', :"new_#{route_singular_name}"],
["/#{matched_keys}/edit", :"edit_#{route_singular_name}"],
["/#{matched_keys}/delete", :"delete_#{route_singular_name}"]
].each do |path,name|
next_level.match(path).to_route.name(name)
end
parent_keys = (matched_keys == ":id") ? ":#{singular}_id" : matched_keys
if block_given?
@@parent_resource.push(singular)
yield next_level.match("/#{parent_keys}")
@@parent_resource.pop
end
routes
end
# Behavior#+resource+ is a route helper for defining a singular RESTful
# resource. It yields to a block for child routes.
#
# ==== Parameters
# name<String, Symbol>:: The name of the resource.
# options<Hash>::
# Overides and parameters to be associated with the route.
#
# ==== Options (options)
# :namespace<~to_s>: The namespace for this route.
# :name_prefix<~to_s>:
# A prefix for the named routes. If a namespace is passed and there
# isn't a name prefix, the namespace will become the prefix.
# :controller<~to_s>: The controller for this route
#
# ==== Block parameters
# next_level<Behavior>:: The child behavior.
#
# ==== Returns
# Array:: Routes which define a RESTful single resource.
#
# ==== Examples
#
# r.resources :account # will result in the typical RESTful CRUD
# # GET /account/new :action => "new"
# # POST /account/?(\.:format)?, :action => "create"
# # GET /account/(\.:format)? :action => "show"
# # GET /account/[;/]edit :action => "edit"
# # PUT /account/(\.:format)? :action => "update"
# # GET /account/[;/]delete :action => "delete"
# # DELETE /account/(\.:format)? :action => "destroy"
#
# You can optionally pass :namespace and :controller to refine the routing
# or pass a block to nest resources.
#
# r.resource :account, :namespace => "admin" do |account|
# account.resources :preferences, :controller => "settings"
# end
# ---
# @public
def resource(name, options = {})
namespace = options[:namespace] || merged_params[:namespace]
next_level = match "/#{name}"
options[:controller] ||= merged_params[:controller] || name.to_s
# Do not pass :name_prefix option on to to_resource
name_prefix = options.delete :name_prefix
if name_prefix.nil? && !namespace.nil?
name_prefix = namespace_to_name_prefix namespace
end
unless @@parent_resource.empty?
parent_resource = namespace_to_name_prefix @@parent_resource.join('_')
end
routes = next_level.to_resource options
route_name = "#{name_prefix}#{name}"
next_level.match('').to_route.name(:"#{route_name}")
next_level.match('/new').to_route.name(:"new_#{route_name}")
next_level.match('/edit').to_route.name(:"edit_#{route_name}")
next_level.match('/delete').to_route.name(:"delete_#{route_name}")
if block_given?
@@parent_resource.push(route_name)
yield next_level
@@parent_resource.pop
end<