mirror of
https://github.com/github/rails.git
synced 2026-01-12 16:19:01 -05:00
Compare commits
79 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94fe0794fd | ||
|
|
39b441bde4 | ||
|
|
77934cbac7 | ||
|
|
ccf254b6cb | ||
|
|
89384aced8 | ||
|
|
b2321ce87d | ||
|
|
3766b1b377 | ||
|
|
d3f87776a3 | ||
|
|
18c7c1f753 | ||
|
|
f63b0340ff | ||
|
|
7224ee1419 | ||
|
|
0c52ae6df3 | ||
|
|
f8b7cd2df7 | ||
|
|
c73ba86136 | ||
|
|
98fa5dd465 | ||
|
|
fa41bedf6b | ||
|
|
0a8282c557 | ||
|
|
d4a4facfcc | ||
|
|
dd4146854a | ||
|
|
cedf026a14 | ||
|
|
7ac3b0fa4f | ||
|
|
31cd7ea26d | ||
|
|
df387ab385 | ||
|
|
0118959601 | ||
|
|
83448c7de5 | ||
|
|
8f99d00868 | ||
|
|
987b61bd1d | ||
|
|
f05e54a9f3 | ||
|
|
b9918117bb | ||
|
|
42f85d118d | ||
|
|
acb182d094 | ||
|
|
6e0fcb788d | ||
|
|
fed4fafa8a | ||
|
|
f699184047 | ||
|
|
55d6a9f2df | ||
|
|
e5bebc01a8 | ||
|
|
a019f07a39 | ||
|
|
d13866d75d | ||
|
|
dfa2f469a4 | ||
|
|
bf0d43bb77 | ||
|
|
72cebbcb59 | ||
|
|
379dd9071c | ||
|
|
a743f17dbd | ||
|
|
25b896611d | ||
|
|
b988837359 | ||
|
|
890aff3b9d | ||
|
|
536b85eedf | ||
|
|
cfe841a665 | ||
|
|
26946cdbc5 | ||
|
|
ceb7b06f79 | ||
|
|
a431bb5e70 | ||
|
|
31ba1174fc | ||
|
|
0749c4598a | ||
|
|
54a1b2a266 | ||
|
|
d4eac0ff04 | ||
|
|
fa6c421edb | ||
|
|
a390e0366a | ||
|
|
46e7ead222 | ||
|
|
5c84664061 | ||
|
|
96b21154d0 | ||
|
|
32295bb1e7 | ||
|
|
1744654e1b | ||
|
|
6fc589dcea | ||
|
|
6372a64940 | ||
|
|
7ffe8e65e9 | ||
|
|
0fee4d272a | ||
|
|
73e5333f3d | ||
|
|
370b93b49c | ||
|
|
b86cf6843b | ||
|
|
d22470ab98 | ||
|
|
5787193c86 | ||
|
|
b75d7ea2c6 | ||
|
|
735c4e790d | ||
|
|
0e467e376b | ||
|
|
e3dafa5669 | ||
|
|
3297a4c446 | ||
|
|
d98e3f8489 | ||
|
|
9ee6c6c082 | ||
|
|
6d4d9dd919 |
@@ -1 +1 @@
|
||||
2.3.14.github31
|
||||
2.3.14.github35
|
||||
|
||||
@@ -280,7 +280,6 @@ module ActionMailer #:nodoc:
|
||||
class Base
|
||||
include AdvAttrAccessor, PartContainer, Quoting, Utils
|
||||
if Object.const_defined?(:ActionController)
|
||||
include ActionController::UrlWriter
|
||||
include ActionController::Layout
|
||||
end
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ spec = Gem::Specification.new do |s|
|
||||
|
||||
s.add_dependency('activesupport', '= 2.3.14' + PKG_BUILD)
|
||||
s.add_dependency('erubis', '~> 2.7.0')
|
||||
s.add_dependency('rack', '~> 1.1')
|
||||
s.add_dependency('rack', '~> 1.1')
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ module ActionController
|
||||
# TODO: Review explicit to see if they will automatically be handled by
|
||||
# the initilizer if they are really needed.
|
||||
def self.load_all!
|
||||
[Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
|
||||
[Base, Request, Response, Http::Headers, UrlRewriter]
|
||||
end
|
||||
|
||||
autoload :Base, 'action_controller/base'
|
||||
@@ -57,7 +57,6 @@ module ActionController
|
||||
autoload :MiddlewareStack, 'action_controller/middleware_stack'
|
||||
autoload :MimeResponds, 'action_controller/mime_responds'
|
||||
autoload :ParamsParser, 'action_controller/params_parser'
|
||||
autoload :PolymorphicRoutes, 'action_controller/polymorphic_routes'
|
||||
autoload :RecordIdentifier, 'action_controller/record_identifier'
|
||||
autoload :Reloader, 'action_controller/reloader'
|
||||
autoload :Request, 'action_controller/request'
|
||||
@@ -78,7 +77,6 @@ module ActionController
|
||||
autoload :UploadedStringIO, 'action_controller/uploaded_file'
|
||||
autoload :UploadedTempfile, 'action_controller/uploaded_file'
|
||||
autoload :UrlRewriter, 'action_controller/url_rewriter'
|
||||
autoload :UrlWriter, 'action_controller/url_rewriter'
|
||||
autoload :Verification, 'action_controller/verification'
|
||||
|
||||
module Assertions
|
||||
@@ -99,10 +97,6 @@ module ActionController
|
||||
autoload :CookieStore, 'action_controller/session/cookie_store'
|
||||
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
|
||||
end
|
||||
|
||||
# DEPRECATE: Remove CGI support
|
||||
autoload :CgiRequest, 'action_controller/cgi_process'
|
||||
autoload :CGIHandler, 'action_controller/cgi_process'
|
||||
end
|
||||
|
||||
autoload :Mime, 'action_controller/mime_type'
|
||||
|
||||
@@ -59,19 +59,9 @@ module ActionController
|
||||
def assert_redirected_to(options = {}, message=nil)
|
||||
clean_backtrace do
|
||||
assert_response(:redirect, message)
|
||||
return true if options == @response.redirected_to
|
||||
return true if options == @response.location
|
||||
|
||||
# Support partial arguments for hash redirections
|
||||
if options.is_a?(Hash) && @response.redirected_to.is_a?(Hash)
|
||||
if options.all? {|(key, value)| @response.redirected_to[key] == value}
|
||||
callstack = caller.dup
|
||||
callstack.slice!(0, 2)
|
||||
::ActiveSupport::Deprecation.warn("Using assert_redirected_to with partial hash arguments is deprecated. Specify the full set arguments instead", callstack)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.redirected_to)
|
||||
redirected_to_after_normalisation = normalize_argument_to_redirection(@response.location)
|
||||
options_after_normalisation = normalize_argument_to_redirection(options)
|
||||
|
||||
if redirected_to_after_normalisation != options_after_normalisation
|
||||
|
||||
@@ -1,61 +1,15 @@
|
||||
require 'set'
|
||||
require 'action_controller/metal/url_for'
|
||||
require 'action_controller/metal/exceptions'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class ActionControllerError < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
class SessionRestoreError < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class RenderError < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class RoutingError < ActionControllerError #:nodoc:
|
||||
attr_reader :failures
|
||||
def initialize(message, failures=[])
|
||||
super(message)
|
||||
@failures = failures
|
||||
end
|
||||
end
|
||||
|
||||
class MethodNotAllowed < ActionControllerError #:nodoc:
|
||||
attr_reader :allowed_methods
|
||||
|
||||
def initialize(*allowed_methods)
|
||||
super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
|
||||
@allowed_methods = allowed_methods
|
||||
end
|
||||
|
||||
def allowed_methods_header
|
||||
allowed_methods.map { |method_symbol| method_symbol.to_s.upcase } * ', '
|
||||
end
|
||||
|
||||
def handle_response!(response)
|
||||
response.headers['Allow'] ||= allowed_methods_header
|
||||
end
|
||||
end
|
||||
|
||||
class NotImplemented < MethodNotAllowed #:nodoc:
|
||||
end
|
||||
|
||||
class UnknownController < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class UnknownAction < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class MissingFile < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class RenderError < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class SessionOverflowError < ActionControllerError #:nodoc:
|
||||
DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
|
||||
|
||||
def initialize(message = nil)
|
||||
super(message || DEFAULT_MESSAGE)
|
||||
end
|
||||
class SessionRestoreError < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class DoubleRenderError < ActionControllerError #:nodoc:
|
||||
@@ -74,9 +28,6 @@ module ActionController #:nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
class UnknownHttpMethod < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
# Action Controllers are the core of a web request in Rails. They are made up of one or more actions that are executed
|
||||
# on request and then either render a template or redirect to another action. An action is defined as a public method
|
||||
# on the controller, which will automatically be made accessible to the web-server through Rails Routes.
|
||||
@@ -250,6 +201,7 @@ module ActionController #:nodoc:
|
||||
DEFAULT_RENDER_STATUS_CODE = "200 OK"
|
||||
|
||||
include StatusCodes
|
||||
include UrlFor
|
||||
|
||||
cattr_reader :protected_instance_variables
|
||||
# Controller specific instance variables which will not be accessible inside views.
|
||||
@@ -541,93 +493,6 @@ module ActionController #:nodoc:
|
||||
response
|
||||
end
|
||||
|
||||
# Returns a URL that has been rewritten according to the options hash and the defined routes.
|
||||
# (For doing a complete redirect, use +redirect_to+).
|
||||
#
|
||||
# <tt>url_for</tt> is used to:
|
||||
#
|
||||
# All keys given to +url_for+ are forwarded to the Route module, save for the following:
|
||||
# * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path. For example,
|
||||
# <tt>url_for :controller => 'posts', :action => 'show', :id => 10, :anchor => 'comments'</tt>
|
||||
# will produce "/posts/show/10#comments".
|
||||
# * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>false</tt> by default).
|
||||
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
|
||||
# is currently not recommended since it breaks caching.
|
||||
# * <tt>:host</tt> - Overrides the default (current) host if provided.
|
||||
# * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
|
||||
# * <tt>:port</tt> - Optionally specify the port to connect to.
|
||||
# * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
|
||||
# * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
|
||||
# * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the +relative_url_root+
|
||||
# of the request so the path will include the web server relative installation directory.
|
||||
#
|
||||
# The URL is generated from the remaining keys in the hash. A URL contains two key parts: the <base> and a query string.
|
||||
# Routes composes a query string as the key/value pairs not included in the <base>.
|
||||
#
|
||||
# The default Routes setup supports a typical Rails path of "controller/action/id" where action and id are optional, with
|
||||
# action defaulting to 'index' when not given. Here are some typical url_for statements and their corresponding URLs:
|
||||
#
|
||||
# url_for :controller => 'posts', :action => 'recent' # => 'proto://host.com/posts/recent'
|
||||
# url_for :controller => 'posts', :action => 'index' # => 'proto://host.com/posts'
|
||||
# url_for :controller => 'posts', :action => 'index', :port=>'8033' # => 'proto://host.com:8033/posts'
|
||||
# url_for :controller => 'posts', :action => 'show', :id => 10 # => 'proto://host.com/posts/show/10'
|
||||
# url_for :controller => 'posts', :user => 'd', :password => '123' # => 'proto://d:123@host.com/posts'
|
||||
#
|
||||
# When generating a new URL, missing values may be filled in from the current request's parameters. For example,
|
||||
# <tt>url_for :action => 'some_action'</tt> will retain the current controller, as expected. This behavior extends to
|
||||
# other parameters, including <tt>:controller</tt>, <tt>:id</tt>, and any other parameters that are placed into a Route's
|
||||
# path.
|
||||
#
|
||||
# The URL helpers such as <tt>url_for</tt> have a limited form of memory: when generating a new URL, they can look for
|
||||
# missing values in the current request's parameters. Routes attempts to guess when a value should and should not be
|
||||
# taken from the defaults. There are a few simple rules on how this is performed:
|
||||
#
|
||||
# * If the controller name begins with a slash no defaults are used:
|
||||
#
|
||||
# url_for :controller => '/home'
|
||||
#
|
||||
# In particular, a leading slash ensures no namespace is assumed. Thus,
|
||||
# while <tt>url_for :controller => 'users'</tt> may resolve to
|
||||
# <tt>Admin::UsersController</tt> if the current controller lives under
|
||||
# that module, <tt>url_for :controller => '/users'</tt> ensures you link
|
||||
# to <tt>::UsersController</tt> no matter what.
|
||||
# * If the controller changes, the action will default to index unless provided
|
||||
#
|
||||
# The final rule is applied while the URL is being generated and is best illustrated by an example. Let us consider the
|
||||
# route given by <tt>map.connect 'people/:last/:first/:action', :action => 'bio', :controller => 'people'</tt>.
|
||||
#
|
||||
# Suppose that the current URL is "people/hh/david/contacts". Let's consider a few different cases of URLs which are generated
|
||||
# from this page.
|
||||
#
|
||||
# * <tt>url_for :action => 'bio'</tt> -- During the generation of this URL, default values will be used for the first and
|
||||
# last components, and the action shall change. The generated URL will be, "people/hh/david/bio".
|
||||
# * <tt>url_for :first => 'davids-little-brother'</tt> This generates the URL 'people/hh/davids-little-brother' -- note
|
||||
# that this URL leaves out the assumed action of 'bio'.
|
||||
#
|
||||
# However, you might ask why the action from the current request, 'contacts', isn't carried over into the new URL. The
|
||||
# answer has to do with the order in which the parameters appear in the generated path. In a nutshell, since the
|
||||
# value that appears in the slot for <tt>:first</tt> is not equal to default value for <tt>:first</tt> we stop using
|
||||
# defaults. On its own, this rule can account for much of the typical Rails URL behavior.
|
||||
#
|
||||
# Although a convenience, defaults can occasionally get in your way. In some cases a default persists longer than desired.
|
||||
# The default may be cleared by adding <tt>:name => nil</tt> to <tt>url_for</tt>'s options.
|
||||
# This is often required when writing form helpers, since the defaults in play may vary greatly depending upon where the
|
||||
# helper is used from. The following line will redirect to PostController's default action, regardless of the page it is
|
||||
# displayed on:
|
||||
#
|
||||
# url_for :controller => 'posts', :action => nil
|
||||
def url_for(options = {})
|
||||
options ||= {}
|
||||
case options
|
||||
when String
|
||||
options
|
||||
when Hash
|
||||
@url.rewrite(rewrite_options(options))
|
||||
else
|
||||
polymorphic_url(options)
|
||||
end
|
||||
end
|
||||
|
||||
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
|
||||
def controller_class_name
|
||||
self.class.controller_class_name
|
||||
@@ -1035,27 +900,6 @@ module ActionController #:nodoc:
|
||||
erase_redirect_results
|
||||
end
|
||||
|
||||
def rewrite_options(options) #:nodoc:
|
||||
if defaults = default_url_options(options)
|
||||
defaults.merge(options)
|
||||
else
|
||||
options
|
||||
end
|
||||
end
|
||||
|
||||
# Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
|
||||
# the form of a hash, just like the one you would use for url_for directly. Example:
|
||||
#
|
||||
# def default_url_options(options)
|
||||
# { :project => @project.active? ? @project.url_name : "unknown" }
|
||||
# end
|
||||
#
|
||||
# As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the
|
||||
# urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set
|
||||
# by this method.
|
||||
def default_url_options(options = nil)
|
||||
end
|
||||
|
||||
# Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
|
||||
#
|
||||
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
|
||||
@@ -1362,7 +1206,8 @@ module ActionController #:nodoc:
|
||||
# Be sure to include shadowed public instance methods of this class
|
||||
public_instance_methods(false).map { |m| m.to_s } -
|
||||
# And always exclude explicitly hidden actions
|
||||
hidden_actions
|
||||
hidden_actions -
|
||||
_routes.named_routes.helper_names
|
||||
end
|
||||
|
||||
def reset_variables_added_to_assigns
|
||||
|
||||
@@ -39,9 +39,9 @@ module ActionController #:nodoc:
|
||||
if cache = read_fragment(name, options)
|
||||
buffer.safe_concat(cache.html_safe)
|
||||
else
|
||||
pos = buffer.length
|
||||
pos = buffer.bytesize
|
||||
block.call
|
||||
write_fragment(name, buffer[pos..-1], options)
|
||||
write_fragment(name, buffer.byteslice(pos..-1), options)
|
||||
end
|
||||
else
|
||||
block.call
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
require 'action_controller/cgi_ext/stdinput'
|
||||
require 'action_controller/cgi_ext/query_extension'
|
||||
require 'action_controller/cgi_ext/cookie'
|
||||
|
||||
class CGI #:nodoc:
|
||||
include ActionController::CgiExt::Stdinput
|
||||
|
||||
class << self
|
||||
alias :escapeHTML_fail_on_nil :escapeHTML
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
require 'delegate'
|
||||
require 'cgi'
|
||||
require 'cgi/cookie'
|
||||
|
||||
CGI.module_eval { remove_const "Cookie" }
|
||||
|
||||
@@ -24,7 +26,7 @@ class CGI #:nodoc:
|
||||
# * <tt>:secure</tt> - Whether this cookie is a secure cookie or not (defaults to
|
||||
# +false+). Secure cookies are only transmitted to HTTPS servers.
|
||||
# * <tt>:http_only</tt> - Whether this cookie can be accessed by client side scripts (e.g. document.cookie) or only over HTTP.
|
||||
# More details in http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx. Defaults to +false+.
|
||||
# More details in http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx. Defaults to +false+.
|
||||
#
|
||||
# These keywords correspond to attributes of the cookie object.
|
||||
def initialize(name = '', *value)
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
require 'cgi'
|
||||
|
||||
class CGI #:nodoc:
|
||||
module QueryExtension
|
||||
# Remove the old initialize_query method before redefining it.
|
||||
remove_method :initialize_query
|
||||
|
||||
# Neuter CGI parameter parsing.
|
||||
def initialize_query
|
||||
# Fix some strange request environments.
|
||||
env_table['REQUEST_METHOD'] ||= 'GET'
|
||||
|
||||
# POST assumes missing Content-Type is application/x-www-form-urlencoded.
|
||||
if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST'
|
||||
env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
||||
end
|
||||
|
||||
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
|
||||
@params = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
require 'cgi'
|
||||
|
||||
module ActionController
|
||||
module CgiExt
|
||||
# Publicize the CGI's internal input stream so we can lazy-read
|
||||
# request.body. Make it writable so we don't have to play $stdin games.
|
||||
module Stdinput
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
remove_method :stdinput
|
||||
attr_accessor :stdinput
|
||||
end
|
||||
|
||||
base.alias_method_chain :initialize, :stdinput
|
||||
end
|
||||
|
||||
def initialize_with_stdinput(type = nil, stdinput = $stdin)
|
||||
@stdinput = stdinput
|
||||
@stdinput.set_encoding(Encoding::BINARY) if @stdinput.respond_to?(:set_encoding)
|
||||
initialize_without_stdinput(type || 'query')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
require 'action_controller/cgi_ext'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class CGIHandler
|
||||
module ProperStream
|
||||
def each
|
||||
while line = gets
|
||||
yield line
|
||||
end
|
||||
end
|
||||
|
||||
def read(*args)
|
||||
if args.empty?
|
||||
super || ""
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.dispatch_cgi(app, cgi, out = $stdout)
|
||||
env = cgi.__send__(:env_table)
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
|
||||
cgi.stdinput.extend ProperStream
|
||||
|
||||
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
||||
|
||||
env.update({
|
||||
"rack.version" => [0,1],
|
||||
"rack.input" => cgi.stdinput,
|
||||
"rack.errors" => $stderr,
|
||||
"rack.multithread" => false,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
|
||||
})
|
||||
|
||||
env["QUERY_STRING"] ||= ""
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env["REQUEST_PATH"] ||= "/"
|
||||
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
|
||||
|
||||
status, headers, body = app.call(env)
|
||||
begin
|
||||
out.binmode if out.respond_to?(:binmode)
|
||||
out.sync = false if out.respond_to?(:sync=)
|
||||
|
||||
headers['Status'] = status.to_s
|
||||
|
||||
if headers.include?('Set-Cookie')
|
||||
headers['cookie'] = headers.delete('Set-Cookie').split("\n")
|
||||
end
|
||||
|
||||
out.write(cgi.header(headers))
|
||||
|
||||
body.each { |part|
|
||||
out.write part
|
||||
out.flush if out.respond_to?(:flush)
|
||||
}
|
||||
ensure
|
||||
body.close if body.respond_to?(:close)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class CgiRequest #:nodoc:
|
||||
DEFAULT_SESSION_OPTIONS = {
|
||||
:database_manager => nil,
|
||||
:prefix => "ruby_sess.",
|
||||
:session_path => "/",
|
||||
:session_key => "_session_id",
|
||||
:cookie_only => true,
|
||||
:session_http_only => true
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -22,11 +22,6 @@ module ActionController
|
||||
end
|
||||
end
|
||||
|
||||
# DEPRECATE: Remove CGI support
|
||||
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
|
||||
new(output).dispatch_cgi(cgi, session_options)
|
||||
end
|
||||
|
||||
# Add a preparation callback. Preparation callbacks are run before every
|
||||
# request in development mode, and before the first request in production
|
||||
# mode.
|
||||
@@ -42,20 +37,14 @@ module ActionController
|
||||
end
|
||||
|
||||
def run_prepare_callbacks
|
||||
if defined?(Rails) && Rails.logger
|
||||
logger = Rails.logger
|
||||
else
|
||||
logger = Logger.new($stderr)
|
||||
end
|
||||
|
||||
new(logger).send :run_callbacks, :prepare_dispatch
|
||||
new.send :run_callbacks, :prepare_dispatch
|
||||
end
|
||||
|
||||
def reload_application
|
||||
# Run prepare callbacks before every request in development mode
|
||||
run_prepare_callbacks
|
||||
|
||||
Routing::Routes.reload
|
||||
ActionController::Routing.routes_reloader.execute_if_updated
|
||||
end
|
||||
|
||||
def cleanup_application
|
||||
@@ -75,10 +64,8 @@ module ActionController
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
|
||||
|
||||
# DEPRECATE: Remove arguments, since they are only used by CGI
|
||||
def initialize(output = $stdout, request = nil, response = nil)
|
||||
@output = output
|
||||
build_middleware_stack if @@cache_classes
|
||||
def initialize
|
||||
build_middleware_stack
|
||||
end
|
||||
|
||||
def dispatch
|
||||
@@ -96,21 +83,11 @@ module ActionController
|
||||
end
|
||||
end
|
||||
|
||||
# DEPRECATE: Remove CGI support
|
||||
def dispatch_cgi(cgi, session_options)
|
||||
CGIHandler.dispatch_cgi(self, cgi, @output)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if @@cache_classes
|
||||
@app.call(env)
|
||||
else
|
||||
Reloader.run do
|
||||
# When class reloading is turned on, we will want to rebuild the
|
||||
# middleware stack every time we process a request. If we don't
|
||||
# rebuild the middleware stack, then the stack may contain references
|
||||
# to old classes metal classes, which will b0rk class reloading.
|
||||
build_middleware_stack
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,6 +77,7 @@ module ActionController #:nodoc:
|
||||
|
||||
@master_helper_class = Class.new(ActionView::Base).tap do |klass|
|
||||
klass.send(:include, master_helper_module)
|
||||
klass.send(:include, ActionController::Routing::Routes.url_helpers)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
require 'stringio'
|
||||
require 'uri'
|
||||
require 'active_support/test_case'
|
||||
require 'action_controller/rack_lint_patch'
|
||||
|
||||
module ActionController
|
||||
module Integration #:nodoc:
|
||||
@@ -91,12 +90,9 @@ module ActionController
|
||||
|
||||
unless defined? @named_routes_configured
|
||||
# install the named routes in this session instance.
|
||||
klass = class << self; self; end
|
||||
Routing::Routes.install_helpers(klass)
|
||||
|
||||
# the helpers are made protected by default--we make them public for
|
||||
# easier access during testing and troubleshooting.
|
||||
klass.module_eval { public *Routing::Routes.named_routes.helpers }
|
||||
class << self
|
||||
include ActionController::Routing::Routes.url_helpers
|
||||
end
|
||||
@named_routes_configured = true
|
||||
end
|
||||
end
|
||||
@@ -130,7 +126,7 @@ module ActionController
|
||||
# performed on the location header.
|
||||
def follow_redirect!
|
||||
raise "not a redirect! #{@status} #{@status_message}" unless redirect?
|
||||
get(interpret_uri(headers['location']))
|
||||
get(headers['location'])
|
||||
status
|
||||
end
|
||||
|
||||
@@ -256,14 +252,15 @@ module ActionController
|
||||
|
||||
# Performs the actual request.
|
||||
def process(method, path, parameters = nil, headers = nil)
|
||||
data = requestify(parameters)
|
||||
data = requestify(parameters) if !parameters.blank?
|
||||
path = interpret_uri(path) if path =~ %r{://}
|
||||
path = "/#{path}" unless path[0] == ?/
|
||||
path, query = path.split('?', 2)
|
||||
@path = path
|
||||
env = {}
|
||||
|
||||
if method == :get
|
||||
env["QUERY_STRING"] = data
|
||||
env["QUERY_STRING"] = query || data
|
||||
data = nil
|
||||
end
|
||||
|
||||
@@ -343,6 +340,10 @@ module ActionController
|
||||
@response = @controller.response
|
||||
@controller.send(:set_test_assigns)
|
||||
else
|
||||
# Fake request for integration tests that never hit a controller ('/' => redirect('/abc'))
|
||||
@request = ActionController::TestRequest.new
|
||||
@request.host = host
|
||||
|
||||
# Decorate responses from Rack Middleware and Rails Metal
|
||||
# as an Response for the purposes of integration testing
|
||||
@response = Response.new
|
||||
|
||||
56
actionpack/lib/action_controller/metal/exceptions.rb
Normal file
56
actionpack/lib/action_controller/metal/exceptions.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
module ActionController
|
||||
class ActionControllerError < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
class BadRequest < ActionControllerError #:nodoc:
|
||||
attr_reader :original_exception
|
||||
|
||||
def initialize(type = nil, e = nil)
|
||||
return super() unless type && e
|
||||
|
||||
super("Invalid #{type} parameters: #{e.message}")
|
||||
@original_exception = e
|
||||
set_backtrace e.backtrace
|
||||
end
|
||||
end
|
||||
|
||||
class RoutingError < ActionControllerError #:nodoc:
|
||||
attr_reader :failures
|
||||
def initialize(message, failures=[])
|
||||
super(message)
|
||||
@failures = failures
|
||||
end
|
||||
end
|
||||
|
||||
class ActionController::UrlGenerationError < RoutingError #:nodoc:
|
||||
end
|
||||
|
||||
class MethodNotAllowed < ActionControllerError #:nodoc:
|
||||
def initialize(*allowed_methods)
|
||||
super("Only #{allowed_methods.to_sentence(:locale => :en)} requests are allowed.")
|
||||
end
|
||||
end
|
||||
|
||||
class NotImplemented < MethodNotAllowed #:nodoc:
|
||||
end
|
||||
|
||||
class UnknownController < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class MissingFile < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class SessionOverflowError < ActionControllerError #:nodoc:
|
||||
DEFAULT_MESSAGE = 'Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data.'
|
||||
|
||||
def initialize(message = nil)
|
||||
super(message || DEFAULT_MESSAGE)
|
||||
end
|
||||
end
|
||||
|
||||
class UnknownHttpMethod < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class UnknownFormat < ActionControllerError #:nodoc:
|
||||
end
|
||||
end
|
||||
22
actionpack/lib/action_controller/metal/url_for.rb
Normal file
22
actionpack/lib/action_controller/metal/url_for.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'active_support/concern'
|
||||
|
||||
module ActionController
|
||||
module UrlFor
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include ActionController::Routing::UrlFor
|
||||
|
||||
def url_options
|
||||
super.reverse_merge(
|
||||
:host => request.host_with_port,
|
||||
:protocol => request.protocol,
|
||||
:_recall => request.symbolized_path_parameters
|
||||
).merge(:script_name => request.script_name)
|
||||
end
|
||||
|
||||
def _routes
|
||||
raise "In order to use #url_for, you must include routing helpers explicitly. " \
|
||||
"For instance, `include Rails.application.routes.url_helpers"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,189 +0,0 @@
|
||||
module ActionController
|
||||
# Polymorphic URL helpers are methods for smart resolution to a named route call when
|
||||
# given an Active Record model instance. They are to be used in combination with
|
||||
# ActionController::Resources.
|
||||
#
|
||||
# These methods are useful when you want to generate correct URL or path to a RESTful
|
||||
# resource without having to know the exact type of the record in question.
|
||||
#
|
||||
# Nested resources and/or namespaces are also supported, as illustrated in the example:
|
||||
#
|
||||
# polymorphic_url([:admin, @article, @comment])
|
||||
#
|
||||
# results in:
|
||||
#
|
||||
# admin_article_comment_url(@article, @comment)
|
||||
#
|
||||
# == Usage within the framework
|
||||
#
|
||||
# Polymorphic URL helpers are used in a number of places throughout the Rails framework:
|
||||
#
|
||||
# * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
|
||||
# <tt>url_for(@article)</tt>;
|
||||
# * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write
|
||||
# <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form
|
||||
# action;
|
||||
# * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write
|
||||
# <tt>redirect_to(post)</tt> in your controllers;
|
||||
# * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs
|
||||
# for feed entries.
|
||||
#
|
||||
# == Prefixed polymorphic helpers
|
||||
#
|
||||
# In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
|
||||
# number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt>
|
||||
# in options. Those are:
|
||||
#
|
||||
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
|
||||
# * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# edit_polymorphic_path(@post) # => "/posts/1/edit"
|
||||
# polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
|
||||
module PolymorphicRoutes
|
||||
# Constructs a call to a named RESTful route for the given record and returns the
|
||||
# resulting URL string. For example:
|
||||
#
|
||||
# # calls post_url(post)
|
||||
# polymorphic_url(post) # => "http://example.com/posts/1"
|
||||
# polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
|
||||
# polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
|
||||
# polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
|
||||
#
|
||||
# ==== Options
|
||||
#
|
||||
# * <tt>:action</tt> - Specifies the action prefix for the named route:
|
||||
# <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
|
||||
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
|
||||
# Default is <tt>:url</tt>.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # an Article record
|
||||
# polymorphic_url(record) # same as article_url(record)
|
||||
#
|
||||
# # a Comment record
|
||||
# polymorphic_url(record) # same as comment_url(record)
|
||||
#
|
||||
# # it recognizes new records and maps to the collection
|
||||
# record = Comment.new
|
||||
# polymorphic_url(record) # same as comments_url()
|
||||
#
|
||||
def polymorphic_url(record_or_hash_or_array, options = {})
|
||||
if record_or_hash_or_array.kind_of?(Array)
|
||||
record_or_hash_or_array = record_or_hash_or_array.compact
|
||||
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
|
||||
end
|
||||
|
||||
record = extract_record(record_or_hash_or_array)
|
||||
|
||||
args = case record_or_hash_or_array
|
||||
when Hash; [ record_or_hash_or_array ]
|
||||
when Array; record_or_hash_or_array.dup
|
||||
else [ record_or_hash_or_array ]
|
||||
end
|
||||
|
||||
inflection =
|
||||
case
|
||||
when options[:action].to_s == "new"
|
||||
args.pop
|
||||
:singular
|
||||
when record.respond_to?(:new_record?) && record.new_record?
|
||||
args.pop
|
||||
:plural
|
||||
else
|
||||
:singular
|
||||
end
|
||||
|
||||
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
|
||||
named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
|
||||
|
||||
url_options = options.except(:action, :routing_type)
|
||||
unless url_options.empty?
|
||||
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
|
||||
end
|
||||
|
||||
__send__(named_route, *args)
|
||||
end
|
||||
|
||||
# Returns the path component of a URL for the given record. It uses
|
||||
# <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>.
|
||||
def polymorphic_path(record_or_hash_or_array, options = {})
|
||||
options[:routing_type] = :path
|
||||
polymorphic_url(record_or_hash_or_array, options)
|
||||
end
|
||||
|
||||
%w(edit new).each do |action|
|
||||
module_eval <<-EOT, __FILE__, __LINE__ + 1
|
||||
def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
|
||||
polymorphic_url( # polymorphic_url(
|
||||
record_or_hash, # record_or_hash,
|
||||
options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
|
||||
end # end
|
||||
#
|
||||
def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
|
||||
polymorphic_url( # polymorphic_url(
|
||||
record_or_hash, # record_or_hash,
|
||||
options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
|
||||
end # end
|
||||
EOT
|
||||
end
|
||||
|
||||
def formatted_polymorphic_url(record_or_hash, options = {})
|
||||
ActiveSupport::Deprecation.warn("formatted_polymorphic_url has been deprecated. Please pass :format to the polymorphic_url method instead", caller)
|
||||
options[:format] = record_or_hash.pop if Array === record_or_hash
|
||||
polymorphic_url(record_or_hash, options)
|
||||
end
|
||||
|
||||
def formatted_polymorphic_path(record_or_hash, options = {})
|
||||
ActiveSupport::Deprecation.warn("formatted_polymorphic_path has been deprecated. Please pass :format to the polymorphic_path method instead", caller)
|
||||
options[:format] = record_or_hash.pop if record_or_hash === Array
|
||||
polymorphic_url(record_or_hash, options.merge(:routing_type => :path))
|
||||
end
|
||||
|
||||
private
|
||||
def action_prefix(options)
|
||||
options[:action] ? "#{options[:action]}_" : ''
|
||||
end
|
||||
|
||||
def routing_type(options)
|
||||
options[:routing_type] || :url
|
||||
end
|
||||
|
||||
def build_named_route_call(records, inflection, options = {})
|
||||
unless records.is_a?(Array)
|
||||
record = extract_record(records)
|
||||
route = ''
|
||||
else
|
||||
record = records.pop
|
||||
route = records.inject("") do |string, parent|
|
||||
if parent.is_a?(Symbol) || parent.is_a?(String)
|
||||
string << "#{parent}_"
|
||||
else
|
||||
string << RecordIdentifier.__send__("plural_class_name", parent).singularize
|
||||
string << "_"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if record.is_a?(Symbol) || record.is_a?(String)
|
||||
route << "#{record}_"
|
||||
else
|
||||
route << RecordIdentifier.__send__("plural_class_name", record)
|
||||
route = route.singularize if inflection == :singular
|
||||
route << "_"
|
||||
end
|
||||
|
||||
action_prefix(options) + route + routing_type(options).to_s
|
||||
end
|
||||
|
||||
def extract_record(record_or_hash_or_array)
|
||||
case record_or_hash_or_array
|
||||
when Array; record_or_hash_or_array.last
|
||||
when Hash; record_or_hash_or_array[:id]
|
||||
else record_or_hash_or_array
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
# Rack 1.0 does not allow string subclass body. This does not play well with our ActiveSupport::SafeBuffer.
|
||||
# The next release of Rack will be allowing string subclass body - http://github.com/rack/rack/commit/de668df02802a0335376a81ba709270e43ba9d55
|
||||
# TODO : Remove this monkey patch after the next release of Rack
|
||||
|
||||
module RackLintPatch
|
||||
module AllowStringSubclass
|
||||
def self.included(base)
|
||||
base.send :alias_method, :each, :each_with_hack
|
||||
end
|
||||
|
||||
def each_with_hack
|
||||
@closed = false
|
||||
|
||||
@body.each { |part|
|
||||
assert("Body yielded non-string value #{part.inspect}") {
|
||||
part.kind_of?(String)
|
||||
}
|
||||
yield part
|
||||
}
|
||||
|
||||
if @body.respond_to?(:to_path)
|
||||
assert("The file identified by body.to_path does not exist") {
|
||||
::File.exist? @body.to_path
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
app = proc {|env| [200, {"Content-Type" => "text/plain", "Content-Length" => "12"}, [Class.new(String).new("Hello World!")]] }
|
||||
response = Rack::MockRequest.new(Rack::Lint.new(app)).get('/')
|
||||
rescue Rack::Lint::LintError => e
|
||||
raise(e) unless e.message =~ /Body yielded non-string value/
|
||||
Rack::Lint.send :include, AllowStringSubclass
|
||||
end
|
||||
end
|
||||
@@ -35,6 +35,10 @@ module ActionController
|
||||
@request_method ||= HTTP_METHOD_LOOKUP[super] || raise(UnknownHttpMethod, "#{super}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
|
||||
end
|
||||
|
||||
def request_method_string
|
||||
@request_method_string ||= @env['REQUEST_METHOD']
|
||||
end
|
||||
|
||||
# Returns the HTTP request \method used for action processing as a
|
||||
# lowercase symbol, such as <tt>:post</tt>. (Unlike #request_method, this
|
||||
# method returns <tt>:get</tt> for a HEAD request because the two are
|
||||
@@ -308,6 +312,10 @@ EOM
|
||||
end
|
||||
end
|
||||
|
||||
def standard_port?
|
||||
port == standard_port
|
||||
end
|
||||
|
||||
# Returns a \port suffix like ":8080" if the \port number of this request
|
||||
# is not the default HTTP \port 80 or HTTPS \port 443.
|
||||
def port_string
|
||||
@@ -332,6 +340,10 @@ EOM
|
||||
parts[0..-(tld_length+2)]
|
||||
end
|
||||
|
||||
def subdomain(tld_length = 1)
|
||||
subdomains(tld_length).join('.')
|
||||
end
|
||||
|
||||
# Returns the query string, accounting for server idiosyncrasies.
|
||||
def query_string
|
||||
@env['QUERY_STRING'].present? ? @env['QUERY_STRING'] : (@env['REQUEST_URI'].split('?', 2)[1] || '')
|
||||
@@ -423,13 +435,13 @@ EOM
|
||||
|
||||
# Override Rack's GET method to support indifferent access
|
||||
def GET
|
||||
@env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
|
||||
@env["action_controller.request.query_parameters"] ||= deep_munge(normalize_parameters(super) || {})
|
||||
end
|
||||
alias_method :query_parameters, :GET
|
||||
|
||||
# Override Rack's POST method to support indifferent access
|
||||
def POST
|
||||
@env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
|
||||
@env["action_controller.request.request_parameters"] ||= deep_munge(normalize_parameters(super) || {})
|
||||
end
|
||||
alias_method :request_parameters, :POST
|
||||
|
||||
@@ -469,6 +481,22 @@ EOM
|
||||
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
||||
end
|
||||
|
||||
# Remove nils from the params hash
|
||||
def deep_munge(hash)
|
||||
hash.each do |k, v|
|
||||
case v
|
||||
when Array
|
||||
v.grep(Hash) { |x| deep_munge(x) }
|
||||
v.compact!
|
||||
hash[k] = nil if v.empty?
|
||||
when Hash
|
||||
deep_munge(v)
|
||||
end
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
# Convert nested Hashs to HashWithIndifferentAccess and replace
|
||||
# file upload hashs with UploadedFile objects
|
||||
def normalize_parameters(value)
|
||||
|
||||
@@ -1,42 +1,17 @@
|
||||
require 'cgi'
|
||||
require 'uri'
|
||||
require 'action_controller/routing/optimisations'
|
||||
require 'action_controller/routing/routing_ext'
|
||||
require 'action_controller/routing/route'
|
||||
require 'action_controller/routing/segments'
|
||||
require 'action_controller/routing/builder'
|
||||
require 'action_controller/routing/route_set'
|
||||
require 'action_controller/routing/recognition_optimisation'
|
||||
require 'active_support/core_ext/object/to_param'
|
||||
require 'active_support/core_ext/regexp'
|
||||
require 'active_support/file_update_checker'
|
||||
|
||||
module ActionController
|
||||
# == Routing
|
||||
#
|
||||
# The routing module provides URL rewriting in native Ruby. It's a way to
|
||||
# redirect incoming requests to controllers and actions. This replaces
|
||||
# mod_rewrite rules. Best of all, Rails' Routing works with any web server.
|
||||
# mod_rewrite rules. Best of all, Rails' \Routing works with any web server.
|
||||
# Routes are defined in <tt>config/routes.rb</tt>.
|
||||
#
|
||||
# Consider the following route, installed by Rails when you generate your
|
||||
# application:
|
||||
#
|
||||
# map.connect ':controller/:action/:id'
|
||||
#
|
||||
# This route states that it expects requests to consist of a
|
||||
# <tt>:controller</tt> followed by an <tt>:action</tt> that in turn is fed
|
||||
# some <tt>:id</tt>.
|
||||
#
|
||||
# Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end up
|
||||
# with:
|
||||
#
|
||||
# params = { :controller => 'blog',
|
||||
# :action => 'edit',
|
||||
# :id => '22'
|
||||
# }
|
||||
#
|
||||
# Think of creating routes as drawing a map for your requests. The map tells
|
||||
# them where to go based on some predefined pattern:
|
||||
#
|
||||
# ActionController::Routing::Routes.draw do |map|
|
||||
# AppName::Application.routes.draw do
|
||||
# Pattern 1 tells some request to go to one place
|
||||
# Pattern 2 tell them to go to another
|
||||
# ...
|
||||
@@ -49,60 +24,49 @@ module ActionController
|
||||
#
|
||||
# Other names simply map to a parameter as in the case of <tt>:id</tt>.
|
||||
#
|
||||
# == Route priority
|
||||
# == Resources
|
||||
#
|
||||
# Not all routes are created equally. Routes have priority defined by the
|
||||
# order of appearance of the routes in the <tt>config/routes.rb</tt> file. The priority goes
|
||||
# from top to bottom. The last route in that file is at the lowest priority
|
||||
# and will be applied last. If no route matches, 404 is returned.
|
||||
# Resource routing allows you to quickly declare all of the common routes
|
||||
# for a given resourceful controller. Instead of declaring separate routes
|
||||
# for your +index+, +show+, +new+, +edit+, +create+, +update+ and +destroy+
|
||||
# actions, a resourceful route declares them in a single line of code:
|
||||
#
|
||||
# Within blocks, the empty pattern is at the highest priority.
|
||||
# In practice this works out nicely:
|
||||
# resources :photos
|
||||
#
|
||||
# ActionController::Routing::Routes.draw do |map|
|
||||
# map.with_options :controller => 'blog' do |blog|
|
||||
# blog.show '', :action => 'list'
|
||||
# end
|
||||
# map.connect ':controller/:action/:view'
|
||||
# Sometimes, you have a resource that clients always look up without
|
||||
# referencing an ID. A common example, /profile always shows the profile of
|
||||
# the currently logged in user. In this case, you can use a singular resource
|
||||
# to map /profile (rather than /profile/:id) to the show action.
|
||||
#
|
||||
# resource :profile
|
||||
#
|
||||
# It's common to have resources that are logically children of other
|
||||
# resources:
|
||||
#
|
||||
# resources :magazines do
|
||||
# resources :ads
|
||||
# end
|
||||
#
|
||||
# In this case, invoking blog controller (with an URL like '/blog/')
|
||||
# without parameters will activate the 'list' action by default.
|
||||
# You may wish to organize groups of controllers under a namespace. Most
|
||||
# commonly, you might group a number of administrative controllers under
|
||||
# an +admin+ namespace. You would place these controllers under the
|
||||
# app/controllers/admin directory, and you can group them together in your
|
||||
# router:
|
||||
#
|
||||
# == Defaults routes and default parameters
|
||||
#
|
||||
# Setting a default route is straightforward in Rails - you simply append a
|
||||
# Hash at the end of your mapping to set any default parameters.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# ActionController::Routing:Routes.draw do |map|
|
||||
# map.connect ':controller/:action/:id', :controller => 'blog'
|
||||
# namespace "admin" do
|
||||
# resources :posts, :comments
|
||||
# end
|
||||
#
|
||||
# This sets up +blog+ as the default controller if no other is specified.
|
||||
# This means visiting '/' would invoke the blog controller.
|
||||
#
|
||||
# More formally, you can include arbitrary parameters in the route, thus:
|
||||
#
|
||||
# map.connect ':controller/:action/:id', :action => 'show', :page => 'Dashboard'
|
||||
#
|
||||
# This will pass the :page parameter to all incoming requests that match this route.
|
||||
#
|
||||
# Note: The default routes, as provided by the Rails generator, make all actions in every
|
||||
# controller accessible via GET requests. You should consider removing them or commenting
|
||||
# them out if you're using named routes and resources.
|
||||
#
|
||||
# == Named routes
|
||||
#
|
||||
# Routes can be named with the syntax <tt>map.name_of_route options</tt>,
|
||||
# Routes can be named by passing an <tt>:as</tt> option,
|
||||
# allowing for easy reference within your source as +name_of_route_url+
|
||||
# for the full URL and +name_of_route_path+ for the URI path.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# # In routes.rb
|
||||
# map.login 'login', :controller => 'accounts', :action => 'login'
|
||||
# match '/login' => 'accounts#login', :as => 'login'
|
||||
#
|
||||
# # With render, redirect_to, tests, etc.
|
||||
# redirect_to login_url
|
||||
@@ -111,32 +75,26 @@ module ActionController
|
||||
#
|
||||
# redirect_to show_item_path(:id => 25)
|
||||
#
|
||||
# Use <tt>map.root</tt> as a shorthand to name a route for the root path "".
|
||||
# Use <tt>root</tt> as a shorthand to name a route for the root path "/".
|
||||
#
|
||||
# # In routes.rb
|
||||
# map.root :controller => 'blogs'
|
||||
# root :to => 'blogs#index'
|
||||
#
|
||||
# # would recognize http://www.example.com/ as
|
||||
# params = { :controller => 'blogs', :action => 'index' }
|
||||
#
|
||||
# # and provide these named routes
|
||||
# root_url # => 'http://www.example.com/'
|
||||
# root_path # => ''
|
||||
# root_path # => '/'
|
||||
#
|
||||
# You can also specify an already-defined named route in your <tt>map.root</tt> call:
|
||||
#
|
||||
# # In routes.rb
|
||||
# map.new_session :controller => 'sessions', :action => 'new'
|
||||
# map.root :new_session
|
||||
#
|
||||
# Note: when using +with_options+, the route is simply named after the
|
||||
# Note: when using +controller+, the route is simply named after the
|
||||
# method you call on the block parameter rather than map.
|
||||
#
|
||||
# # In routes.rb
|
||||
# map.with_options :controller => 'blog' do |blog|
|
||||
# blog.show '', :action => 'list'
|
||||
# blog.delete 'delete/:id', :action => 'delete',
|
||||
# blog.edit 'edit/:id', :action => 'edit'
|
||||
# controller :blog do
|
||||
# match 'blog/show' => :list
|
||||
# match 'blog/delete' => :delete
|
||||
# match 'blog/edit/:id' => :edit
|
||||
# end
|
||||
#
|
||||
# # provides named routes for show, delete, and edit
|
||||
@@ -146,12 +104,11 @@ module ActionController
|
||||
#
|
||||
# Routes can generate pretty URLs. For example:
|
||||
#
|
||||
# map.connect 'articles/:year/:month/:day',
|
||||
# :controller => 'articles',
|
||||
# :action => 'find_by_date',
|
||||
# :year => /\d{4}/,
|
||||
# :month => /\d{1,2}/,
|
||||
# :day => /\d{1,2}/
|
||||
# match '/articles/:year/:month/:day' => 'articles#find_by_id', :constraints => {
|
||||
# :year => /\d{4}/,
|
||||
# :month => /\d{1,2}/,
|
||||
# :day => /\d{1,2}/
|
||||
# }
|
||||
#
|
||||
# Using the route above, the URL "http://localhost:3000/articles/2005/11/06"
|
||||
# maps to
|
||||
@@ -161,64 +118,105 @@ module ActionController
|
||||
# == Regular Expressions and parameters
|
||||
# You can specify a regular expression to define a format for a parameter.
|
||||
#
|
||||
# map.geocode 'geocode/:postalcode', :controller => 'geocode',
|
||||
# :action => 'show', :postalcode => /\d{5}(-\d{4})?/
|
||||
# controller 'geocode' do
|
||||
# match 'geocode/:postalcode' => :show, :constraints => {
|
||||
# :postalcode => /\d{5}(-\d{4})?/
|
||||
# }
|
||||
#
|
||||
# or, more formally:
|
||||
#
|
||||
# map.geocode 'geocode/:postalcode', :controller => 'geocode',
|
||||
# :action => 'show', :requirements => { :postalcode => /\d{5}(-\d{4})?/ }
|
||||
#
|
||||
# Formats can include the 'ignorecase' and 'extended syntax' regular
|
||||
# Constraints can include the 'ignorecase' and 'extended syntax' regular
|
||||
# expression modifiers:
|
||||
#
|
||||
# map.geocode 'geocode/:postalcode', :controller => 'geocode',
|
||||
# :action => 'show', :postalcode => /hx\d\d\s\d[a-z]{2}/i
|
||||
# controller 'geocode' do
|
||||
# match 'geocode/:postalcode' => :show, :constraints => {
|
||||
# :postalcode => /hx\d\d\s\d[a-z]{2}/i
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# map.geocode 'geocode/:postalcode', :controller => 'geocode',
|
||||
# :action => 'show',:requirements => {
|
||||
# :postalcode => /# Postcode format
|
||||
# \d{5} #Prefix
|
||||
# (-\d{4})? #Suffix
|
||||
# /x
|
||||
# }
|
||||
# controller 'geocode' do
|
||||
# match 'geocode/:postalcode' => :show, :constraints => {
|
||||
# :postalcode => /# Postcode format
|
||||
# \d{5} #Prefix
|
||||
# (-\d{4})? #Suffix
|
||||
# /x
|
||||
# }
|
||||
# end
|
||||
#
|
||||
# Using the multiline match modifier will raise an ArgumentError.
|
||||
# Encoding regular expression modifiers are silently ignored. The
|
||||
# match will always use the default encoding or ASCII.
|
||||
#
|
||||
# == Route globbing
|
||||
# == Default route
|
||||
#
|
||||
# Specifying <tt>*[string]</tt> as part of a rule like:
|
||||
# Consider the following route, which you will find commented out at the
|
||||
# bottom of your generated <tt>config/routes.rb</tt>:
|
||||
#
|
||||
# map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
|
||||
# match ':controller(/:action(/:id(.:format)))'
|
||||
#
|
||||
# will glob all remaining parts of the route that were not recognized earlier.
|
||||
# The globbed values are in <tt>params[:path]</tt> as an array of path segments.
|
||||
# This route states that it expects requests to consist of a
|
||||
# <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in
|
||||
# turn is followed optionally by an <tt>:id</tt>, which in turn is followed
|
||||
# optionally by a <tt>:format</tt>.
|
||||
#
|
||||
# == Route conditions
|
||||
# Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end
|
||||
# up with:
|
||||
#
|
||||
# With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>.
|
||||
# params = { :controller => 'blog',
|
||||
# :action => 'edit',
|
||||
# :id => '22'
|
||||
# }
|
||||
#
|
||||
# * <tt>:method</tt> - Allows you to specify which method can access the route. Possible values are <tt>:post</tt>,
|
||||
# <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. The default value is <tt>:any</tt>,
|
||||
# <tt>:any</tt> means that any method can access the route.
|
||||
# By not relying on default routes, you improve the security of your
|
||||
# application since not all controller actions, which includes actions you
|
||||
# might add at a later time, are exposed by default.
|
||||
#
|
||||
# Example:
|
||||
# == HTTP Methods
|
||||
#
|
||||
# map.connect 'post/:id', :controller => 'posts', :action => 'show',
|
||||
# :conditions => { :method => :get }
|
||||
# map.connect 'post/:id', :controller => 'posts', :action => 'create_comment',
|
||||
# :conditions => { :method => :post }
|
||||
# Using the <tt>:via</tt> option when specifying a route allows you to restrict it to a specific HTTP method.
|
||||
# Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>.
|
||||
# If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>.
|
||||
# The default value is <tt>:any</tt> which means that the route will respond to any of the HTTP methods.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# match 'post/:id' => 'posts#show', :via => :get
|
||||
# match 'post/:id' => "posts#create_comment', :via => :post
|
||||
#
|
||||
# Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
|
||||
# URL will route to the <tt>show</tt> action.
|
||||
#
|
||||
# === HTTP helper methods
|
||||
#
|
||||
# An alternative method of specifying which HTTP method a route should respond to is to use the helper
|
||||
# methods <tt>get</tt>, <tt>post</tt>, <tt>put</tt> and <tt>delete</tt>.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# get 'post/:id' => 'posts#show'
|
||||
# post 'post/:id' => "posts#create_comment'
|
||||
#
|
||||
# This syntax is less verbose and the intention is more apparent to someone else reading your code,
|
||||
# however if your route needs to respond to more than one HTTP method (or all methods) then using the
|
||||
# <tt>:via</tt> option on <tt>match</tt> is preferable.
|
||||
#
|
||||
# == External redirects
|
||||
#
|
||||
# You can redirect any path to another path using the redirect helper in your router:
|
||||
#
|
||||
# match "/stories" => redirect("/posts")
|
||||
#
|
||||
# == Routing to Rack Applications
|
||||
#
|
||||
# Instead of a String, like <tt>posts#index</tt>, which corresponds to the
|
||||
# index action in the PostsController, you can specify any Rack application
|
||||
# as the endpoint for a matcher:
|
||||
#
|
||||
# match "/application.js" => Sprockets
|
||||
#
|
||||
# == Reloading routes
|
||||
#
|
||||
# You can reload routes if you feel you must:
|
||||
#
|
||||
# ActionController::Routing::Routes.reload
|
||||
# Rails.application.reload_routes!
|
||||
#
|
||||
# This will clear all named routes and reload routes.rb if the file has been modified from
|
||||
# last load. To absolutely force reloading, use <tt>reload!</tt>.
|
||||
@@ -262,127 +260,34 @@ module ActionController
|
||||
#
|
||||
# == View a list of all your routes
|
||||
#
|
||||
# Run <tt>rake routes</tt>.
|
||||
# rake routes
|
||||
#
|
||||
# Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>.
|
||||
#
|
||||
module Routing
|
||||
SEPARATORS = %w( / . ? )
|
||||
autoload :DeprecatedMapper, 'action_controller/routing/deprecated_mapper'
|
||||
autoload :Mapper, 'action_controller/routing/mapper'
|
||||
autoload :RouteSet, 'action_controller/routing/route_set'
|
||||
autoload :UrlFor, 'action_controller/routing/url_for'
|
||||
autoload :PolymorphicRoutes, 'action_controller/routing/polymorphic_routes'
|
||||
|
||||
HTTP_METHODS = [:get, :head, :post, :put, :delete, :options]
|
||||
|
||||
ALLOWED_REQUIREMENTS_FOR_OPTIMISATION = [:controller, :action].to_set
|
||||
|
||||
mattr_accessor :generate_best_match
|
||||
self.generate_best_match = true
|
||||
|
||||
# The root paths which may contain controller files
|
||||
mattr_accessor :controller_paths
|
||||
self.controller_paths = []
|
||||
|
||||
# A helper module to hold URL related helpers.
|
||||
module Helpers
|
||||
include PolymorphicRoutes
|
||||
end
|
||||
|
||||
class << self
|
||||
# Expects an array of controller names as the first argument.
|
||||
# Executes the passed block with only the named controllers named available.
|
||||
# This method is used in internal Rails testing.
|
||||
def with_controllers(names)
|
||||
prior_controllers = @possible_controllers
|
||||
use_controllers! names
|
||||
yield
|
||||
ensure
|
||||
use_controllers! prior_controllers
|
||||
end
|
||||
|
||||
# Returns an array of paths, cleaned of double-slashes and relative path references.
|
||||
# * "\\\" and "//" become "\\" or "/".
|
||||
# * "/foo/bar/../config" becomes "/foo/config".
|
||||
# The returned array is sorted by length, descending.
|
||||
def normalize_paths(paths)
|
||||
# do the hokey-pokey of path normalization...
|
||||
paths = paths.collect do |path|
|
||||
path = path.
|
||||
gsub("//", "/"). # replace double / chars with a single
|
||||
gsub("\\\\", "\\"). # replace double \ chars with a single
|
||||
gsub(%r{(.)[\\/]$}, '\1') # drop final / or \ if path ends with it
|
||||
|
||||
# eliminate .. paths where possible
|
||||
re = %r{[^/\\]+[/\\]\.\.[/\\]}
|
||||
path.gsub!(re, "") while path.match(re)
|
||||
path
|
||||
end
|
||||
|
||||
# start with longest path, first
|
||||
paths = paths.uniq.sort_by { |path| - path.length }
|
||||
end
|
||||
|
||||
# Returns the array of controller names currently available to ActionController::Routing.
|
||||
def possible_controllers
|
||||
unless @possible_controllers
|
||||
@possible_controllers = []
|
||||
|
||||
paths = controller_paths.select { |path| File.directory?(path) && path != "." }
|
||||
|
||||
seen_paths = Hash.new {|h, k| h[k] = true; false}
|
||||
normalize_paths(paths).each do |load_path|
|
||||
Dir["#{load_path}/**/*_controller.rb"].collect do |path|
|
||||
next if seen_paths[path.gsub(%r{^\.[/\\]}, "")]
|
||||
|
||||
controller_name = path[(load_path.length + 1)..-1]
|
||||
|
||||
controller_name.gsub!(/_controller\.rb\Z/, '')
|
||||
@possible_controllers << controller_name
|
||||
end
|
||||
end
|
||||
|
||||
# remove duplicates
|
||||
@possible_controllers.uniq!
|
||||
end
|
||||
@possible_controllers
|
||||
end
|
||||
|
||||
# Replaces the internal list of controllers available to ActionController::Routing with the passed argument.
|
||||
# ActionController::Routing.use_controllers!([ "posts", "comments", "admin/comments" ])
|
||||
def use_controllers!(controller_names)
|
||||
@possible_controllers = controller_names
|
||||
end
|
||||
|
||||
# Returns a controller path for a new +controller+ based on a +previous+ controller path.
|
||||
# Handles 4 scenarios:
|
||||
#
|
||||
# * stay in the previous controller:
|
||||
# controller_relative_to( nil, "groups/discussion" ) # => "groups/discussion"
|
||||
#
|
||||
# * stay in the previous namespace:
|
||||
# controller_relative_to( "posts", "groups/discussion" ) # => "groups/posts"
|
||||
#
|
||||
# * forced move to the root namespace:
|
||||
# controller_relative_to( "/posts", "groups/discussion" ) # => "posts"
|
||||
#
|
||||
# * previous namespace is root:
|
||||
# controller_relative_to( "posts", "anything_with_no_slashes" ) # =>"posts"
|
||||
#
|
||||
def controller_relative_to(controller, previous)
|
||||
if controller.nil? then previous
|
||||
elsif controller[0] == ?/ then controller[1..-1]
|
||||
elsif %r{^(.*)/} =~ previous then "#{$1}/#{controller}"
|
||||
else controller
|
||||
end
|
||||
end
|
||||
end
|
||||
SEPARATORS = %w( / . ? ) #:nodoc:
|
||||
HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
|
||||
|
||||
Routes = RouteSet.new
|
||||
|
||||
ActiveSupport::Inflector.module_eval do
|
||||
# Ensures that routes are reloaded when Rails inflections are updated.
|
||||
def inflections_with_route_reloading(&block)
|
||||
(inflections_without_route_reloading(&block)).tap {
|
||||
ActionController::Routing::Routes.reload! if block_given?
|
||||
}
|
||||
end
|
||||
def self.routes_reloader
|
||||
@routes_reloader ||= ActiveSupport::FileUpdateChecker.new([]){ reload_routes! }
|
||||
end
|
||||
|
||||
alias_method_chain :inflections, :route_reloading
|
||||
def self.reload_routes!
|
||||
_routes = Routes
|
||||
_routes.disable_clear_and_finalize = true
|
||||
_routes.clear!
|
||||
routes_reloader.paths.each { |path| load(path) }
|
||||
_routes.finalize!
|
||||
ensure
|
||||
_routes.disable_clear_and_finalize = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
module ActionController
|
||||
module Routing
|
||||
class RouteBuilder #:nodoc:
|
||||
attr_reader :separators, :optional_separators
|
||||
attr_reader :separator_regexp, :nonseparator_regexp, :interval_regexp
|
||||
|
||||
def initialize
|
||||
@separators = Routing::SEPARATORS
|
||||
@optional_separators = %w( / )
|
||||
|
||||
@separator_regexp = /[#{Regexp.escape(separators.join)}]/
|
||||
@nonseparator_regexp = /\A([^#{Regexp.escape(separators.join)}]+)/
|
||||
@interval_regexp = /(.*?)(#{separator_regexp}|$)/
|
||||
end
|
||||
|
||||
# Accepts a "route path" (a string defining a route), and returns the array
|
||||
# of segments that corresponds to it. Note that the segment array is only
|
||||
# partially initialized--the defaults and requirements, for instance, need
|
||||
# to be set separately, via the +assign_route_options+ method, and the
|
||||
# <tt>optional?</tt> method for each segment will not be reliable until after
|
||||
# +assign_route_options+ is called, as well.
|
||||
def segments_for_route_path(path)
|
||||
rest, segments = path, []
|
||||
|
||||
until rest.empty?
|
||||
segment, rest = segment_for(rest)
|
||||
segments << segment
|
||||
end
|
||||
segments
|
||||
end
|
||||
|
||||
# A factory method that returns a new segment instance appropriate for the
|
||||
# format of the given string.
|
||||
def segment_for(string)
|
||||
segment =
|
||||
case string
|
||||
when /\A\.(:format)?\//
|
||||
OptionalFormatSegment.new
|
||||
when /\A:(\w+)/
|
||||
key = $1.to_sym
|
||||
key == :controller ? ControllerSegment.new(key) : DynamicSegment.new(key)
|
||||
when /\A\*(\w+)/
|
||||
PathSegment.new($1.to_sym, :optional => true)
|
||||
when /\A\?(.*?)\?/
|
||||
StaticSegment.new($1, :optional => true)
|
||||
when nonseparator_regexp
|
||||
StaticSegment.new($1)
|
||||
when separator_regexp
|
||||
DividerSegment.new($&, :optional => optional_separators.include?($&))
|
||||
end
|
||||
[segment, $~.post_match]
|
||||
end
|
||||
|
||||
# Split the given hash of options into requirement and default hashes. The
|
||||
# segments are passed alongside in order to distinguish between default values
|
||||
# and requirements.
|
||||
def divide_route_options(segments, options)
|
||||
options = options.except(:path_prefix, :name_prefix)
|
||||
|
||||
if options[:namespace]
|
||||
options[:controller] = "#{options.delete(:namespace).sub(/\/$/, '')}/#{options[:controller]}"
|
||||
end
|
||||
|
||||
requirements = (options.delete(:requirements) || {}).dup
|
||||
defaults = (options.delete(:defaults) || {}).dup
|
||||
conditions = (options.delete(:conditions) || {}).dup
|
||||
|
||||
validate_route_conditions(conditions)
|
||||
|
||||
path_keys = segments.collect { |segment| segment.key if segment.respond_to?(:key) }.compact
|
||||
options.each do |key, value|
|
||||
hash = (path_keys.include?(key) && ! value.is_a?(Regexp)) ? defaults : requirements
|
||||
hash[key] = value
|
||||
end
|
||||
|
||||
[defaults, requirements, conditions]
|
||||
end
|
||||
|
||||
# Takes a hash of defaults and a hash of requirements, and assigns them to
|
||||
# the segments. Any unused requirements (which do not correspond to a segment)
|
||||
# are returned as a hash.
|
||||
def assign_route_options(segments, defaults, requirements)
|
||||
route_requirements = {} # Requirements that do not belong to a segment
|
||||
|
||||
segment_named = Proc.new do |key|
|
||||
segments.detect { |segment| segment.key == key if segment.respond_to?(:key) }
|
||||
end
|
||||
|
||||
requirements.each do |key, requirement|
|
||||
segment = segment_named[key]
|
||||
if segment
|
||||
raise TypeError, "#{key}: requirements on a path segment must be regular expressions" unless requirement.is_a?(Regexp)
|
||||
if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
|
||||
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
|
||||
end
|
||||
if requirement.multiline?
|
||||
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
|
||||
end
|
||||
segment.regexp = requirement
|
||||
else
|
||||
route_requirements[key] = requirement
|
||||
end
|
||||
end
|
||||
|
||||
defaults.each do |key, default|
|
||||
segment = segment_named[key]
|
||||
raise ArgumentError, "#{key}: No matching segment exists; cannot assign default" unless segment
|
||||
segment.is_optional = true
|
||||
segment.default = default.to_param if default
|
||||
end
|
||||
|
||||
assign_default_route_options(segments)
|
||||
ensure_required_segments(segments)
|
||||
route_requirements
|
||||
end
|
||||
|
||||
# Assign default options, such as 'index' as a default for <tt>:action</tt>. This
|
||||
# method must be run *after* user supplied requirements and defaults have
|
||||
# been applied to the segments.
|
||||
def assign_default_route_options(segments)
|
||||
segments.each do |segment|
|
||||
next unless segment.is_a? DynamicSegment
|
||||
case segment.key
|
||||
when :action
|
||||
if segment.regexp.nil? || segment.regexp.match('index').to_s == 'index'
|
||||
segment.default ||= 'index'
|
||||
segment.is_optional = true
|
||||
end
|
||||
when :id
|
||||
if segment.default.nil? && segment.regexp.nil? || segment.regexp =~ ''
|
||||
segment.is_optional = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Makes sure that there are no optional segments that precede a required
|
||||
# segment. If any are found that precede a required segment, they are
|
||||
# made required.
|
||||
def ensure_required_segments(segments)
|
||||
allow_optional = true
|
||||
segments.reverse_each do |segment|
|
||||
allow_optional &&= segment.optional?
|
||||
if !allow_optional && segment.optional?
|
||||
unless segment.optionality_implied?
|
||||
warn "Route segment \"#{segment.to_s}\" cannot be optional because it precedes a required segment. This segment will be required."
|
||||
end
|
||||
segment.is_optional = false
|
||||
elsif allow_optional && segment.respond_to?(:default) && segment.default
|
||||
# if a segment has a default, then it is optional
|
||||
segment.is_optional = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Construct and return a route with the given path and options.
|
||||
def build(path, options)
|
||||
# Wrap the path with slashes
|
||||
path = "/#{path}" unless path[0] == ?/
|
||||
path = "#{path}/" unless path[-1] == ?/
|
||||
|
||||
prefix = options[:path_prefix].to_s.gsub(/^\//,'')
|
||||
path = "/#{prefix}#{path}" unless prefix.blank?
|
||||
|
||||
segments = segments_for_route_path(path)
|
||||
defaults, requirements, conditions = divide_route_options(segments, options)
|
||||
requirements = assign_route_options(segments, defaults, requirements)
|
||||
|
||||
# TODO: Segments should be frozen on initialize
|
||||
segments.each { |segment| segment.freeze }
|
||||
|
||||
route = Route.new(segments, requirements, conditions)
|
||||
|
||||
if !route.significant_keys.include?(:controller)
|
||||
raise ArgumentError, "Illegal route: the :controller must be specified!"
|
||||
end
|
||||
|
||||
route.freeze
|
||||
end
|
||||
|
||||
private
|
||||
def validate_route_conditions(conditions)
|
||||
if method = conditions[:method]
|
||||
[method].flatten.each do |m|
|
||||
if m == :head
|
||||
raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
|
||||
end
|
||||
|
||||
unless HTTP_METHODS.include?(m.to_sym)
|
||||
raise ArgumentError, "Invalid HTTP method specified in route conditions: #{conditions.inspect}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
506
actionpack/lib/action_controller/routing/deprecated_mapper.rb
Normal file
506
actionpack/lib/action_controller/routing/deprecated_mapper.rb
Normal file
@@ -0,0 +1,506 @@
|
||||
require 'active_support/core_ext/object/blank'
|
||||
require 'active_support/core_ext/object/with_options'
|
||||
require 'active_support/core_ext/object/try'
|
||||
|
||||
module ActionController
|
||||
module Routing
|
||||
class DeprecatedMapper #:nodoc:
|
||||
def initialize(set) #:nodoc:
|
||||
# ActiveSupport::Deprecation.warn "You are using the old router DSL which will be removed in Rails 3.1. " <<
|
||||
# "Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/"
|
||||
@set = set
|
||||
end
|
||||
|
||||
def connect(path, options = {})
|
||||
options = options.dup
|
||||
|
||||
if conditions = options.delete(:conditions)
|
||||
conditions = conditions.dup
|
||||
subdomain = conditions.delete(:subdomain)
|
||||
method = [conditions.delete(:method)].flatten.compact
|
||||
method.map! { |m|
|
||||
m = m.to_s.upcase
|
||||
|
||||
if m == "HEAD"
|
||||
raise ArgumentError, "HTTP method HEAD is invalid in route conditions. Rails processes HEAD requests the same as GETs, returning just the response headers"
|
||||
end
|
||||
|
||||
unless HTTP_METHODS.include?(m.downcase.to_sym)
|
||||
raise ArgumentError, "Invalid HTTP method specified in route conditions"
|
||||
end
|
||||
|
||||
m
|
||||
}
|
||||
|
||||
if method.length > 1
|
||||
method = Regexp.union(*method)
|
||||
elsif method.length == 1
|
||||
method = method.first
|
||||
else
|
||||
method = nil
|
||||
end
|
||||
end
|
||||
|
||||
path_prefix = options.delete(:path_prefix)
|
||||
name_prefix = options.delete(:name_prefix)
|
||||
namespace = options.delete(:namespace)
|
||||
|
||||
name = options.delete(:_name)
|
||||
name = "#{name_prefix}#{name}" if name_prefix && name
|
||||
|
||||
requirements = options.delete(:requirements) || {}
|
||||
defaults = options.delete(:defaults) || {}
|
||||
options.each do |k, v|
|
||||
if v.is_a?(Regexp)
|
||||
if value = options.delete(k)
|
||||
requirements[k.to_sym] = value
|
||||
end
|
||||
else
|
||||
value = options.delete(k)
|
||||
defaults[k.to_sym] = value.is_a?(Symbol) ? value : value.to_param
|
||||
end
|
||||
end
|
||||
|
||||
requirements.each do |_, requirement|
|
||||
if requirement.source =~ %r{\A(\\A|\^)|(\\Z|\\z|\$)\Z}
|
||||
raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
|
||||
end
|
||||
if requirement.multiline?
|
||||
raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
if defaults[:controller]
|
||||
defaults[:action] ||= 'index'
|
||||
defaults[:controller] = defaults[:controller].to_s
|
||||
defaults[:controller] = "#{namespace}#{defaults[:controller]}" if namespace
|
||||
end
|
||||
|
||||
if defaults[:action]
|
||||
defaults[:action] = defaults[:action].to_s
|
||||
end
|
||||
|
||||
if path.is_a?(String)
|
||||
path = "#{path_prefix}/#{path}" if path_prefix
|
||||
path = path.gsub('.:format', '(.:format)')
|
||||
path = optionalize_trailing_dynamic_segments(path, requirements, defaults)
|
||||
glob = $1.to_sym if path =~ /\/\*(\w+)$/
|
||||
path = Journey::Router::Utils.normalize_path(path)
|
||||
|
||||
if glob && !defaults[glob].blank?
|
||||
raise ActionController::RoutingError, "paths cannot have non-empty default values"
|
||||
end
|
||||
end
|
||||
|
||||
app = Routing::RouteSet::Dispatcher.new(:defaults => defaults, :glob => glob)
|
||||
|
||||
conditions = {}
|
||||
conditions[:request_method_string] = method if method
|
||||
conditions[:path_info] = path if path
|
||||
conditions[:subdomain] = subdomain if subdomain
|
||||
|
||||
conditions[:required_defaults] = []
|
||||
defaults.each do |key, required_default|
|
||||
next if Regexp === required_default
|
||||
conditions[:required_defaults] << key
|
||||
end
|
||||
|
||||
@set.add_route(app, conditions, requirements, defaults, name)
|
||||
end
|
||||
|
||||
def optionalize_trailing_dynamic_segments(path, requirements, defaults) #:nodoc:
|
||||
path = (path =~ /^\//) ? path.dup : "/#{path}"
|
||||
optional, segments = true, []
|
||||
|
||||
required_segments = requirements.keys
|
||||
required_segments -= defaults.keys.compact
|
||||
|
||||
old_segments = path.split('/')
|
||||
old_segments.shift
|
||||
length = old_segments.length
|
||||
|
||||
old_segments.reverse.each_with_index do |segment, index|
|
||||
required_segments.each do |required|
|
||||
if segment =~ /#{required}/
|
||||
optional = false
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if optional
|
||||
if segment == ":id" && segments.include?(":action")
|
||||
optional = false
|
||||
elsif segment == ":controller" || segment == ":action" || segment == ":id"
|
||||
# Ignore
|
||||
elsif !(segment =~ /^:\w+$/) &&
|
||||
!(segment =~ /^:\w+\(\.:format\)$/)
|
||||
optional = false
|
||||
elsif segment =~ /^:(\w+)$/
|
||||
if defaults.has_key?($1.to_sym)
|
||||
defaults.delete($1.to_sym) if defaults[$1.to_sym].nil?
|
||||
else
|
||||
optional = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if optional && index < length - 1
|
||||
segments.unshift('(/', segment)
|
||||
segments.push(')')
|
||||
elsif optional
|
||||
segments.unshift('/(', segment)
|
||||
segments.push(')')
|
||||
else
|
||||
segments.unshift('/', segment)
|
||||
end
|
||||
end
|
||||
|
||||
segments.join
|
||||
end
|
||||
private :optionalize_trailing_dynamic_segments
|
||||
|
||||
# Creates a named route called "root" for matching the root level request.
|
||||
def root(options = {})
|
||||
if options.is_a?(Symbol)
|
||||
if source_route = @set.named_routes.routes[options]
|
||||
options = source_route.defaults.merge({ :conditions => source_route.conditions })
|
||||
end
|
||||
end
|
||||
named_route("root", '', options)
|
||||
end
|
||||
|
||||
def named_route(name, path, options = {}) #:nodoc:
|
||||
options[:_name] = name
|
||||
connect(path, options)
|
||||
end
|
||||
|
||||
def namespace(name, options = {}, &block)
|
||||
if options[:namespace]
|
||||
with_options({:path_prefix => "#{options.delete(:path_prefix)}/#{name}", :name_prefix => "#{options.delete(:name_prefix)}#{name}_", :namespace => "#{options.delete(:namespace)}#{name}/" }.merge(options), &block)
|
||||
else
|
||||
with_options({:path_prefix => name, :name_prefix => "#{name}_", :namespace => "#{name}/" }.merge(options), &block)
|
||||
end
|
||||
end
|
||||
|
||||
def method_missing(route_name, *args, &proc) #:nodoc:
|
||||
super unless args.length >= 1 && proc.nil?
|
||||
named_route(route_name, *args)
|
||||
end
|
||||
|
||||
INHERITABLE_OPTIONS = :namespace, :shallow
|
||||
|
||||
class Resource #:nodoc:
|
||||
DEFAULT_ACTIONS = :index, :create, :new, :edit, :show, :update, :destroy
|
||||
|
||||
attr_reader :collection_methods, :member_methods, :new_methods
|
||||
attr_reader :path_prefix, :name_prefix, :path_segment
|
||||
attr_reader :plural, :singular
|
||||
attr_reader :options, :defaults
|
||||
|
||||
def initialize(entities, options, defaults)
|
||||
@plural ||= entities
|
||||
@singular ||= options[:singular] || plural.to_s.singularize
|
||||
@path_segment = options.delete(:as) || @plural
|
||||
|
||||
@options = options
|
||||
@defaults = defaults
|
||||
|
||||
arrange_actions
|
||||
add_default_actions
|
||||
set_allowed_actions
|
||||
set_prefixes
|
||||
end
|
||||
|
||||
def controller
|
||||
@controller ||= "#{options[:namespace]}#{(options[:controller] || plural).to_s}"
|
||||
end
|
||||
|
||||
def requirements(with_id = false)
|
||||
@requirements ||= @options[:requirements] || {}
|
||||
@id_requirement ||= { :id => @requirements.delete(:id) || /[^#{Routing::SEPARATORS.join}]+/ }
|
||||
|
||||
with_id ? @requirements.merge(@id_requirement) : @requirements
|
||||
end
|
||||
|
||||
def conditions
|
||||
@conditions ||= @options[:conditions] || {}
|
||||
end
|
||||
|
||||
def path
|
||||
@path ||= "#{path_prefix}/#{path_segment}"
|
||||
end
|
||||
|
||||
def new_path
|
||||
new_action = self.options[:path_names][:new] if self.options[:path_names]
|
||||
new_action ||= self.defaults[:path_names][:new]
|
||||
@new_path ||= "#{path}/#{new_action}"
|
||||
end
|
||||
|
||||
def shallow_path_prefix
|
||||
@shallow_path_prefix ||= @options[:shallow] ? @options[:namespace].try(:sub, /\/$/, '') : path_prefix
|
||||
end
|
||||
|
||||
def member_path
|
||||
@member_path ||= "#{shallow_path_prefix}/#{path_segment}/:id"
|
||||
end
|
||||
|
||||
def nesting_path_prefix
|
||||
@nesting_path_prefix ||= "#{shallow_path_prefix}/#{path_segment}/:#{singular}_id"
|
||||
end
|
||||
|
||||
def shallow_name_prefix
|
||||
@shallow_name_prefix ||= @options[:shallow] ? @options[:namespace].try(:gsub, /\//, '_') : name_prefix
|
||||
end
|
||||
|
||||
def nesting_name_prefix
|
||||
"#{shallow_name_prefix}#{singular}_"
|
||||
end
|
||||
|
||||
def action_separator
|
||||
@action_separator ||= ActionController::Base.resource_action_separator
|
||||
end
|
||||
|
||||
def uncountable?
|
||||
@singular.to_s == @plural.to_s
|
||||
end
|
||||
|
||||
def has_action?(action)
|
||||
!DEFAULT_ACTIONS.include?(action) || action_allowed?(action)
|
||||
end
|
||||
|
||||
protected
|
||||
def arrange_actions
|
||||
@collection_methods = arrange_actions_by_methods(options.delete(:collection))
|
||||
@member_methods = arrange_actions_by_methods(options.delete(:member))
|
||||
@new_methods = arrange_actions_by_methods(options.delete(:new))
|
||||
end
|
||||
|
||||
def add_default_actions
|
||||
add_default_action(member_methods, :get, :edit)
|
||||
add_default_action(new_methods, :get, :new)
|
||||
end
|
||||
|
||||
def set_allowed_actions
|
||||
only, except = @options.values_at(:only, :except)
|
||||
@allowed_actions ||= {}
|
||||
|
||||
if only == :all || except == :none
|
||||
only = nil
|
||||
except = []
|
||||
elsif only == :none || except == :all
|
||||
only = []
|
||||
except = nil
|
||||
end
|
||||
|
||||
if only
|
||||
@allowed_actions[:only] = Array(only).map {|a| a.to_sym }
|
||||
elsif except
|
||||
@allowed_actions[:except] = Array(except).map {|a| a.to_sym }
|
||||
end
|
||||
end
|
||||
|
||||
def action_allowed?(action)
|
||||
only, except = @allowed_actions.values_at(:only, :except)
|
||||
(!only || only.include?(action)) && (!except || !except.include?(action))
|
||||
end
|
||||
|
||||
def set_prefixes
|
||||
@path_prefix = options.delete(:path_prefix)
|
||||
@name_prefix = options.delete(:name_prefix)
|
||||
end
|
||||
|
||||
def arrange_actions_by_methods(actions)
|
||||
(actions || {}).inject({}) do |flipped_hash, (key, value)|
|
||||
(flipped_hash[value] ||= []) << key
|
||||
flipped_hash
|
||||
end
|
||||
end
|
||||
|
||||
def add_default_action(collection, method, action)
|
||||
(collection[method] ||= []).unshift(action)
|
||||
end
|
||||
end
|
||||
|
||||
class SingletonResource < Resource #:nodoc:
|
||||
def initialize(entity, options, defaults)
|
||||
@singular = @plural = entity
|
||||
options[:controller] ||= @singular.to_s.pluralize
|
||||
super
|
||||
end
|
||||
|
||||
alias_method :shallow_path_prefix, :path_prefix
|
||||
alias_method :shallow_name_prefix, :name_prefix
|
||||
alias_method :member_path, :path
|
||||
alias_method :nesting_path_prefix, :path
|
||||
end
|
||||
|
||||
def resources(*entities, &block)
|
||||
options = entities.extract_options!
|
||||
entities.each { |entity| map_resource(entity, options.dup, &block) }
|
||||
end
|
||||
|
||||
def resource(*entities, &block)
|
||||
options = entities.extract_options!
|
||||
entities.each { |entity| map_singleton_resource(entity, options.dup, &block) }
|
||||
end
|
||||
|
||||
private
|
||||
def map_resource(entities, options = {}, &block)
|
||||
resource = Resource.new(entities, options, :path_names => @set.resources_path_names)
|
||||
|
||||
with_options :controller => resource.controller do |map|
|
||||
map_associations(resource, options)
|
||||
|
||||
if block_given?
|
||||
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
|
||||
end
|
||||
|
||||
map_collection_actions(map, resource)
|
||||
map_default_collection_actions(map, resource)
|
||||
map_new_actions(map, resource)
|
||||
map_member_actions(map, resource)
|
||||
end
|
||||
end
|
||||
|
||||
def map_singleton_resource(entities, options = {}, &block)
|
||||
resource = SingletonResource.new(entities, options, :path_names => @set.resources_path_names)
|
||||
|
||||
with_options :controller => resource.controller do |map|
|
||||
map_associations(resource, options)
|
||||
|
||||
if block_given?
|
||||
with_options(options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix), &block)
|
||||
end
|
||||
|
||||
map_collection_actions(map, resource)
|
||||
map_new_actions(map, resource)
|
||||
map_member_actions(map, resource)
|
||||
map_default_singleton_actions(map, resource)
|
||||
end
|
||||
end
|
||||
|
||||
def map_associations(resource, options)
|
||||
map_has_many_associations(resource, options.delete(:has_many), options) if options[:has_many]
|
||||
|
||||
path_prefix = "#{options.delete(:path_prefix)}#{resource.nesting_path_prefix}"
|
||||
name_prefix = "#{options.delete(:name_prefix)}#{resource.nesting_name_prefix}"
|
||||
|
||||
Array(options[:has_one]).each do |association|
|
||||
resource(association, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => path_prefix, :name_prefix => name_prefix))
|
||||
end
|
||||
end
|
||||
|
||||
def map_has_many_associations(resource, associations, options)
|
||||
case associations
|
||||
when Hash
|
||||
associations.each do |association,has_many|
|
||||
map_has_many_associations(resource, association, options.merge(:has_many => has_many))
|
||||
end
|
||||
when Array
|
||||
associations.each do |association|
|
||||
map_has_many_associations(resource, association, options)
|
||||
end
|
||||
when Symbol, String
|
||||
resources(associations, options.slice(*INHERITABLE_OPTIONS).merge(:path_prefix => resource.nesting_path_prefix, :name_prefix => resource.nesting_name_prefix, :has_many => options[:has_many]))
|
||||
else
|
||||
end
|
||||
end
|
||||
|
||||
def map_collection_actions(map, resource)
|
||||
resource.collection_methods.each do |method, actions|
|
||||
actions.each do |action|
|
||||
[method].flatten.each do |m|
|
||||
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
|
||||
action_path ||= action
|
||||
|
||||
map_resource_routes(map, resource, action, "#{resource.path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.name_prefix}#{resource.plural}", m)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def map_default_collection_actions(map, resource)
|
||||
index_route_name = "#{resource.name_prefix}#{resource.plural}"
|
||||
|
||||
if resource.uncountable?
|
||||
index_route_name << "_index"
|
||||
end
|
||||
|
||||
map_resource_routes(map, resource, :index, resource.path, index_route_name)
|
||||
map_resource_routes(map, resource, :create, resource.path, index_route_name)
|
||||
end
|
||||
|
||||
def map_default_singleton_actions(map, resource)
|
||||
map_resource_routes(map, resource, :create, resource.path, "#{resource.shallow_name_prefix}#{resource.singular}")
|
||||
end
|
||||
|
||||
def map_new_actions(map, resource)
|
||||
resource.new_methods.each do |method, actions|
|
||||
actions.each do |action|
|
||||
route_path = resource.new_path
|
||||
route_name = "new_#{resource.name_prefix}#{resource.singular}"
|
||||
|
||||
unless action == :new
|
||||
route_path = "#{route_path}#{resource.action_separator}#{action}"
|
||||
route_name = "#{action}_#{route_name}"
|
||||
end
|
||||
|
||||
map_resource_routes(map, resource, action, route_path, route_name, method)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def map_member_actions(map, resource)
|
||||
resource.member_methods.each do |method, actions|
|
||||
actions.each do |action|
|
||||
[method].flatten.each do |m|
|
||||
action_path = resource.options[:path_names][action] if resource.options[:path_names].is_a?(Hash)
|
||||
action_path ||= @set.resources_path_names[action] || action
|
||||
|
||||
map_resource_routes(map, resource, action, "#{resource.member_path}#{resource.action_separator}#{action_path}", "#{action}_#{resource.shallow_name_prefix}#{resource.singular}", m, { :force_id => true })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
route_path = "#{resource.shallow_name_prefix}#{resource.singular}"
|
||||
map_resource_routes(map, resource, :show, resource.member_path, route_path)
|
||||
map_resource_routes(map, resource, :update, resource.member_path, route_path)
|
||||
map_resource_routes(map, resource, :destroy, resource.member_path, route_path)
|
||||
end
|
||||
|
||||
def map_resource_routes(map, resource, action, route_path, route_name = nil, method = nil, resource_options = {} )
|
||||
if resource.has_action?(action)
|
||||
action_options = action_options_for(action, resource, method, resource_options)
|
||||
formatted_route_path = "#{route_path}.:format"
|
||||
|
||||
if route_name && @set.named_routes[route_name.to_sym].nil?
|
||||
map.named_route(route_name, formatted_route_path, action_options)
|
||||
else
|
||||
map.connect(formatted_route_path, action_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def add_conditions_for(conditions, method)
|
||||
{:conditions => conditions.dup}.tap do |options|
|
||||
options[:conditions][:method] = method unless method == :any
|
||||
end
|
||||
end
|
||||
|
||||
def action_options_for(action, resource, method = nil, resource_options = {})
|
||||
default_options = { :action => action.to_s }
|
||||
require_id = !resource.kind_of?(SingletonResource)
|
||||
force_id = resource_options[:force_id] && !resource.kind_of?(SingletonResource)
|
||||
|
||||
case default_options[:action]
|
||||
when "index", "new"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements)
|
||||
when "create"; default_options.merge(add_conditions_for(resource.conditions, method || :post)).merge(resource.requirements)
|
||||
when "show", "edit"; default_options.merge(add_conditions_for(resource.conditions, method || :get)).merge(resource.requirements(require_id))
|
||||
when "update"; default_options.merge(add_conditions_for(resource.conditions, method || :put)).merge(resource.requirements(require_id))
|
||||
when "destroy"; default_options.merge(add_conditions_for(resource.conditions, method || :delete)).merge(resource.requirements(require_id))
|
||||
else default_options.merge(add_conditions_for(resource.conditions, method)).merge(resource.requirements(force_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
240
actionpack/lib/action_controller/routing/inspector.rb
Normal file
240
actionpack/lib/action_controller/routing/inspector.rb
Normal file
@@ -0,0 +1,240 @@
|
||||
require 'delegate'
|
||||
require 'active_support/core_ext/string/strip'
|
||||
|
||||
module ActionController
|
||||
module Routing
|
||||
class RouteWrapper < SimpleDelegator
|
||||
def endpoint
|
||||
rack_app ? rack_app.inspect : "#{controller}##{action}"
|
||||
end
|
||||
|
||||
def constraints
|
||||
requirements.except(:controller, :action)
|
||||
end
|
||||
|
||||
def rack_app(app = self.app)
|
||||
@rack_app ||= begin
|
||||
class_name = app.class.name.to_s
|
||||
if class_name == "ActionController::Routing::Mapper::Constraints"
|
||||
rack_app(app.app)
|
||||
elsif ActionController::Routing::Redirect === app || class_name !~ /^ActionController::Routing/
|
||||
app
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def verb
|
||||
super.source.gsub(/[$^]/, '')
|
||||
end
|
||||
|
||||
def path
|
||||
super.spec.to_s
|
||||
end
|
||||
|
||||
def name
|
||||
super.to_s
|
||||
end
|
||||
|
||||
def regexp
|
||||
__getobj__.path.to_regexp
|
||||
end
|
||||
|
||||
def json_regexp
|
||||
str = regexp.inspect.
|
||||
sub('\\A' , '^').
|
||||
sub('\\Z' , '$').
|
||||
sub('\\z' , '$').
|
||||
sub(/^\// , '').
|
||||
sub(/\/[a-z]*$/ , '').
|
||||
gsub(/\(\?#.+\)/ , '').
|
||||
gsub(/\(\?-\w+:/ , '(').
|
||||
gsub(/\s/ , '')
|
||||
Regexp.new(str).source
|
||||
end
|
||||
|
||||
def reqs
|
||||
@reqs ||= begin
|
||||
reqs = endpoint
|
||||
reqs += " #{constraints.to_s}" unless constraints.empty?
|
||||
reqs
|
||||
end
|
||||
end
|
||||
|
||||
def controller
|
||||
requirements[:controller] || ':controller'
|
||||
end
|
||||
|
||||
def action
|
||||
requirements[:action] || ':action'
|
||||
end
|
||||
|
||||
def internal?
|
||||
controller =~ %r{\Arails/(info|welcome)}
|
||||
end
|
||||
|
||||
def engine?
|
||||
rack_app && rack_app.respond_to?(:routes)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# This class is just used for displaying route information when someone
|
||||
# executes `rake routes` or looks at the RoutingError page.
|
||||
# People should not use this class.
|
||||
class RoutesInspector # :nodoc:
|
||||
def initialize(routes)
|
||||
@engines = {}
|
||||
@routes = routes
|
||||
end
|
||||
|
||||
def format(formatter, filter = nil)
|
||||
routes_to_display = filter_routes(filter)
|
||||
|
||||
routes = collect_routes(routes_to_display)
|
||||
|
||||
if routes.none?
|
||||
formatter.no_routes
|
||||
return formatter.result
|
||||
end
|
||||
|
||||
formatter.header routes
|
||||
formatter.section routes
|
||||
|
||||
@engines.each do |name, engine_routes|
|
||||
formatter.section_title "Routes for #{name}"
|
||||
formatter.section engine_routes
|
||||
end
|
||||
|
||||
formatter.result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filter_routes(filter)
|
||||
if filter
|
||||
@routes.select { |route| route.defaults[:controller] == filter }
|
||||
else
|
||||
@routes
|
||||
end
|
||||
end
|
||||
|
||||
def collect_routes(routes)
|
||||
routes.collect do |route|
|
||||
RouteWrapper.new(route)
|
||||
end.reject do |route|
|
||||
route.internal?
|
||||
end.collect do |route|
|
||||
collect_engine_routes(route)
|
||||
|
||||
{ :name => route.name,
|
||||
:verb => route.verb,
|
||||
:path => route.path,
|
||||
:reqs => route.reqs,
|
||||
:regexp => route.json_regexp }
|
||||
end
|
||||
end
|
||||
|
||||
def collect_engine_routes(route)
|
||||
name = route.endpoint
|
||||
return unless route.engine?
|
||||
return if @engines[name]
|
||||
|
||||
routes = route.rack_app.routes
|
||||
if routes.is_a?(ActionController::Routing::RouteSet)
|
||||
@engines[name] = collect_routes(routes.routes)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class ConsoleFormatter
|
||||
def initialize
|
||||
@buffer = []
|
||||
end
|
||||
|
||||
def result
|
||||
@buffer.join("\n")
|
||||
end
|
||||
|
||||
def section_title(title)
|
||||
@buffer << "\n#{title}:"
|
||||
end
|
||||
|
||||
def section(routes)
|
||||
@buffer << draw_section(routes)
|
||||
end
|
||||
|
||||
def header(routes)
|
||||
@buffer << draw_header(routes)
|
||||
end
|
||||
|
||||
def no_routes
|
||||
@buffer << <<-MESSAGE.strip_heredoc
|
||||
You don't have any routes defined!
|
||||
|
||||
Please add some routes in config/routes.rb.
|
||||
|
||||
For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
private
|
||||
def draw_section(routes)
|
||||
name_width, verb_width, path_width = widths(routes)
|
||||
|
||||
routes.map do |r|
|
||||
"#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
|
||||
end
|
||||
end
|
||||
|
||||
def draw_header(routes)
|
||||
name_width, verb_width, path_width = widths(routes)
|
||||
|
||||
"#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
|
||||
end
|
||||
|
||||
def widths(routes)
|
||||
[routes.map { |r| r[:name].length }.max,
|
||||
routes.map { |r| r[:verb].length }.max,
|
||||
routes.map { |r| r[:path].length }.max]
|
||||
end
|
||||
end
|
||||
|
||||
class HtmlTableFormatter
|
||||
def initialize(view)
|
||||
@view = view
|
||||
@buffer = []
|
||||
end
|
||||
|
||||
def section_title(title)
|
||||
@buffer << %(<tr><th colspan="4">#{title}</th></tr>)
|
||||
end
|
||||
|
||||
def section(routes)
|
||||
@buffer << @view.render(:partial => "routes/route", :collection => routes)
|
||||
end
|
||||
|
||||
# the header is part of the HTML page, so we don't construct it here.
|
||||
def header(routes)
|
||||
end
|
||||
|
||||
def no_routes
|
||||
@buffer << <<-MESSAGE.strip_heredoc
|
||||
<p>You don't have any routes defined!</p>
|
||||
<ul>
|
||||
<li>Please add some routes in <tt>config/routes.rb</tt>.</li>
|
||||
<li>
|
||||
For more information about routes, please see the Rails guide
|
||||
<a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
|
||||
</li>
|
||||
</ul>
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
def result
|
||||
@view.raw @view.render(:layout => "routes/table") {
|
||||
@view.raw @buffer.join("\n")
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
1599
actionpack/lib/action_controller/routing/mapper.rb
Normal file
1599
actionpack/lib/action_controller/routing/mapper.rb
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,130 +0,0 @@
|
||||
module ActionController
|
||||
module Routing
|
||||
# Much of the slow performance from routes comes from the
|
||||
# complexity of expiry, <tt>:requirements</tt> matching, defaults providing
|
||||
# and figuring out which url pattern to use. With named routes
|
||||
# we can avoid the expense of finding the right route. So if
|
||||
# they've provided the right number of arguments, and have no
|
||||
# <tt>:requirements</tt>, we can just build up a string and return it.
|
||||
#
|
||||
# To support building optimisations for other common cases, the
|
||||
# generation code is separated into several classes
|
||||
module Optimisation
|
||||
def generate_optimisation_block(route, kind)
|
||||
return "" unless route.optimise?
|
||||
OPTIMISERS.inject("") do |memo, klazz|
|
||||
memo << klazz.new(route, kind).source_code
|
||||
memo
|
||||
end
|
||||
end
|
||||
|
||||
class Optimiser
|
||||
attr_reader :route, :kind
|
||||
GLOBAL_GUARD_CONDITIONS = [
|
||||
"(!defined?(default_url_options) || default_url_options.blank?)",
|
||||
"(!defined?(controller.default_url_options) || controller.default_url_options.blank?)",
|
||||
"defined?(request)",
|
||||
"request"
|
||||
]
|
||||
|
||||
def initialize(route, kind)
|
||||
@route = route
|
||||
@kind = kind
|
||||
end
|
||||
|
||||
def guard_conditions
|
||||
["false"]
|
||||
end
|
||||
|
||||
def generation_code
|
||||
'nil'
|
||||
end
|
||||
|
||||
def source_code
|
||||
if applicable?
|
||||
guard_condition = (GLOBAL_GUARD_CONDITIONS + guard_conditions).join(" && ")
|
||||
"return #{generation_code} if #{guard_condition}\n"
|
||||
else
|
||||
"\n"
|
||||
end
|
||||
end
|
||||
|
||||
# Temporarily disabled <tt>:url</tt> optimisation pending proper solution to
|
||||
# Issues around request.host etc.
|
||||
def applicable?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
# Given a route
|
||||
#
|
||||
# map.person '/people/:id'
|
||||
#
|
||||
# If the user calls <tt>person_url(@person)</tt>, we can simply
|
||||
# return a string like "/people/#{@person.to_param}"
|
||||
# rather than triggering the expensive logic in +url_for+.
|
||||
class PositionalArguments < Optimiser
|
||||
def guard_conditions
|
||||
number_of_arguments = route.required_segment_keys.size
|
||||
# if they're using foo_url(:id=>2) it's one
|
||||
# argument, but we don't want to generate /foos/id2
|
||||
if number_of_arguments == 1
|
||||
["args.size == 1", "!args.first.is_a?(Hash)"]
|
||||
else
|
||||
["args.size == #{number_of_arguments}"]
|
||||
end
|
||||
end
|
||||
|
||||
def generation_code
|
||||
elements = []
|
||||
idx = 0
|
||||
|
||||
if kind == :url
|
||||
elements << '#{request.protocol}'
|
||||
elements << '#{request.host_with_port}'
|
||||
end
|
||||
|
||||
elements << '#{ActionController::Base.relative_url_root if ActionController::Base.relative_url_root}'
|
||||
|
||||
# The last entry in <tt>route.segments</tt> appears to *always* be a
|
||||
# 'divider segment' for '/' but we have assertions to ensure that
|
||||
# we don't include the trailing slashes, so skip them.
|
||||
(route.segments.size == 1 ? route.segments : route.segments[0..-2]).each do |segment|
|
||||
if segment.is_a?(DynamicSegment)
|
||||
elements << segment.interpolation_chunk("args[#{idx}].to_param")
|
||||
idx += 1
|
||||
else
|
||||
elements << segment.interpolation_chunk
|
||||
end
|
||||
end
|
||||
%("#{elements * ''}")
|
||||
end
|
||||
end
|
||||
|
||||
# This case is mostly the same as the positional arguments case
|
||||
# above, but it supports additional query parameters as the last
|
||||
# argument
|
||||
class PositionalArgumentsWithAdditionalParams < PositionalArguments
|
||||
def guard_conditions
|
||||
["args.size == #{route.segment_keys.size + 1}"] +
|
||||
UrlRewriter::RESERVED_OPTIONS.collect{ |key| "!args.last.has_key?(:#{key})" }
|
||||
end
|
||||
|
||||
# This case uses almost the same code as positional arguments,
|
||||
# but add a question mark and args.last.to_query on the end,
|
||||
# unless the last arg is empty
|
||||
def generation_code
|
||||
super.insert(-2, '#{\'?\' + args.last.to_query unless args.last.empty?}')
|
||||
end
|
||||
|
||||
# To avoid generating "http://localhost/?host=foo.example.com" we
|
||||
# can't use this optimisation on routes without any segments
|
||||
def applicable?
|
||||
super && route.segment_keys.size > 0
|
||||
end
|
||||
end
|
||||
|
||||
OPTIMISERS = [PositionalArguments, PositionalArgumentsWithAdditionalParams]
|
||||
end
|
||||
end
|
||||
end
|
||||
183
actionpack/lib/action_controller/routing/polymorphic_routes.rb
Normal file
183
actionpack/lib/action_controller/routing/polymorphic_routes.rb
Normal file
@@ -0,0 +1,183 @@
|
||||
module ActionController
|
||||
module Routing
|
||||
# Polymorphic URL helpers are methods for smart resolution to a named route call when
|
||||
# given an Active Record model instance. They are to be used in combination with
|
||||
# ActionController::Resources.
|
||||
#
|
||||
# These methods are useful when you want to generate correct URL or path to a RESTful
|
||||
# resource without having to know the exact type of the record in question.
|
||||
#
|
||||
# Nested resources and/or namespaces are also supported, as illustrated in the example:
|
||||
#
|
||||
# polymorphic_url([:admin, @article, @comment])
|
||||
#
|
||||
# results in:
|
||||
#
|
||||
# admin_article_comment_url(@article, @comment)
|
||||
#
|
||||
# == Usage within the framework
|
||||
#
|
||||
# Polymorphic URL helpers are used in a number of places throughout the \Rails framework:
|
||||
#
|
||||
# * <tt>url_for</tt>, so you can use it with a record as the argument, e.g.
|
||||
# <tt>url_for(@article)</tt>;
|
||||
# * ActionView::Helpers::FormHelper uses <tt>polymorphic_path</tt>, so you can write
|
||||
# <tt>form_for(@article)</tt> without having to specify <tt>:url</tt> parameter for the form
|
||||
# action;
|
||||
# * <tt>redirect_to</tt> (which, in fact, uses <tt>url_for</tt>) so you can write
|
||||
# <tt>redirect_to(post)</tt> in your controllers;
|
||||
# * ActionView::Helpers::AtomFeedHelper, so you don't have to explicitly specify URLs
|
||||
# for feed entries.
|
||||
#
|
||||
# == Prefixed polymorphic helpers
|
||||
#
|
||||
# In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
|
||||
# number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt>
|
||||
# in options. Those are:
|
||||
#
|
||||
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
|
||||
# * <tt>new_polymorphic_url</tt>, <tt>new_polymorphic_path</tt>
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# edit_polymorphic_path(@post) # => "/posts/1/edit"
|
||||
# polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
|
||||
module PolymorphicRoutes
|
||||
# Constructs a call to a named RESTful route for the given record and returns the
|
||||
# resulting URL string. For example:
|
||||
#
|
||||
# # calls post_url(post)
|
||||
# polymorphic_url(post) # => "http://example.com/posts/1"
|
||||
# polymorphic_url([blog, post]) # => "http://example.com/blogs/1/posts/1"
|
||||
# polymorphic_url([:admin, blog, post]) # => "http://example.com/admin/blogs/1/posts/1"
|
||||
# polymorphic_url([user, :blog, post]) # => "http://example.com/users/1/blog/posts/1"
|
||||
# polymorphic_url(Comment) # => "http://example.com/comments"
|
||||
#
|
||||
# ==== Options
|
||||
#
|
||||
# * <tt>:action</tt> - Specifies the action prefix for the named route:
|
||||
# <tt>:new</tt> or <tt>:edit</tt>. Default is no prefix.
|
||||
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
|
||||
# Default is <tt>:url</tt>.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# # an Article record
|
||||
# polymorphic_url(record) # same as article_url(record)
|
||||
#
|
||||
# # a Comment record
|
||||
# polymorphic_url(record) # same as comment_url(record)
|
||||
#
|
||||
# # it recognizes new records and maps to the collection
|
||||
# record = Comment.new
|
||||
# polymorphic_url(record) # same as comments_url()
|
||||
#
|
||||
# # the class of a record will also map to the collection
|
||||
# polymorphic_url(Comment) # same as comments_url()
|
||||
#
|
||||
def polymorphic_url(record_or_hash_or_array, options = {})
|
||||
if record_or_hash_or_array.kind_of?(Array)
|
||||
record_or_hash_or_array = record_or_hash_or_array.compact
|
||||
record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
|
||||
end
|
||||
|
||||
record = extract_record(record_or_hash_or_array)
|
||||
record = record.to_model if record.respond_to?(:to_model)
|
||||
|
||||
args = Array === record_or_hash_or_array ?
|
||||
record_or_hash_or_array.dup :
|
||||
[ record_or_hash_or_array ]
|
||||
|
||||
inflection = if options[:action] && options[:action].to_s == "new"
|
||||
args.pop
|
||||
:singular
|
||||
elsif record.respond_to?(:new_record?) && record.new_record?
|
||||
args.pop
|
||||
:plural
|
||||
elsif record.is_a?(Class)
|
||||
args.pop
|
||||
:plural
|
||||
else
|
||||
:singular
|
||||
end
|
||||
|
||||
args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
|
||||
named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
|
||||
|
||||
url_options = options.except(:action, :routing_type)
|
||||
unless url_options.empty?
|
||||
args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
|
||||
end
|
||||
|
||||
send(named_route, *args)
|
||||
end
|
||||
|
||||
# Returns the path component of a URL for the given record. It uses
|
||||
# <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>.
|
||||
def polymorphic_path(record_or_hash_or_array, options = {})
|
||||
polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path))
|
||||
end
|
||||
|
||||
%w(edit new).each do |action|
|
||||
module_eval <<-EOT, __FILE__, __LINE__ + 1
|
||||
def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
|
||||
polymorphic_url( # polymorphic_url(
|
||||
record_or_hash, # record_or_hash,
|
||||
options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
|
||||
end # end
|
||||
#
|
||||
def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
|
||||
polymorphic_url( # polymorphic_url(
|
||||
record_or_hash, # record_or_hash,
|
||||
options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
|
||||
end # end
|
||||
EOT
|
||||
end
|
||||
|
||||
private
|
||||
def action_prefix(options)
|
||||
options[:action] ? "#{options[:action]}_" : ''
|
||||
end
|
||||
|
||||
def routing_type(options)
|
||||
options[:routing_type] || :url
|
||||
end
|
||||
|
||||
def build_named_route_call(records, inflection, options = {})
|
||||
unless records.is_a?(Array)
|
||||
record = extract_record(records)
|
||||
route = []
|
||||
else
|
||||
record = records.pop
|
||||
route = records.map do |parent|
|
||||
if parent.is_a?(Symbol) || parent.is_a?(String)
|
||||
parent
|
||||
else
|
||||
RecordIdentifier.__send__("plural_class_name", parent).singularize
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if record.is_a?(Symbol) || record.is_a?(String)
|
||||
route << record
|
||||
else
|
||||
route << RecordIdentifier.__send__("plural_class_name", record)
|
||||
route = [route.join("_").singularize] if inflection == :singular
|
||||
end
|
||||
|
||||
route << routing_type(options)
|
||||
|
||||
action_prefix(options) + route.join("_")
|
||||
end
|
||||
|
||||
def extract_record(record_or_hash_or_array)
|
||||
case record_or_hash_or_array
|
||||
when Array; record_or_hash_or_array.last
|
||||
when Hash; record_or_hash_or_array[:id]
|
||||
else record_or_hash_or_array
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
module ActionController
|
||||
module Routing
|
||||
# BEFORE: 0.191446860631307 ms/url
|
||||
# AFTER: 0.029847304022858 ms/url
|
||||
# Speed up: 6.4 times
|
||||
#
|
||||
# Route recognition is slow due to one-by-one iterating over
|
||||
# a whole routeset (each map.resources generates at least 14 routes)
|
||||
# and matching weird regexps on each step.
|
||||
#
|
||||
# We optimize this by skipping all URI segments that 100% sure can't
|
||||
# be matched, moving deeper in a tree of routes (where node == segment)
|
||||
# until first possible match is accured. In such case, we start walking
|
||||
# a flat list of routes, matching them with accurate matcher.
|
||||
# So, first step: search a segment tree for the first relevant index.
|
||||
# Second step: iterate routes starting with that index.
|
||||
#
|
||||
# How tree is walked? We can do a recursive tests, but it's smarter:
|
||||
# We just create a tree of if-s and elsif-s matching segments.
|
||||
#
|
||||
# We have segments of 3 flavors:
|
||||
# 1) nil (no segment, route finished)
|
||||
# 2) const-dot-dynamic (like "/posts.:xml", "/preview.:size.jpg")
|
||||
# 3) const (like "/posts", "/comments")
|
||||
# 4) dynamic ("/:id", "file.:size.:extension")
|
||||
#
|
||||
# We split incoming string into segments and iterate over them.
|
||||
# When segment is nil, we drop immediately, on a current node index.
|
||||
# When segment is equal to some const, we step into branch.
|
||||
# If none constants matched, we step into 'dynamic' branch (it's a last).
|
||||
# If we can't match anything, we drop to last index on a level.
|
||||
#
|
||||
# Note: we maintain the original routes order, so we finish building
|
||||
# steps on a first dynamic segment.
|
||||
#
|
||||
#
|
||||
# Example. Given the routes:
|
||||
# 0 /posts/
|
||||
# 1 /posts/:id
|
||||
# 2 /posts/:id/comments
|
||||
# 3 /posts/blah
|
||||
# 4 /users/
|
||||
# 5 /users/:id
|
||||
# 6 /users/:id/profile
|
||||
#
|
||||
# request_uri = /users/123
|
||||
#
|
||||
# There will be only 4 iterations:
|
||||
# 1) segm test for /posts prefix, skip all /posts/* routes
|
||||
# 2) segm test for /users/
|
||||
# 3) segm test for /users/:id
|
||||
# (jump to list index = 5)
|
||||
# 4) full test for /users/:id => here we are!
|
||||
class RouteSet
|
||||
def recognize_path(path, environment={})
|
||||
result = recognize_optimized(path, environment) and return result
|
||||
|
||||
# Route was not recognized. Try to find out why (maybe wrong verb).
|
||||
allows = HTTP_METHODS.select { |verb| routes.find { |r| r.recognize(path, environment.merge(:method => verb)) } }
|
||||
|
||||
if environment[:method] && !HTTP_METHODS.include?(environment[:method])
|
||||
raise NotImplemented.new(*allows)
|
||||
elsif !allows.empty?
|
||||
raise MethodNotAllowed.new(*allows)
|
||||
else
|
||||
raise RoutingError, "No route matches #{path.inspect} with #{environment.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def segment_tree(routes)
|
||||
tree = [0]
|
||||
|
||||
i = -1
|
||||
routes.each do |route|
|
||||
i += 1
|
||||
# not fast, but runs only once
|
||||
segments = to_plain_segments(route.segments.inject("") { |str,s| str << s.to_s })
|
||||
|
||||
node = tree
|
||||
segments.each do |seg|
|
||||
seg = :dynamic if seg && seg[0] == ?:
|
||||
node << [seg, [i]] if node.empty? || node[node.size - 1][0] != seg
|
||||
node = node[node.size - 1][1]
|
||||
end
|
||||
end
|
||||
tree
|
||||
end
|
||||
|
||||
def generate_code(list, padding=' ', level = 0)
|
||||
# a digit
|
||||
return padding + "#{list[0]}\n" if list.size == 1 && !(Array === list[0])
|
||||
|
||||
body = padding + "(seg = segments[#{level}]; \n"
|
||||
|
||||
i = 0
|
||||
was_nil = false
|
||||
list.each do |item|
|
||||
if Array === item
|
||||
i += 1
|
||||
start = (i == 1)
|
||||
tag, sub = item
|
||||
if tag == :dynamic
|
||||
body += padding + "#{start ? 'if' : 'elsif'} true\n"
|
||||
body += generate_code(sub, padding + " ", level + 1)
|
||||
break
|
||||
elsif tag == nil && !was_nil
|
||||
was_nil = true
|
||||
body += padding + "#{start ? 'if' : 'elsif'} seg.nil?\n"
|
||||
body += generate_code(sub, padding + " ", level + 1)
|
||||
else
|
||||
body += padding + "#{start ? 'if' : 'elsif'} seg == '#{tag}'\n"
|
||||
body += generate_code(sub, padding + " ", level + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
body += padding + "else\n"
|
||||
body += padding + " #{list[0]}\n"
|
||||
body += padding + "end)\n"
|
||||
body
|
||||
end
|
||||
|
||||
# this must be really fast
|
||||
def to_plain_segments(str)
|
||||
str = str.dup
|
||||
str.sub!(/^\/+/,'')
|
||||
str.sub!(/\/+$/,'')
|
||||
segments = str.split(/\.[^\/]+\/+|\/+|\.[^\/]+\Z/) # cut off ".format" also
|
||||
segments << nil
|
||||
segments
|
||||
end
|
||||
|
||||
private
|
||||
def write_recognize_optimized!
|
||||
tree = segment_tree(routes)
|
||||
body = generate_code(tree)
|
||||
|
||||
remove_recognize_optimized!
|
||||
|
||||
instance_eval %{
|
||||
def recognize_optimized(path, env)
|
||||
segments = to_plain_segments(path)
|
||||
index = #{body}
|
||||
return nil unless index
|
||||
while index < routes.size
|
||||
result = routes[index].recognize(path, env) and return result
|
||||
index += 1
|
||||
end
|
||||
nil
|
||||
end
|
||||
}, '(recognize_optimized)', 1
|
||||
end
|
||||
|
||||
def clear_recognize_optimized!
|
||||
remove_recognize_optimized!
|
||||
write_recognize_optimized!
|
||||
end
|
||||
|
||||
def remove_recognize_optimized!
|
||||
if respond_to?(:recognize_optimized)
|
||||
class << self
|
||||
remove_method :recognize_optimized
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
147
actionpack/lib/action_controller/routing/redirection.rb
Normal file
147
actionpack/lib/action_controller/routing/redirection.rb
Normal file
@@ -0,0 +1,147 @@
|
||||
require 'active_support/core_ext/uri'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
require 'rack/utils'
|
||||
require 'action_controller/metal/exceptions'
|
||||
|
||||
module ActionController
|
||||
module Routing
|
||||
class Redirect # :nodoc:
|
||||
attr_reader :status, :block
|
||||
|
||||
def initialize(status, block)
|
||||
@status = status
|
||||
@block = block
|
||||
end
|
||||
|
||||
def call(env)
|
||||
req = Request.new(env)
|
||||
|
||||
# If any of the path parameters has a invalid encoding then
|
||||
# raise since it's likely to trigger errors further on.
|
||||
req.symbolized_path_parameters.each do |key, value|
|
||||
unless value.valid_encoding?
|
||||
raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
|
||||
end
|
||||
end
|
||||
|
||||
uri = URI.parse(path(req.symbolized_path_parameters, req))
|
||||
uri.scheme ||= req.scheme
|
||||
uri.host ||= req.host
|
||||
uri.port ||= req.port unless req.standard_port?
|
||||
|
||||
body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
|
||||
|
||||
headers = {
|
||||
'Location' => uri.to_s,
|
||||
'Content-Type' => 'text/html',
|
||||
'Content-Length' => body.length.to_s
|
||||
}
|
||||
|
||||
[ status, headers, [body] ]
|
||||
end
|
||||
|
||||
def path(params, request)
|
||||
block.call params, request
|
||||
end
|
||||
|
||||
def inspect
|
||||
"redirect(#{status})"
|
||||
end
|
||||
end
|
||||
|
||||
class PathRedirect < Redirect
|
||||
def path(params, request)
|
||||
(params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params))
|
||||
end
|
||||
|
||||
def inspect
|
||||
"redirect(#{status}, #{block})"
|
||||
end
|
||||
|
||||
private
|
||||
def escape(params)
|
||||
Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
|
||||
end
|
||||
end
|
||||
|
||||
class OptionRedirect < Redirect # :nodoc:
|
||||
alias :options :block
|
||||
|
||||
def path(params, request)
|
||||
url_options = {
|
||||
:protocol => request.protocol,
|
||||
:host => request.host,
|
||||
:port => request.optional_port,
|
||||
:path => request.path,
|
||||
:params => request.query_parameters
|
||||
}.merge! options
|
||||
|
||||
if !params.empty? && url_options[:path].match(/%\{\w*\}/)
|
||||
url_options[:path] = (url_options[:path] % escape_path(params))
|
||||
end
|
||||
|
||||
ActionController::Http::URL.url_for url_options
|
||||
end
|
||||
|
||||
def inspect
|
||||
"redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
|
||||
end
|
||||
|
||||
private
|
||||
def escape_path(params)
|
||||
Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }]
|
||||
end
|
||||
end
|
||||
|
||||
module Redirection
|
||||
|
||||
# Redirect any path to another path:
|
||||
#
|
||||
# get "/stories" => redirect("/posts")
|
||||
#
|
||||
# You can also use interpolation in the supplied redirect argument:
|
||||
#
|
||||
# get 'docs/:article', to: redirect('/wiki/%{article}')
|
||||
#
|
||||
# Alternatively you can use one of the other syntaxes:
|
||||
#
|
||||
# The block version of redirect allows for the easy encapsulation of any logic associated with
|
||||
# the redirect in question. Either the params and request are supplied as arguments, or just
|
||||
# params, depending of how many arguments your block accepts. A string is required as a
|
||||
# return value.
|
||||
#
|
||||
# get 'jokes/:number', to: redirect { |params, request|
|
||||
# path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
|
||||
# "http://#{request.host_with_port}/#{path}"
|
||||
# }
|
||||
#
|
||||
# Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
|
||||
# the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
|
||||
#
|
||||
# The options version of redirect allows you to supply only the parts of the url which need
|
||||
# to change, it also supports interpolation of the path similar to the first example.
|
||||
#
|
||||
# get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
|
||||
# get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
|
||||
#
|
||||
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
|
||||
# common redirect routes. The call method must accept two arguments, params and request, and return
|
||||
# a string.
|
||||
#
|
||||
# get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
|
||||
#
|
||||
def redirect(*args, &block)
|
||||
options = args.extract_options!
|
||||
status = options.delete(:status) || 301
|
||||
path = args.shift
|
||||
|
||||
return OptionRedirect.new(status, options) if options.any?
|
||||
return PathRedirect.new(status, path) if String === path
|
||||
|
||||
block = path if path.respond_to? :call
|
||||
raise ArgumentError, "redirection argument not supported" unless block
|
||||
Redirect.new status, block
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,269 +0,0 @@
|
||||
module ActionController
|
||||
module Routing
|
||||
class Route #:nodoc:
|
||||
attr_accessor :segments, :requirements, :conditions, :optimise
|
||||
|
||||
def initialize(segments = [], requirements = {}, conditions = {})
|
||||
@segments = segments
|
||||
@requirements = requirements
|
||||
@conditions = conditions
|
||||
|
||||
if !significant_keys.include?(:action) && !requirements[:action]
|
||||
@requirements[:action] = "index"
|
||||
@significant_keys << :action
|
||||
end
|
||||
|
||||
# Routes cannot use the current string interpolation method
|
||||
# if there are user-supplied <tt>:requirements</tt> as the interpolation
|
||||
# code won't raise RoutingErrors when generating
|
||||
has_requirements = @segments.detect { |segment| segment.respond_to?(:regexp) && segment.regexp }
|
||||
if has_requirements || @requirements.keys.to_set != Routing::ALLOWED_REQUIREMENTS_FOR_OPTIMISATION
|
||||
@optimise = false
|
||||
else
|
||||
@optimise = true
|
||||
end
|
||||
end
|
||||
|
||||
# Indicates whether the routes should be optimised with the string interpolation
|
||||
# version of the named routes methods.
|
||||
def optimise?
|
||||
@optimise && ActionController::Base::optimise_named_routes
|
||||
end
|
||||
|
||||
def segment_keys
|
||||
segments.collect do |segment|
|
||||
segment.key if segment.respond_to? :key
|
||||
end.compact
|
||||
end
|
||||
|
||||
def required_segment_keys
|
||||
required_segments = segments.select {|seg| (!seg.optional? && !seg.is_a?(DividerSegment)) || seg.is_a?(PathSegment) }
|
||||
required_segments.collect { |seg| seg.key if seg.respond_to?(:key)}.compact
|
||||
end
|
||||
|
||||
# Build a query string from the keys of the given hash. If +only_keys+
|
||||
# is given (as an array), only the keys indicated will be used to build
|
||||
# the query string. The query string will correctly build array parameter
|
||||
# values.
|
||||
def build_query_string(hash, only_keys = nil)
|
||||
elements = []
|
||||
|
||||
(only_keys || hash.keys).each do |key|
|
||||
if value = hash[key]
|
||||
elements << value.to_query(key)
|
||||
end
|
||||
end
|
||||
|
||||
elements.empty? ? '' : "?#{elements.sort * '&'}"
|
||||
end
|
||||
|
||||
# A route's parameter shell contains parameter values that are not in the
|
||||
# route's path, but should be placed in the recognized hash.
|
||||
#
|
||||
# For example, +{:controller => 'pages', :action => 'show'} is the shell for the route:
|
||||
#
|
||||
# map.connect '/page/:id', :controller => 'pages', :action => 'show', :id => /\d+/
|
||||
#
|
||||
def parameter_shell
|
||||
@parameter_shell ||= {}.tap do |shell|
|
||||
requirements.each do |key, requirement|
|
||||
shell[key] = requirement unless requirement.is_a? Regexp
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Return an array containing all the keys that are used in this route. This
|
||||
# includes keys that appear inside the path, and keys that have requirements
|
||||
# placed upon them.
|
||||
def significant_keys
|
||||
@significant_keys ||= [].tap do |sk|
|
||||
segments.each { |segment| sk << segment.key if segment.respond_to? :key }
|
||||
sk.concat requirements.keys
|
||||
sk.uniq!
|
||||
end
|
||||
end
|
||||
|
||||
# Return a hash of key/value pairs representing the keys in the route that
|
||||
# have defaults, or which are specified by non-regexp requirements.
|
||||
def defaults
|
||||
@defaults ||= {}.tap do |hash|
|
||||
segments.each do |segment|
|
||||
next unless segment.respond_to? :default
|
||||
hash[segment.key] = segment.default unless segment.default.nil?
|
||||
end
|
||||
requirements.each do |key,req|
|
||||
next if Regexp === req || req.nil?
|
||||
hash[key] = req
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def matches_controller_and_action?(controller, action)
|
||||
prepare_matching!
|
||||
(@controller_requirement.nil? || @controller_requirement === controller) &&
|
||||
(@action_requirement.nil? || @action_requirement === action)
|
||||
end
|
||||
|
||||
def to_s
|
||||
@to_s ||= begin
|
||||
segs = segments.inject("") { |str,s| str << s.to_s }
|
||||
"%-6s %-40s %s" % [(conditions[:method] || :any).to_s.upcase, segs, requirements.inspect]
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Route should be prepared and frozen on initialize
|
||||
def freeze
|
||||
unless frozen?
|
||||
write_generation!
|
||||
write_recognition!
|
||||
prepare_matching!
|
||||
|
||||
parameter_shell
|
||||
significant_keys
|
||||
defaults
|
||||
to_s
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def generate(options, hash, expire_on = {})
|
||||
path, hash = generate_raw(options, hash, expire_on)
|
||||
append_query_string(path, hash, extra_keys(options))
|
||||
end
|
||||
|
||||
def generate_extras(options, hash, expire_on = {})
|
||||
path, hash = generate_raw(options, hash, expire_on)
|
||||
[path, extra_keys(options)]
|
||||
end
|
||||
|
||||
private
|
||||
def requirement_for(key)
|
||||
return requirements[key] if requirements.key? key
|
||||
segments.each do |segment|
|
||||
return segment.regexp if segment.respond_to?(:key) && segment.key == key
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
# Write and compile a +generate+ method for this Route.
|
||||
def write_generation!
|
||||
# Build the main body of the generation
|
||||
body = "expired = false\n#{generation_extraction}\n#{generation_structure}"
|
||||
|
||||
# If we have conditions that must be tested first, nest the body inside an if
|
||||
body = "if #{generation_requirements}\n#{body}\nend" if generation_requirements
|
||||
args = "options, hash, expire_on = {}"
|
||||
|
||||
# Nest the body inside of a def block, and then compile it.
|
||||
raw_method = method_decl = "def generate_raw(#{args})\npath = begin\n#{body}\nend\n[path, hash]\nend"
|
||||
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
||||
|
||||
# expire_on.keys == recall.keys; in other words, the keys in the expire_on hash
|
||||
# are the same as the keys that were recalled from the previous request. Thus,
|
||||
# we can use the expire_on.keys to determine which keys ought to be used to build
|
||||
# the query string. (Never use keys from the recalled request when building the
|
||||
# query string.)
|
||||
|
||||
raw_method
|
||||
end
|
||||
|
||||
# Build several lines of code that extract values from the options hash. If any
|
||||
# of the values are missing or rejected then a return will be executed.
|
||||
def generation_extraction
|
||||
segments.collect do |segment|
|
||||
segment.extraction_code
|
||||
end.compact * "\n"
|
||||
end
|
||||
|
||||
# Produce a condition expression that will check the requirements of this route
|
||||
# upon generation.
|
||||
def generation_requirements
|
||||
requirement_conditions = requirements.collect do |key, req|
|
||||
if req.is_a? Regexp
|
||||
value_regexp = Regexp.new "\\A#{req.to_s}\\Z"
|
||||
"hash[:#{key}] && #{value_regexp.inspect} =~ options[:#{key}]"
|
||||
else
|
||||
"hash[:#{key}] == #{req.inspect}"
|
||||
end
|
||||
end
|
||||
requirement_conditions * ' && ' unless requirement_conditions.empty?
|
||||
end
|
||||
|
||||
def generation_structure
|
||||
segments.last.string_structure segments[0..-2]
|
||||
end
|
||||
|
||||
# Write and compile a +recognize+ method for this Route.
|
||||
def write_recognition!
|
||||
# Create an if structure to extract the params from a match if it occurs.
|
||||
body = "params = parameter_shell.dup\n#{recognition_extraction * "\n"}\nparams"
|
||||
body = "if #{recognition_conditions.join(" && ")}\n#{body}\nend"
|
||||
|
||||
# Build the method declaration and compile it
|
||||
method_decl = "def recognize(path, env = {})\n#{body}\nend"
|
||||
instance_eval method_decl, "generated code (#{__FILE__}:#{__LINE__})"
|
||||
method_decl
|
||||
end
|
||||
|
||||
# Plugins may override this method to add other conditions, like checks on
|
||||
# host, subdomain, and so forth. Note that changes here only affect route
|
||||
# recognition, not generation.
|
||||
def recognition_conditions
|
||||
result = ["(match = #{Regexp.new(recognition_pattern).inspect}.match(path))"]
|
||||
result << "[conditions[:method]].flatten.include?(env[:method])" if conditions[:method]
|
||||
result << "conditions[:host] === env[:host]" if conditions[:host]
|
||||
result << "conditions[:domain] === env[:domain]" if conditions[:domain]
|
||||
result << "conditions[:subdomain] === env[:subdomain]" if conditions[:subdomain]
|
||||
result << "conditions[:fullsubdomain] === env[:fullsubdomain]" if conditions[:fullsubdomain]
|
||||
result
|
||||
end
|
||||
|
||||
# Build the regular expression pattern that will match this route.
|
||||
def recognition_pattern(wrap = true)
|
||||
pattern = ''
|
||||
segments.reverse_each do |segment|
|
||||
pattern = segment.build_pattern pattern
|
||||
end
|
||||
wrap ? ("\\A" + pattern + "\\Z") : pattern
|
||||
end
|
||||
|
||||
# Write the code to extract the parameters from a matched route.
|
||||
def recognition_extraction
|
||||
next_capture = 1
|
||||
extraction = segments.collect do |segment|
|
||||
x = segment.match_extraction(next_capture)
|
||||
next_capture += segment.number_of_captures
|
||||
x
|
||||
end
|
||||
extraction.compact
|
||||
end
|
||||
|
||||
# Generate the query string with any extra keys in the hash and append
|
||||
# it to the given path, returning the new path.
|
||||
def append_query_string(path, hash, query_keys = nil)
|
||||
return nil unless path
|
||||
query_keys ||= extra_keys(hash)
|
||||
"#{path}#{build_query_string(hash, query_keys)}"
|
||||
end
|
||||
|
||||
# Determine which keys in the given hash are "extra". Extra keys are
|
||||
# those that were not used to generate a particular route. The extra
|
||||
# keys also do not include those recalled from the prior request, nor
|
||||
# do they include any keys that were implied in the route (like a
|
||||
# <tt>:controller</tt> that is required, but not explicitly used in the
|
||||
# text of the route.)
|
||||
def extra_keys(hash, recall = {})
|
||||
(hash || {}).keys.map { |k| k.to_sym } - (recall || {}).keys - significant_keys
|
||||
end
|
||||
|
||||
def prepare_matching!
|
||||
unless defined? @matching_prepared
|
||||
@controller_requirement = requirement_for(:controller)
|
||||
@action_requirement = requirement_for(:action)
|
||||
@matching_prepared = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,49 +0,0 @@
|
||||
class Object
|
||||
def to_param
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def to_param
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class FalseClass
|
||||
def to_param
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class NilClass
|
||||
def to_param
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Regexp #:nodoc:
|
||||
def number_of_captures
|
||||
Regexp.new("|#{source}").match('').captures.length
|
||||
end
|
||||
|
||||
def multiline?
|
||||
options & MULTILINE == MULTILINE
|
||||
end
|
||||
|
||||
class << self
|
||||
def optionalize(pattern)
|
||||
case unoptionalize(pattern)
|
||||
when /\A(.|\(.*\))\Z/ then "#{pattern}?"
|
||||
else "(?:#{pattern})?"
|
||||
end
|
||||
end
|
||||
|
||||
def unoptionalize(pattern)
|
||||
[/\A\(\?:(.*)\)\?\Z/, /\A(.|\(.*\))\?\Z/].each do |regexp|
|
||||
return $1 if regexp =~ pattern
|
||||
end
|
||||
return pattern
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,343 +0,0 @@
|
||||
module ActionController
|
||||
module Routing
|
||||
class Segment #:nodoc:
|
||||
RESERVED_PCHAR = ':@&=+$,;'
|
||||
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
|
||||
if RUBY_VERSION >= '1.9'
|
||||
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
|
||||
else
|
||||
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
|
||||
end
|
||||
|
||||
# TODO: Convert :is_optional accessor to read only
|
||||
attr_accessor :is_optional
|
||||
alias_method :optional?, :is_optional
|
||||
|
||||
def initialize
|
||||
@is_optional = false
|
||||
end
|
||||
|
||||
def number_of_captures
|
||||
Regexp.new(regexp_chunk).number_of_captures
|
||||
end
|
||||
|
||||
def extraction_code
|
||||
nil
|
||||
end
|
||||
|
||||
# Continue generating string for the prior segments.
|
||||
def continue_string_structure(prior_segments)
|
||||
if prior_segments.empty?
|
||||
interpolation_statement(prior_segments)
|
||||
else
|
||||
new_priors = prior_segments[0..-2]
|
||||
prior_segments.last.string_structure(new_priors)
|
||||
end
|
||||
end
|
||||
|
||||
def interpolation_chunk
|
||||
URI::DEFAULT_PARSER.escape(value, UNSAFE_PCHAR)
|
||||
end
|
||||
|
||||
# Return a string interpolation statement for this segment and those before it.
|
||||
def interpolation_statement(prior_segments)
|
||||
chunks = prior_segments.collect { |s| s.interpolation_chunk }
|
||||
chunks << interpolation_chunk
|
||||
"\"#{chunks * ''}\"#{all_optionals_available_condition(prior_segments)}"
|
||||
end
|
||||
|
||||
def string_structure(prior_segments)
|
||||
optional? ? continue_string_structure(prior_segments) : interpolation_statement(prior_segments)
|
||||
end
|
||||
|
||||
# Return an if condition that is true if all the prior segments can be generated.
|
||||
# If there are no optional segments before this one, then nil is returned.
|
||||
def all_optionals_available_condition(prior_segments)
|
||||
optional_locals = prior_segments.collect { |s| s.local_name if s.optional? && s.respond_to?(:local_name) }.compact
|
||||
optional_locals.empty? ? nil : " if #{optional_locals * ' && '}"
|
||||
end
|
||||
|
||||
# Recognition
|
||||
|
||||
def match_extraction(next_capture)
|
||||
nil
|
||||
end
|
||||
|
||||
# Warning
|
||||
|
||||
# Returns true if this segment is optional? because of a default. If so, then
|
||||
# no warning will be emitted regarding this segment.
|
||||
def optionality_implied?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class StaticSegment < Segment #:nodoc:
|
||||
attr_reader :value, :raw
|
||||
alias_method :raw?, :raw
|
||||
|
||||
def initialize(value = nil, options = {})
|
||||
super()
|
||||
@value = value
|
||||
@raw = options[:raw] if options.key?(:raw)
|
||||
@is_optional = options[:optional] if options.key?(:optional)
|
||||
end
|
||||
|
||||
def interpolation_chunk
|
||||
raw? ? value : super
|
||||
end
|
||||
|
||||
def regexp_chunk
|
||||
chunk = Regexp.escape(value)
|
||||
optional? ? Regexp.optionalize(chunk) : chunk
|
||||
end
|
||||
|
||||
def number_of_captures
|
||||
0
|
||||
end
|
||||
|
||||
def build_pattern(pattern)
|
||||
escaped = Regexp.escape(value)
|
||||
if optional? && ! pattern.empty?
|
||||
"(?:#{Regexp.optionalize escaped}\\Z|#{escaped}#{Regexp.unoptionalize pattern})"
|
||||
elsif optional?
|
||||
Regexp.optionalize escaped
|
||||
else
|
||||
escaped + pattern
|
||||
end
|
||||
end
|
||||
|
||||
def to_s
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
class DividerSegment < StaticSegment #:nodoc:
|
||||
def initialize(value = nil, options = {})
|
||||
super(value, {:raw => true, :optional => true}.merge(options))
|
||||
end
|
||||
|
||||
def optionality_implied?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class DynamicSegment < Segment #:nodoc:
|
||||
attr_reader :key
|
||||
|
||||
# TODO: Convert these accessors to read only
|
||||
attr_accessor :default, :regexp
|
||||
|
||||
def initialize(key = nil, options = {})
|
||||
super()
|
||||
@key = key
|
||||
@default = options[:default] if options.key?(:default)
|
||||
@regexp = options[:regexp] if options.key?(:regexp)
|
||||
@is_optional = true if options[:optional] || options.key?(:default)
|
||||
end
|
||||
|
||||
def to_s
|
||||
":#{key}"
|
||||
end
|
||||
|
||||
# The local variable name that the value of this segment will be extracted to.
|
||||
def local_name
|
||||
"#{key}_value"
|
||||
end
|
||||
|
||||
def extract_value
|
||||
"#{local_name} = hash[:#{key}] && hash[:#{key}].to_param #{"|| #{default.inspect}" if default}"
|
||||
end
|
||||
|
||||
def value_check
|
||||
if default # Then we know it won't be nil
|
||||
"#{value_regexp.inspect} =~ #{local_name}" if regexp
|
||||
elsif optional?
|
||||
# If we have a regexp check that the value is not given, or that it matches.
|
||||
# If we have no regexp, return nil since we do not require a condition.
|
||||
"#{local_name}.nil? || #{value_regexp.inspect} =~ #{local_name}" if regexp
|
||||
else # Then it must be present, and if we have a regexp, it must match too.
|
||||
"#{local_name} #{"&& #{value_regexp.inspect} =~ #{local_name}" if regexp}"
|
||||
end
|
||||
end
|
||||
|
||||
def expiry_statement
|
||||
"expired, hash = true, options if !expired && expire_on[:#{key}]"
|
||||
end
|
||||
|
||||
def extraction_code
|
||||
s = extract_value
|
||||
vc = value_check
|
||||
s << "\nreturn [nil,nil] unless #{vc}" if vc
|
||||
s << "\n#{expiry_statement}"
|
||||
end
|
||||
|
||||
def interpolation_chunk(value_code = local_name)
|
||||
"\#{URI::DEFAULT_PARSER.escape(#{value_code}.to_s, ActionController::Routing::Segment::UNSAFE_PCHAR)}"
|
||||
end
|
||||
|
||||
def string_structure(prior_segments)
|
||||
if optional? # We have a conditional to do...
|
||||
# If we should not appear in the url, just write the code for the prior
|
||||
# segments. This occurs if our value is the default value, or, if we are
|
||||
# optional, if we have nil as our value.
|
||||
"if #{local_name} == #{default.inspect}\n" +
|
||||
continue_string_structure(prior_segments) +
|
||||
"\nelse\n" + # Otherwise, write the code up to here
|
||||
"#{interpolation_statement(prior_segments)}\nend"
|
||||
else
|
||||
interpolation_statement(prior_segments)
|
||||
end
|
||||
end
|
||||
|
||||
def value_regexp
|
||||
Regexp.new "\\A#{regexp.to_s}\\Z" if regexp
|
||||
end
|
||||
|
||||
def regexp_chunk
|
||||
regexp ? regexp_string : default_regexp_chunk
|
||||
end
|
||||
|
||||
def regexp_string
|
||||
regexp_has_modifiers? ? "(#{regexp.to_s})" : "(#{regexp.source})"
|
||||
end
|
||||
|
||||
def default_regexp_chunk
|
||||
"([^#{Routing::SEPARATORS.join}]+)"
|
||||
end
|
||||
|
||||
def number_of_captures
|
||||
regexp ? regexp.number_of_captures + 1 : 1
|
||||
end
|
||||
|
||||
def build_pattern(pattern)
|
||||
pattern = "#{regexp_chunk}#{pattern}"
|
||||
optional? ? Regexp.optionalize(pattern) : pattern
|
||||
end
|
||||
|
||||
def match_extraction(next_capture)
|
||||
# All non code-related keys (such as :id, :slug) are URI-unescaped as
|
||||
# path parameters.
|
||||
default_value = default ? default.inspect : nil
|
||||
%[
|
||||
value = if (m = match[#{next_capture}])
|
||||
URI::DEFAULT_PARSER.unescape(m)
|
||||
else
|
||||
#{default_value}
|
||||
end
|
||||
params[:#{key}] = value if value
|
||||
]
|
||||
end
|
||||
|
||||
def optionality_implied?
|
||||
[:action, :id].include? key
|
||||
end
|
||||
|
||||
def regexp_has_modifiers?
|
||||
regexp.options & (Regexp::IGNORECASE | Regexp::EXTENDED) != 0
|
||||
end
|
||||
end
|
||||
|
||||
class ControllerSegment < DynamicSegment #:nodoc:
|
||||
def regexp_chunk
|
||||
possible_names = Routing.possible_controllers.collect { |name| Regexp.escape name }
|
||||
"(?i-:(#{(regexp || Regexp.union(*possible_names)).source}))"
|
||||
end
|
||||
|
||||
# Don't URI::DEFAULT_PARSER.escape the controller name since it may contain slashes.
|
||||
def interpolation_chunk(value_code = local_name)
|
||||
"\#{#{value_code}.to_s}"
|
||||
end
|
||||
|
||||
# Make sure controller names like Admin/Content are correctly normalized to
|
||||
# admin/content
|
||||
def extract_value
|
||||
"#{local_name} = (hash[:#{key}] #{"|| #{default.inspect}" if default}).downcase"
|
||||
end
|
||||
|
||||
def match_extraction(next_capture)
|
||||
if default
|
||||
"params[:#{key}] = match[#{next_capture}] ? match[#{next_capture}].downcase : '#{default}'"
|
||||
else
|
||||
"params[:#{key}] = match[#{next_capture}].downcase if match[#{next_capture}]"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class PathSegment < DynamicSegment #:nodoc:
|
||||
def interpolation_chunk(value_code = local_name)
|
||||
"\#{#{value_code}}"
|
||||
end
|
||||
|
||||
def extract_value
|
||||
"#{local_name} = hash[:#{key}] && Array(hash[:#{key}]).collect { |path_component| URI::DEFAULT_PARSER.escape(path_component.to_param, ActionController::Routing::Segment::UNSAFE_PCHAR) }.to_param #{"|| #{default.inspect}" if default}"
|
||||
end
|
||||
|
||||
def default
|
||||
''
|
||||
end
|
||||
|
||||
def default=(path)
|
||||
raise RoutingError, "paths cannot have non-empty default values" unless path.blank?
|
||||
end
|
||||
|
||||
def match_extraction(next_capture)
|
||||
"params[:#{key}] = PathSegment::Result.new_escaped((match[#{next_capture}]#{" || " + default.inspect if default}).split('/'))#{" if match[" + next_capture + "]" if !default}"
|
||||
end
|
||||
|
||||
def default_regexp_chunk
|
||||
"(.*)"
|
||||
end
|
||||
|
||||
def number_of_captures
|
||||
regexp ? regexp.number_of_captures : 1
|
||||
end
|
||||
|
||||
def optionality_implied?
|
||||
true
|
||||
end
|
||||
|
||||
class Result < ::Array #:nodoc:
|
||||
def to_s() join '/' end
|
||||
def self.new_escaped(strings)
|
||||
new strings.collect {|str| URI::DEFAULT_PARSER.unescape str}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# The OptionalFormatSegment allows for any resource route to have an optional
|
||||
# :format, which decreases the amount of routes created by 50%.
|
||||
class OptionalFormatSegment < DynamicSegment
|
||||
|
||||
def initialize(key = nil, options = {})
|
||||
super(:format, {:optional => true}.merge(options))
|
||||
end
|
||||
|
||||
def interpolation_chunk
|
||||
"." + super
|
||||
end
|
||||
|
||||
def regexp_chunk
|
||||
'/|(\.[^/?\.]+)?'
|
||||
end
|
||||
|
||||
def to_s
|
||||
'(.:format)?'
|
||||
end
|
||||
|
||||
def extract_value
|
||||
"#{local_name} = options[:#{key}] && options[:#{key}].to_s.downcase"
|
||||
end
|
||||
|
||||
#the value should not include the period (.)
|
||||
def match_extraction(next_capture)
|
||||
%[
|
||||
if (m = match[#{next_capture}])
|
||||
params[:#{key}] = URI::DEFAULT_PARSER.unescape(m.from(1))
|
||||
end
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
169
actionpack/lib/action_controller/routing/url_for.rb
Normal file
169
actionpack/lib/action_controller/routing/url_for.rb
Normal file
@@ -0,0 +1,169 @@
|
||||
require 'active_support/concern'
|
||||
|
||||
module ActionController
|
||||
module Routing
|
||||
# In <tt>config/routes.rb</tt> you define URL-to-controller mappings, but the reverse
|
||||
# is also possible: an URL can be generated from one of your routing definitions.
|
||||
# URL generation functionality is centralized in this module.
|
||||
#
|
||||
# See ActionDispatch::Routing for general information about routing and routes.rb.
|
||||
#
|
||||
# <b>Tip:</b> If you need to generate URLs from your models or some other place,
|
||||
# then ActionController::UrlFor is what you're looking for. Read on for
|
||||
# an introduction.
|
||||
#
|
||||
# == URL generation from parameters
|
||||
#
|
||||
# As you may know, some functions, such as ActionController::Base#url_for
|
||||
# and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
|
||||
# of parameters. For example, you've probably had the chance to write code
|
||||
# like this in one of your views:
|
||||
#
|
||||
# <%= link_to('Click here', :controller => 'users',
|
||||
# :action => 'new', :message => 'Welcome!') %>
|
||||
# # => "/users/new?message=Welcome%21"
|
||||
#
|
||||
# link_to, and all other functions that require URL generation functionality,
|
||||
# actually use ActionController::UrlFor under the hood. And in particular,
|
||||
# they use the ActionController::UrlFor#url_for method. One can generate
|
||||
# the same path as the above example by using the following code:
|
||||
#
|
||||
# include UrlFor
|
||||
# url_for(:controller => 'users',
|
||||
# :action => 'new',
|
||||
# :message => 'Welcome!',
|
||||
# :only_path => true)
|
||||
# # => "/users/new?message=Welcome%21"
|
||||
#
|
||||
# Notice the <tt>:only_path => true</tt> part. This is because UrlFor has no
|
||||
# information about the website hostname that your Rails app is serving. So if you
|
||||
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
|
||||
# argument:
|
||||
#
|
||||
# include UrlFor
|
||||
# url_for(:controller => 'users',
|
||||
# :action => 'new',
|
||||
# :message => 'Welcome!',
|
||||
# :host => 'www.example.com') # Changed this.
|
||||
# # => "http://www.example.com/users/new?message=Welcome%21"
|
||||
#
|
||||
# By default, all controllers and views have access to a special version of url_for,
|
||||
# that already knows what the current hostname is. So if you use url_for in your
|
||||
# controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
|
||||
# argument.
|
||||
#
|
||||
# For convenience reasons, mailers provide a shortcut for ActionController::UrlFor#url_for.
|
||||
# So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlFor#url_for'
|
||||
# in full. However, mailers don't have hostname information, and what's why you'll still
|
||||
# have to specify the <tt>:host</tt> argument when generating URLs in mailers.
|
||||
#
|
||||
#
|
||||
# == URL generation for named routes
|
||||
#
|
||||
# UrlFor also allows one to access methods that have been auto-generated from
|
||||
# named routes. For example, suppose that you have a 'users' resource in your
|
||||
# <tt>config/routes.rb</tt>:
|
||||
#
|
||||
# resources :users
|
||||
#
|
||||
# This generates, among other things, the method <tt>users_path</tt>. By default,
|
||||
# this method is accessible from your controllers, views and mailers. If you need
|
||||
# to access this auto-generated method from other places (such as a model), then
|
||||
# you can do that by including ActionController::UrlFor in your class:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# include Rails.application.routes.url_helpers
|
||||
#
|
||||
# def base_uri
|
||||
# user_path(self)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# User.find(1).base_uri # => "/users/1"
|
||||
#
|
||||
module UrlFor
|
||||
extend ActiveSupport::Concern
|
||||
include PolymorphicRoutes
|
||||
|
||||
included do
|
||||
# TODO: with_routing extends @controller with url_helpers, trickling down to including this module which overrides its default_url_options
|
||||
unless method_defined?(:default_url_options)
|
||||
# Including in a class uses an inheritable hash. Modules get a plain hash.
|
||||
if respond_to?(:class_attribute)
|
||||
class_attribute :default_url_options
|
||||
else
|
||||
mattr_writer :default_url_options
|
||||
end
|
||||
|
||||
self.default_url_options = {}
|
||||
end
|
||||
|
||||
include(*_url_for_modules) if respond_to?(:_url_for_modules)
|
||||
end
|
||||
|
||||
def initialize(*)
|
||||
@_routes = nil
|
||||
super
|
||||
end
|
||||
|
||||
# Hook overridden in controller to add request information
|
||||
# with `default_url_options`. Application logic should not
|
||||
# go into url_options.
|
||||
def url_options
|
||||
default_url_options
|
||||
end
|
||||
|
||||
# Generate a url based on the options provided, default_url_options and the
|
||||
# routes defined in routes.rb. The following options are supported:
|
||||
#
|
||||
# * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+.
|
||||
# * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
|
||||
# * <tt>:host</tt> - Specifies the host the link should be targeted at.
|
||||
# If <tt>:only_path</tt> is false, this option must be
|
||||
# provided either explicitly, or via +default_url_options+.
|
||||
# * <tt>:port</tt> - Optionally specify the port to connect to.
|
||||
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
|
||||
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
|
||||
#
|
||||
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
|
||||
# +url_for+ is forwarded to the Routes module.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing'
|
||||
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok'
|
||||
# url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/'
|
||||
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33'
|
||||
def url_for(options = nil)
|
||||
case options
|
||||
when nil
|
||||
_routes.url_for(url_options.symbolize_keys)
|
||||
when Hash
|
||||
_routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
|
||||
when String
|
||||
options
|
||||
else
|
||||
polymorphic_url(options)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def optimize_routes_generation?
|
||||
return @_optimized_routes if defined?(@_optimized_routes)
|
||||
@_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty?
|
||||
end
|
||||
|
||||
def _with_routes(routes)
|
||||
old_routes, @_routes = @_routes, routes
|
||||
yield
|
||||
ensure
|
||||
@_routes = old_routes
|
||||
end
|
||||
|
||||
def _routes_context
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -36,6 +36,7 @@ module ActionController #:nodoc:
|
||||
@env['RAW_POST_DATA'] ||= begin
|
||||
data = url_encoded_request_parameters
|
||||
data.force_encoding(Encoding::BINARY) if data.respond_to?(:force_encoding)
|
||||
@env['CONTENT_LENGTH'] = data.bytesize.to_s
|
||||
data
|
||||
end
|
||||
end
|
||||
@@ -44,11 +45,6 @@ module ActionController #:nodoc:
|
||||
@env["SERVER_PORT"] = number.to_i
|
||||
end
|
||||
|
||||
def action=(action_name)
|
||||
@query_parameters.update({ "action" => action_name })
|
||||
@parameters = nil
|
||||
end
|
||||
|
||||
# Used to check AbstractRequest's request_uri functionality.
|
||||
# Disables the use of @path and @request_uri so superclass can handle those.
|
||||
def set_REQUEST_URI(value)
|
||||
@@ -91,23 +87,32 @@ module ActionController #:nodoc:
|
||||
@path || super()
|
||||
end
|
||||
|
||||
def assign_parameters(controller_path, action, parameters = {})
|
||||
def assign_parameters(routes, controller_path, action, parameters = {})
|
||||
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
|
||||
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
|
||||
extra_keys = routes.extra_keys(parameters)
|
||||
non_path_parameters = get? ? query_parameters : request_parameters
|
||||
parameters.each do |key, value|
|
||||
if value.is_a? Fixnum
|
||||
value = value.to_s
|
||||
elsif value.is_a? Array
|
||||
value = ActionController::Routing::PathSegment::Result.new(value)
|
||||
if value.is_a?(Array) && (value.frozen? || value.any?(&:frozen?))
|
||||
value = value.map{ |v| v.duplicable? ? v.dup : v }
|
||||
elsif value.is_a?(Hash) && (value.frozen? || value.any?{ |k,v| v.frozen? })
|
||||
value = Hash[value.map{ |k,v| [k, v.duplicable? ? v.dup : v] }]
|
||||
elsif value.frozen? && value.duplicable?
|
||||
value = value.dup
|
||||
end
|
||||
|
||||
if extra_keys.include?(key.to_sym)
|
||||
non_path_parameters[key] = value
|
||||
else
|
||||
if value.is_a?(Array)
|
||||
value = value.map(&:to_param)
|
||||
else
|
||||
value = value.to_param
|
||||
end
|
||||
|
||||
path_parameters[key.to_s] = value
|
||||
end
|
||||
end
|
||||
|
||||
raw_post # populate env['RAW_POST_DATA']
|
||||
@parameters = nil # reset TestRequest#parameters to use the new path_parameters
|
||||
end
|
||||
@@ -420,9 +425,11 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET')
|
||||
@routes ||= ActionController::Routing::Routes
|
||||
|
||||
# Sanity check for required instance variables so we can give an
|
||||
# understandable error message.
|
||||
%w(@controller @request @response).each do |iv_name|
|
||||
%w(@routes @controller @request @response).each do |iv_name|
|
||||
if !(instance_variable_names.include?(iv_name) || instance_variable_names.include?(iv_name.to_sym)) || instance_variable_get(iv_name).nil?
|
||||
raise "#{iv_name} is nil: make sure you set it in your test's setup method."
|
||||
end
|
||||
@@ -434,10 +441,8 @@ module ActionController #:nodoc:
|
||||
@html_document = nil
|
||||
@request.env['REQUEST_METHOD'] = http_method
|
||||
|
||||
@request.action = action.to_s
|
||||
|
||||
parameters ||= {}
|
||||
@request.assign_parameters(@controller.class.controller_path, action.to_s, parameters)
|
||||
@request.assign_parameters(@routes, @controller.class.controller_path, action.to_s, parameters)
|
||||
|
||||
@request.session = ActionController::TestSession.new(session) unless session.nil?
|
||||
@request.session["flash"] = ActionController::Flash::FlashHash.new.update(flash) if flash
|
||||
@@ -482,13 +487,20 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def build_request_uri(action, parameters)
|
||||
unless @request.env['REQUEST_URI']
|
||||
options = @controller.__send__(:rewrite_options, parameters)
|
||||
options.update(:only_path => true, :action => action)
|
||||
@controller.request = @request
|
||||
options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
|
||||
options.update(
|
||||
:only_path => true,
|
||||
:action => action,
|
||||
:relative_url_root => nil,
|
||||
:_recall => @request.symbolized_path_parameters)
|
||||
|
||||
url = ActionController::UrlRewriter.new(@request, parameters)
|
||||
@request.set_REQUEST_URI(url.rewrite(options))
|
||||
end
|
||||
url, query_string = @routes.url_for(options).split("?", 2)
|
||||
|
||||
@request.env["PATH_INFO"] = url
|
||||
@request.env["QUERY_STRING"] = (@request.get? && query_string) || ""
|
||||
@request.set_REQUEST_URI(nil)
|
||||
@request.request_uri # populate REQUEST_URI
|
||||
end
|
||||
|
||||
def html_document
|
||||
|
||||
@@ -1,166 +1,6 @@
|
||||
require 'uri'
|
||||
|
||||
module ActionController
|
||||
# In <b>routes.rb</b> one defines URL-to-controller mappings, but the reverse
|
||||
# is also possible: an URL can be generated from one of your routing definitions.
|
||||
# URL generation functionality is centralized in this module.
|
||||
#
|
||||
# See ActionController::Routing and ActionController::Resources for general
|
||||
# information about routing and routes.rb.
|
||||
#
|
||||
# <b>Tip:</b> If you need to generate URLs from your models or some other place,
|
||||
# then ActionController::UrlWriter is what you're looking for. Read on for
|
||||
# an introduction.
|
||||
#
|
||||
# == URL generation from parameters
|
||||
#
|
||||
# As you may know, some functions - such as ActionController::Base#url_for
|
||||
# and ActionView::Helpers::UrlHelper#link_to, can generate URLs given a set
|
||||
# of parameters. For example, you've probably had the chance to write code
|
||||
# like this in one of your views:
|
||||
#
|
||||
# <%= link_to('Click here', :controller => 'users',
|
||||
# :action => 'new', :message => 'Welcome!') %>
|
||||
#
|
||||
# #=> Generates a link to: /users/new?message=Welcome%21
|
||||
#
|
||||
# link_to, and all other functions that require URL generation functionality,
|
||||
# actually use ActionController::UrlWriter under the hood. And in particular,
|
||||
# they use the ActionController::UrlWriter#url_for method. One can generate
|
||||
# the same path as the above example by using the following code:
|
||||
#
|
||||
# include UrlWriter
|
||||
# url_for(:controller => 'users',
|
||||
# :action => 'new',
|
||||
# :message => 'Welcome!',
|
||||
# :only_path => true)
|
||||
# # => "/users/new?message=Welcome%21"
|
||||
#
|
||||
# Notice the <tt>:only_path => true</tt> part. This is because UrlWriter has no
|
||||
# information about the website hostname that your Rails app is serving. So if you
|
||||
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
|
||||
# argument:
|
||||
#
|
||||
# include UrlWriter
|
||||
# url_for(:controller => 'users',
|
||||
# :action => 'new',
|
||||
# :message => 'Welcome!',
|
||||
# :host => 'www.example.com') # Changed this.
|
||||
# # => "http://www.example.com/users/new?message=Welcome%21"
|
||||
#
|
||||
# By default, all controllers and views have access to a special version of url_for,
|
||||
# that already knows what the current hostname is. So if you use url_for in your
|
||||
# controllers or your views, then you don't need to explicitly pass the <tt>:host</tt>
|
||||
# argument.
|
||||
#
|
||||
# For convenience reasons, mailers provide a shortcut for ActionController::UrlWriter#url_for.
|
||||
# So within mailers, you only have to type 'url_for' instead of 'ActionController::UrlWriter#url_for'
|
||||
# in full. However, mailers don't have hostname information, and what's why you'll still
|
||||
# have to specify the <tt>:host</tt> argument when generating URLs in mailers.
|
||||
#
|
||||
#
|
||||
# == URL generation for named routes
|
||||
#
|
||||
# UrlWriter also allows one to access methods that have been auto-generated from
|
||||
# named routes. For example, suppose that you have a 'users' resource in your
|
||||
# <b>routes.rb</b>:
|
||||
#
|
||||
# map.resources :users
|
||||
#
|
||||
# This generates, among other things, the method <tt>users_path</tt>. By default,
|
||||
# this method is accessible from your controllers, views and mailers. If you need
|
||||
# to access this auto-generated method from other places (such as a model), then
|
||||
# you can do that in two ways.
|
||||
#
|
||||
# The first way is to include ActionController::UrlWriter in your class:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# include ActionController::UrlWriter # !!!
|
||||
#
|
||||
# def name=(value)
|
||||
# write_attribute('name', value)
|
||||
# write_attribute('base_uri', users_path) # !!!
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The second way is to access them through ActionController::UrlWriter.
|
||||
# The autogenerated named routes methods are available as class methods:
|
||||
#
|
||||
# class User < ActiveRecord::Base
|
||||
# def name=(value)
|
||||
# write_attribute('name', value)
|
||||
# path = ActionController::UrlWriter.users_path # !!!
|
||||
# write_attribute('base_uri', path) # !!!
|
||||
# end
|
||||
# end
|
||||
module UrlWriter
|
||||
RESERVED_PCHAR = ':@&=+$,;%'
|
||||
SAFE_PCHAR = "#{URI::REGEXP::PATTERN::UNRESERVED}#{RESERVED_PCHAR}"
|
||||
if RUBY_VERSION >= '1.9'
|
||||
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false).freeze
|
||||
else
|
||||
UNSAFE_PCHAR = Regexp.new("[^#{SAFE_PCHAR}]", false, 'N').freeze
|
||||
end
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
ActionController::Routing::Routes.install_helpers(base)
|
||||
base.mattr_accessor :default_url_options
|
||||
|
||||
# The default options for urls written by this writer. Typically a <tt>:host</tt> pair is provided.
|
||||
base.default_url_options ||= {}
|
||||
end
|
||||
|
||||
# Generate a url based on the options provided, default_url_options and the
|
||||
# routes defined in routes.rb. The following options are supported:
|
||||
#
|
||||
# * <tt>:only_path</tt> - If true, the relative url is returned. Defaults to +false+.
|
||||
# * <tt>:protocol</tt> - The protocol to connect to. Defaults to 'http'.
|
||||
# * <tt>:host</tt> - Specifies the host the link should be targetted at.
|
||||
# If <tt>:only_path</tt> is false, this option must be
|
||||
# provided either explicitly, or via +default_url_options+.
|
||||
# * <tt>:port</tt> - Optionally specify the port to connect to.
|
||||
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
|
||||
# * <tt>:skip_relative_url_root</tt> - If true, the url is not constructed using the
|
||||
# +relative_url_root+ set in ActionController::Base.relative_url_root.
|
||||
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
|
||||
#
|
||||
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
|
||||
# +url_for+ is forwarded to the Routes module.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :port=>'8080' # => 'http://somehost.org:8080/tasks/testing'
|
||||
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok'
|
||||
# url_for :controller => 'tasks', :action => 'testing', :trailing_slash=>true # => 'http://somehost.org/tasks/testing/'
|
||||
# url_for :controller => 'tasks', :action => 'testing', :host=>'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33'
|
||||
def url_for(options)
|
||||
options = self.class.default_url_options.merge(options)
|
||||
|
||||
url = ''
|
||||
|
||||
unless options.delete(:only_path)
|
||||
url << (options.delete(:protocol) || 'http')
|
||||
url << '://' unless url.match("://")
|
||||
|
||||
raise "Missing host to link to! Please provide :host parameter or set default_url_options[:host]" unless options[:host]
|
||||
|
||||
url << options.delete(:host)
|
||||
url << ":#{options.delete(:port)}" if options.key?(:port)
|
||||
else
|
||||
# Delete the unused options to prevent their appearance in the query string.
|
||||
[:protocol, :host, :port, :skip_relative_url_root].each { |k| options.delete(k) }
|
||||
end
|
||||
trailing_slash = options.delete(:trailing_slash) if options.key?(:trailing_slash)
|
||||
url << ActionController::Base.relative_url_root.to_s unless options[:skip_relative_url_root]
|
||||
anchor = "##{URI::DEFAULT_PARSER.escape(options.delete(:anchor).to_param.to_s, UNSAFE_PCHAR)}" if options[:anchor]
|
||||
generated = Routing::Routes.generate(options, {})
|
||||
url << (trailing_slash ? generated.sub(/\?|\z/) { "/" + $& } : generated)
|
||||
url << anchor if anchor
|
||||
|
||||
url
|
||||
end
|
||||
end
|
||||
|
||||
# Rewrites URLs for Base.redirect_to and Base.url_for in the controller.
|
||||
class UrlRewriter #:nodoc:
|
||||
RESERVED_OPTIONS = [:anchor, :params, :only_path, :host, :protocol, :port, :trailing_slash, :skip_relative_url_root]
|
||||
@@ -215,7 +55,7 @@ module ActionController
|
||||
RESERVED_OPTIONS.each { |k| options.delete(k) }
|
||||
|
||||
# Generates the query string, too
|
||||
Routing::Routes.generate(options, @request.symbolized_path_parameters)
|
||||
Routing::Routes.url_for({:host => @request.host_with_port, :protocol => @request.protocol}.merge(options))
|
||||
end
|
||||
|
||||
def rewrite_authentication(options)
|
||||
|
||||
211
actionpack/lib/action_dispatch/http/url.rb
Normal file
211
actionpack/lib/action_dispatch/http/url.rb
Normal file
@@ -0,0 +1,211 @@
|
||||
require 'active_support/core_ext/module/attribute_accessors'
|
||||
require 'active_support/core_ext/hash/slice'
|
||||
|
||||
module ActionDispatch
|
||||
module Http
|
||||
module URL
|
||||
IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
|
||||
|
||||
mattr_accessor :tld_length
|
||||
self.tld_length = 1
|
||||
|
||||
class << self
|
||||
def extract_domain(host, tld_length = @@tld_length)
|
||||
host.split('.').last(1 + tld_length).join('.') if named_host?(host)
|
||||
end
|
||||
|
||||
def extract_subdomains(host, tld_length = @@tld_length)
|
||||
if named_host?(host)
|
||||
parts = host.split('.')
|
||||
parts[0..-(tld_length + 2)]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def extract_subdomain(host, tld_length = @@tld_length)
|
||||
extract_subdomains(host, tld_length).join('.')
|
||||
end
|
||||
|
||||
def url_for(options = {})
|
||||
path = options.delete(:script_name).to_s.chomp("/")
|
||||
path << options.delete(:path).to_s
|
||||
|
||||
params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params)
|
||||
params.reject! { |_,v| v.to_param.nil? }
|
||||
|
||||
result = build_host_url(options)
|
||||
if options[:trailing_slash]
|
||||
if path.include?('?')
|
||||
result << path.sub(/\?/, '/\&')
|
||||
else
|
||||
result << path.sub(/[^\/]\z|\A\z/, '\&/')
|
||||
end
|
||||
else
|
||||
result << path
|
||||
end
|
||||
result << "?#{params.to_query}" unless params.empty?
|
||||
result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
|
||||
result
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def build_host_url(options)
|
||||
if options[:host].blank? && options[:only_path].blank?
|
||||
raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
|
||||
end
|
||||
|
||||
result = ""
|
||||
|
||||
unless options[:only_path]
|
||||
protocol = extract_protocol(options)
|
||||
unless options[:protocol] == false
|
||||
result << protocol
|
||||
result << ":" unless result.match(%r{:|//})
|
||||
end
|
||||
result << "//" unless result.match("//")
|
||||
result << rewrite_authentication(options)
|
||||
result << host_or_subdomain_and_domain(options)
|
||||
result << ":#{options.delete(:port)}" if options[:port]
|
||||
end
|
||||
result
|
||||
end
|
||||
|
||||
def named_host?(host)
|
||||
host && IP_HOST_REGEXP !~ host
|
||||
end
|
||||
|
||||
def rewrite_authentication(options)
|
||||
if options[:user] && options[:password]
|
||||
"#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
|
||||
else
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
# Extracts protocol http:// or https:// from options[:host]
|
||||
# needs to be called whether the :protocol is being used or not
|
||||
def extract_protocol(options)
|
||||
if options[:host] && match = options[:host].match(/(^.*:\/\/)(.*)/)
|
||||
options[:protocol] ||= match[1]
|
||||
options[:host] = match[2]
|
||||
end
|
||||
options[:protocol] || "http"
|
||||
end
|
||||
|
||||
def host_or_subdomain_and_domain(options)
|
||||
return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?)
|
||||
|
||||
tld_length = options[:tld_length] || @@tld_length
|
||||
|
||||
host = ""
|
||||
unless options[:subdomain] == false
|
||||
host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param
|
||||
host << "."
|
||||
end
|
||||
host << (options[:domain] || extract_domain(options[:host], tld_length))
|
||||
host
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(env)
|
||||
super
|
||||
@protocol = nil
|
||||
@port = nil
|
||||
end
|
||||
|
||||
# Returns the complete URL used for this request.
|
||||
def url
|
||||
protocol + host_with_port + fullpath
|
||||
end
|
||||
|
||||
# Returns 'https://' if this is an SSL request and 'http://' otherwise.
|
||||
def protocol
|
||||
@protocol ||= ssl? ? 'https://' : 'http://'
|
||||
end
|
||||
|
||||
# Returns the \host for this request, such as "example.com".
|
||||
def raw_host_with_port
|
||||
if forwarded = env["HTTP_X_FORWARDED_HOST"]
|
||||
forwarded.split(/,\s?/).last
|
||||
else
|
||||
env['HTTP_HOST'] || "#{env['SERVER_NAME'] || env['SERVER_ADDR']}:#{env['SERVER_PORT']}"
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the host for this request, such as example.com.
|
||||
def host
|
||||
raw_host_with_port.sub(/:\d+$/, '')
|
||||
end
|
||||
|
||||
# Returns a \host:\port string for this request, such as "example.com" or
|
||||
# "example.com:8080".
|
||||
def host_with_port
|
||||
"#{host}#{port_string}"
|
||||
end
|
||||
|
||||
# Returns the port number of this request as an integer.
|
||||
def port
|
||||
@port ||= begin
|
||||
if raw_host_with_port =~ /:(\d+)$/
|
||||
$1.to_i
|
||||
else
|
||||
standard_port
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the standard \port number for this request's protocol.
|
||||
def standard_port
|
||||
case protocol
|
||||
when 'https://' then 443
|
||||
else 80
|
||||
end
|
||||
end
|
||||
|
||||
# Returns whether this request is using the standard port
|
||||
def standard_port?
|
||||
port == standard_port
|
||||
end
|
||||
|
||||
# Returns a number \port suffix like 8080 if the \port number of this request
|
||||
# is not the default HTTP \port 80 or HTTPS \port 443.
|
||||
def optional_port
|
||||
standard_port? ? nil : port
|
||||
end
|
||||
|
||||
# Returns a string \port suffix, including colon, like ":8080" if the \port
|
||||
# number of this request is not the default HTTP \port 80 or HTTPS \port 443.
|
||||
def port_string
|
||||
standard_port? ? '' : ":#{port}"
|
||||
end
|
||||
|
||||
def server_port
|
||||
@env['SERVER_PORT'].to_i
|
||||
end
|
||||
|
||||
# Returns the \domain part of a \host, such as "rubyonrails.org" in "www.rubyonrails.org". You can specify
|
||||
# a different <tt>tld_length</tt>, such as 2 to catch rubyonrails.co.uk in "www.rubyonrails.co.uk".
|
||||
def domain(tld_length = @@tld_length)
|
||||
ActionDispatch::Http::URL.extract_domain(host, tld_length)
|
||||
end
|
||||
|
||||
# Returns all the \subdomains as an array, so <tt>["dev", "www"]</tt> would be
|
||||
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
|
||||
# such as 2 to catch <tt>["www"]</tt> instead of <tt>["www", "rubyonrails"]</tt>
|
||||
# in "www.rubyonrails.co.uk".
|
||||
def subdomains(tld_length = @@tld_length)
|
||||
ActionDispatch::Http::URL.extract_subdomains(host, tld_length)
|
||||
end
|
||||
|
||||
# Returns all the \subdomains as a string, so <tt>"dev.www"</tt> would be
|
||||
# returned for "dev.www.rubyonrails.org". You can specify a different <tt>tld_length</tt>,
|
||||
# such as 2 to catch <tt>"www"</tt> instead of <tt>"www.rubyonrails"</tt>
|
||||
# in "www.rubyonrails.co.uk".
|
||||
def subdomain(tld_length = @@tld_length)
|
||||
ActionDispatch::Http::URL.extract_subdomain(host, tld_length)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
5
actionpack/lib/action_dispatch/journey.rb
Normal file
5
actionpack/lib/action_dispatch/journey.rb
Normal file
@@ -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'
|
||||
5
actionpack/lib/action_dispatch/journey/backwards.rb
Normal file
5
actionpack/lib/action_dispatch/journey/backwards.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
module Rack # :nodoc:
|
||||
Mount = ActionDispatch::Journey::Router
|
||||
Mount::RouteSet = ActionDispatch::Journey::Router
|
||||
Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern
|
||||
end
|
||||
146
actionpack/lib/action_dispatch/journey/formatter.rb
Normal file
146
actionpack/lib/action_dispatch/journey/formatter.rb
Normal file
@@ -0,0 +1,146 @@
|
||||
require 'action_controller/metal/exceptions'
|
||||
|
||||
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 # :nodoc:
|
||||
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
|
||||
|
||||
message = "No route matches #{constraints.inspect}"
|
||||
message << " missing required keys: #{missing_keys.inspect}" if name
|
||||
|
||||
raise ActionController::UrlGenerationError, message
|
||||
end
|
||||
|
||||
def clear
|
||||
@cache = nil
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_parameterized_parts(route, options, recall, parameterize = nil)
|
||||
parameterized_parts = recall.merge(options)
|
||||
|
||||
keys_to_keep = route.parts.reverse.drop_while { |part|
|
||||
!options.key?(part) || (options[part] || recall[part]).nil?
|
||||
} | route.required_parts
|
||||
|
||||
(parameterized_parts.keys - keys_to_keep).each do |bad_key|
|
||||
parameterized_parts.delete(bad_key)
|
||||
end
|
||||
|
||||
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 = 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 +true+ if no missing keys are present, otherwise +false+.
|
||||
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
|
||||
162
actionpack/lib/action_dispatch/journey/gtg/builder.rb
Normal file
162
actionpack/lib/action_dispatch/journey/gtg/builder.rb
Normal file
@@ -0,0 +1,162 @@
|
||||
require 'action_dispatch/journey/gtg/transition_table'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module GTG # :nodoc:
|
||||
class Builder # :nodoc:
|
||||
DUMMY = Nodes::Dummy.new
|
||||
|
||||
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
|
||||
44
actionpack/lib/action_dispatch/journey/gtg/simulator.rb
Normal file
44
actionpack/lib/action_dispatch/journey/gtg/simulator.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
require 'strscan'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module GTG # :nodoc:
|
||||
class MatchData # :nodoc:
|
||||
attr_reader :memos
|
||||
|
||||
def initialize(memos)
|
||||
@memos = memos
|
||||
end
|
||||
end
|
||||
|
||||
class Simulator # :nodoc:
|
||||
attr_reader :tt
|
||||
|
||||
def initialize(transition_table)
|
||||
@tt = transition_table
|
||||
end
|
||||
|
||||
def simulate(string)
|
||||
input = StringScanner.new(string)
|
||||
state = [0]
|
||||
while sym = input.scan(%r([/.?]|[^/.?]+))
|
||||
state = tt.move(state, sym)
|
||||
end
|
||||
|
||||
acceptance_states = state.find_all { |s|
|
||||
tt.accepting? s
|
||||
}
|
||||
|
||||
return if acceptance_states.empty?
|
||||
|
||||
memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact
|
||||
|
||||
MatchData.new(memos)
|
||||
end
|
||||
|
||||
alias :=~ :simulate
|
||||
alias :match :simulate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
156
actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
Normal file
156
actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
Normal file
@@ -0,0 +1,156 @@
|
||||
require 'action_dispatch/journey/nfa/dot'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module GTG # :nodoc:
|
||||
class TransitionTable # :nodoc:
|
||||
include Journey::NFA::Dot
|
||||
|
||||
attr_reader :memos
|
||||
|
||||
def initialize
|
||||
@regexp_states = Hash.new { |h,k| h[k] = {} }
|
||||
@string_states = Hash.new { |h,k| h[k] = {} }
|
||||
@accepting = {}
|
||||
@memos = Hash.new { |h,k| h[k] = [] }
|
||||
end
|
||||
|
||||
def add_accepting(state)
|
||||
@accepting[state] = true
|
||||
end
|
||||
|
||||
def accepting_states
|
||||
@accepting.keys
|
||||
end
|
||||
|
||||
def accepting?(state)
|
||||
@accepting[state]
|
||||
end
|
||||
|
||||
def add_memo(idx, memo)
|
||||
@memos[idx] << memo
|
||||
end
|
||||
|
||||
def memo(idx)
|
||||
@memos[idx]
|
||||
end
|
||||
|
||||
def eclosure(t)
|
||||
Array(t)
|
||||
end
|
||||
|
||||
def move(t, a)
|
||||
move_string(t, a).concat(move_regexp(t, a))
|
||||
end
|
||||
|
||||
def to_json
|
||||
require 'json'
|
||||
|
||||
simple_regexp = Hash.new { |h,k| h[k] = {} }
|
||||
|
||||
@regexp_states.each do |from, hash|
|
||||
hash.each do |re, to|
|
||||
simple_regexp[from][re.source] = to
|
||||
end
|
||||
end
|
||||
|
||||
JSON.dump({
|
||||
:regexp_states => simple_regexp,
|
||||
:string_states => @string_states,
|
||||
:accepting => @accepting
|
||||
})
|
||||
end
|
||||
|
||||
def to_svg
|
||||
svg = IO.popen('dot -Tsvg', 'w+') { |f|
|
||||
f.write(to_dot)
|
||||
f.close_write
|
||||
f.readlines
|
||||
}
|
||||
3.times { svg.shift }
|
||||
svg.join.sub(/width="[^"]*"/, '').sub(/height="[^"]*"/, '')
|
||||
end
|
||||
|
||||
def visualizer(paths, title = 'FSM')
|
||||
viz_dir = File.join File.dirname(__FILE__), '..', 'visualizer'
|
||||
fsm_js = File.read File.join(viz_dir, 'fsm.js')
|
||||
fsm_css = File.read File.join(viz_dir, 'fsm.css')
|
||||
erb = File.read File.join(viz_dir, 'index.html.erb')
|
||||
states = "function tt() { return #{to_json}; }"
|
||||
|
||||
fun_routes = paths.shuffle.first(3).map do |ast|
|
||||
ast.map { |n|
|
||||
case n
|
||||
when Nodes::Symbol
|
||||
case n.left
|
||||
when ':id' then rand(100).to_s
|
||||
when ':format' then %w{ xml json }.shuffle.first
|
||||
else
|
||||
'omg'
|
||||
end
|
||||
when Nodes::Terminal then n.symbol
|
||||
else
|
||||
nil
|
||||
end
|
||||
}.compact.join
|
||||
end
|
||||
|
||||
stylesheets = [fsm_css]
|
||||
svg = to_svg
|
||||
javascripts = [states, fsm_js]
|
||||
|
||||
# Annoying hack for 1.9 warnings
|
||||
fun_routes = fun_routes
|
||||
stylesheets = stylesheets
|
||||
svg = svg
|
||||
javascripts = javascripts
|
||||
|
||||
require 'erb'
|
||||
template = ERB.new erb
|
||||
template.result(binding)
|
||||
end
|
||||
|
||||
def []=(from, to, sym)
|
||||
case sym
|
||||
when String
|
||||
@string_states[from][sym] = to
|
||||
when Regexp
|
||||
@regexp_states[from][sym] = to
|
||||
else
|
||||
raise ArgumentError, 'unknown symbol: %s' % sym.class
|
||||
end
|
||||
end
|
||||
|
||||
def states
|
||||
ss = @string_states.keys + @string_states.values.map(&:values).flatten
|
||||
rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten
|
||||
(ss + rs).uniq
|
||||
end
|
||||
|
||||
def transitions
|
||||
@string_states.map { |from, hash|
|
||||
hash.map { |s, to| [from, s, to] }
|
||||
}.flatten(1) + @regexp_states.map { |from, hash|
|
||||
hash.map { |s, to| [from, s, to] }
|
||||
}.flatten(1)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def move_regexp(t, a)
|
||||
return [] if t.empty?
|
||||
|
||||
t.map { |s|
|
||||
@regexp_states[s].map { |re, v| re === a ? v : nil }
|
||||
}.flatten.compact.uniq
|
||||
end
|
||||
|
||||
def move_string(t, a)
|
||||
return [] if t.empty?
|
||||
|
||||
t.map { |s| @string_states[s][a] }.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
76
actionpack/lib/action_dispatch/journey/nfa/builder.rb
Normal file
76
actionpack/lib/action_dispatch/journey/nfa/builder.rb
Normal file
@@ -0,0 +1,76 @@
|
||||
require 'action_dispatch/journey/nfa/transition_table'
|
||||
require 'action_dispatch/journey/gtg/transition_table'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module NFA # :nodoc:
|
||||
class Visitor < Visitors::Visitor # :nodoc:
|
||||
def initialize(tt)
|
||||
@tt = tt
|
||||
@i = -1
|
||||
end
|
||||
|
||||
def visit_CAT(node)
|
||||
left = visit(node.left)
|
||||
right = visit(node.right)
|
||||
|
||||
@tt.merge(left.last, right.first)
|
||||
|
||||
[left.first, right.last]
|
||||
end
|
||||
|
||||
def visit_GROUP(node)
|
||||
from = @i += 1
|
||||
left = visit(node.left)
|
||||
to = @i += 1
|
||||
|
||||
@tt.accepting = to
|
||||
|
||||
@tt[from, left.first] = nil
|
||||
@tt[left.last, to] = nil
|
||||
@tt[from, to] = nil
|
||||
|
||||
[from, to]
|
||||
end
|
||||
|
||||
def visit_OR(node)
|
||||
from = @i += 1
|
||||
children = node.children.map { |c| visit(c) }
|
||||
to = @i += 1
|
||||
|
||||
children.each do |child|
|
||||
@tt[from, child.first] = nil
|
||||
@tt[child.last, to] = nil
|
||||
end
|
||||
|
||||
@tt.accepting = to
|
||||
|
||||
[from, to]
|
||||
end
|
||||
|
||||
def terminal(node)
|
||||
from_i = @i += 1 # new state
|
||||
to_i = @i += 1 # new state
|
||||
|
||||
@tt[from_i, to_i] = node
|
||||
@tt.accepting = to_i
|
||||
@tt.add_memo(to_i, node.memo)
|
||||
|
||||
[from_i, to_i]
|
||||
end
|
||||
end
|
||||
|
||||
class Builder # :nodoc:
|
||||
def initialize(ast)
|
||||
@ast = ast
|
||||
end
|
||||
|
||||
def transition_table
|
||||
tt = TransitionTable.new
|
||||
Visitor.new(tt).accept(@ast)
|
||||
tt
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
36
actionpack/lib/action_dispatch/journey/nfa/dot.rb
Normal file
36
actionpack/lib/action_dispatch/journey/nfa/dot.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module NFA # :nodoc:
|
||||
module Dot # :nodoc:
|
||||
def to_dot
|
||||
edges = transitions.map { |from, sym, to|
|
||||
" #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
|
||||
}
|
||||
|
||||
#memo_nodes = memos.values.flatten.map { |n|
|
||||
# label = n
|
||||
# if Journey::Route === n
|
||||
# label = "#{n.verb.source} #{n.path.spec}"
|
||||
# end
|
||||
# " #{n.object_id} [label=\"#{label}\", shape=box];"
|
||||
#}
|
||||
#memo_edges = memos.map { |k, memos|
|
||||
# (memos || []).map { |v| " #{k} -> #{v.object_id};" }
|
||||
#}.flatten.uniq
|
||||
|
||||
<<-eodot
|
||||
digraph nfa {
|
||||
rankdir=LR;
|
||||
node [shape = doublecircle];
|
||||
#{accepting_states.join ' '};
|
||||
node [shape = circle];
|
||||
#{edges.join "\n"}
|
||||
}
|
||||
eodot
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
47
actionpack/lib/action_dispatch/journey/nfa/simulator.rb
Normal file
47
actionpack/lib/action_dispatch/journey/nfa/simulator.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
require 'strscan'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module NFA # :nodoc:
|
||||
class MatchData # :nodoc:
|
||||
attr_reader :memos
|
||||
|
||||
def initialize(memos)
|
||||
@memos = memos
|
||||
end
|
||||
end
|
||||
|
||||
class Simulator # :nodoc:
|
||||
attr_reader :tt
|
||||
|
||||
def initialize(transition_table)
|
||||
@tt = transition_table
|
||||
end
|
||||
|
||||
def simulate(string)
|
||||
input = StringScanner.new(string)
|
||||
state = tt.eclosure(0)
|
||||
until input.eos?
|
||||
sym = input.scan(%r([/.?]|[^/.?]+))
|
||||
|
||||
# FIXME: tt.eclosure is not needed for the GTG
|
||||
state = tt.eclosure(tt.move(state, sym))
|
||||
end
|
||||
|
||||
acceptance_states = state.find_all { |s|
|
||||
tt.accepting?(tt.eclosure(s).sort.last)
|
||||
}
|
||||
|
||||
return if acceptance_states.empty?
|
||||
|
||||
memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact
|
||||
|
||||
MatchData.new(memos)
|
||||
end
|
||||
|
||||
alias :=~ :simulate
|
||||
alias :match :simulate
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
163
actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
Normal file
163
actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
Normal file
@@ -0,0 +1,163 @@
|
||||
require 'action_dispatch/journey/nfa/dot'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module NFA # :nodoc:
|
||||
class TransitionTable # :nodoc:
|
||||
include Journey::NFA::Dot
|
||||
|
||||
attr_accessor :accepting
|
||||
attr_reader :memos
|
||||
|
||||
def initialize
|
||||
@table = Hash.new { |h,f| h[f] = {} }
|
||||
@memos = {}
|
||||
@accepting = nil
|
||||
@inverted = nil
|
||||
end
|
||||
|
||||
def accepting?(state)
|
||||
accepting == state
|
||||
end
|
||||
|
||||
def accepting_states
|
||||
[accepting]
|
||||
end
|
||||
|
||||
def add_memo(idx, memo)
|
||||
@memos[idx] = memo
|
||||
end
|
||||
|
||||
def memo(idx)
|
||||
@memos[idx]
|
||||
end
|
||||
|
||||
def []=(i, f, s)
|
||||
@table[f][i] = s
|
||||
end
|
||||
|
||||
def merge(left, right)
|
||||
@memos[right] = @memos.delete(left)
|
||||
@table[right] = @table.delete(left)
|
||||
end
|
||||
|
||||
def states
|
||||
(@table.keys + @table.values.map(&:keys).flatten).uniq
|
||||
end
|
||||
|
||||
# Returns a generalized transition graph with reduced states. The states
|
||||
# are reduced like a DFA, but the table must be simulated like an NFA.
|
||||
#
|
||||
# Edges of the GTG are regular expressions.
|
||||
def generalized_table
|
||||
gt = GTG::TransitionTable.new
|
||||
marked = {}
|
||||
state_id = Hash.new { |h,k| h[k] = h.length }
|
||||
alphabet = self.alphabet
|
||||
|
||||
stack = [eclosure(0)]
|
||||
|
||||
until stack.empty?
|
||||
state = stack.pop
|
||||
next if marked[state] || state.empty?
|
||||
|
||||
marked[state] = true
|
||||
|
||||
alphabet.each do |alpha|
|
||||
next_state = eclosure(following_states(state, alpha))
|
||||
next if next_state.empty?
|
||||
|
||||
gt[state_id[state], state_id[next_state]] = alpha
|
||||
stack << next_state
|
||||
end
|
||||
end
|
||||
|
||||
final_groups = state_id.keys.find_all { |s|
|
||||
s.sort.last == accepting
|
||||
}
|
||||
|
||||
final_groups.each do |states|
|
||||
id = state_id[states]
|
||||
|
||||
gt.add_accepting(id)
|
||||
save = states.find { |s|
|
||||
@memos.key?(s) && eclosure(s).sort.last == accepting
|
||||
}
|
||||
|
||||
gt.add_memo(id, memo(save))
|
||||
end
|
||||
|
||||
gt
|
||||
end
|
||||
|
||||
# Returns set of NFA states to which there is a transition on ast symbol
|
||||
# +a+ from some state +s+ in +t+.
|
||||
def following_states(t, a)
|
||||
Array(t).map { |s| inverted[s][a] }.flatten.uniq
|
||||
end
|
||||
|
||||
# Returns set of NFA states to which there is a transition on ast symbol
|
||||
# +a+ from some state +s+ in +t+.
|
||||
def move(t, a)
|
||||
Array(t).map { |s|
|
||||
inverted[s].keys.compact.find_all { |sym|
|
||||
sym === a
|
||||
}.map { |sym| inverted[s][sym] }
|
||||
}.flatten.uniq
|
||||
end
|
||||
|
||||
def alphabet
|
||||
inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s }
|
||||
end
|
||||
|
||||
# Returns a set of NFA states reachable from some NFA state +s+ in set
|
||||
# +t+ on nil-transitions alone.
|
||||
def eclosure(t)
|
||||
stack = Array(t)
|
||||
seen = {}
|
||||
children = []
|
||||
|
||||
until stack.empty?
|
||||
s = stack.pop
|
||||
next if seen[s]
|
||||
|
||||
seen[s] = true
|
||||
children << s
|
||||
|
||||
stack.concat(inverted[s][nil])
|
||||
end
|
||||
|
||||
children.uniq
|
||||
end
|
||||
|
||||
def transitions
|
||||
@table.map { |to, hash|
|
||||
hash.map { |from, sym| [from, sym, to] }
|
||||
}.flatten(1)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def inverted
|
||||
return @inverted if @inverted
|
||||
|
||||
@inverted = Hash.new { |h, from|
|
||||
h[from] = Hash.new { |j, s| j[s] = [] }
|
||||
}
|
||||
|
||||
@table.each { |to, hash|
|
||||
hash.each { |from, sym|
|
||||
if sym
|
||||
sym = Nodes::Symbol === sym ? sym.regexp : sym.left
|
||||
end
|
||||
|
||||
@inverted[from][sym] << to
|
||||
}
|
||||
}
|
||||
|
||||
@inverted
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
124
actionpack/lib/action_dispatch/journey/nodes/node.rb
Normal file
124
actionpack/lib/action_dispatch/journey/nodes/node.rb
Normal file
@@ -0,0 +1,124 @@
|
||||
require 'action_dispatch/journey/visitors'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module Nodes # :nodoc:
|
||||
class Node # :nodoc:
|
||||
include Enumerable
|
||||
|
||||
attr_accessor :left, :memo
|
||||
|
||||
def initialize(left)
|
||||
@left = left
|
||||
@memo = nil
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
Visitors::Each.new(block).accept(self)
|
||||
end
|
||||
|
||||
def to_s
|
||||
Visitors::String.new.accept(self)
|
||||
end
|
||||
|
||||
def to_dot
|
||||
Visitors::Dot.new.accept(self)
|
||||
end
|
||||
|
||||
def to_sym
|
||||
name.to_sym
|
||||
end
|
||||
|
||||
def name
|
||||
left.tr '*:', ''
|
||||
end
|
||||
|
||||
def type
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def symbol?; false; end
|
||||
def literal?; false; end
|
||||
end
|
||||
|
||||
class Terminal < Node # :nodoc:
|
||||
alias :symbol :left
|
||||
end
|
||||
|
||||
class Literal < Terminal # :nodoc:
|
||||
def literal?; true; end
|
||||
def type; :LITERAL; end
|
||||
end
|
||||
|
||||
class Dummy < Literal # :nodoc:
|
||||
def initialize(x = Object.new)
|
||||
super
|
||||
end
|
||||
|
||||
def literal?; false; end
|
||||
end
|
||||
|
||||
%w{ Symbol Slash Dot }.each do |t|
|
||||
class_eval <<-eoruby, __FILE__, __LINE__ + 1
|
||||
class #{t} < Terminal;
|
||||
def type; :#{t.upcase}; end
|
||||
end
|
||||
eoruby
|
||||
end
|
||||
|
||||
class Symbol < Terminal # :nodoc:
|
||||
attr_accessor :regexp
|
||||
alias :symbol :regexp
|
||||
|
||||
DEFAULT_EXP = /[^\.\/\?]+/
|
||||
def initialize(left)
|
||||
super
|
||||
@regexp = DEFAULT_EXP
|
||||
end
|
||||
|
||||
def default_regexp?
|
||||
regexp == DEFAULT_EXP
|
||||
end
|
||||
|
||||
def symbol?; true; end
|
||||
end
|
||||
|
||||
class Unary < Node # :nodoc:
|
||||
def children; [left] end
|
||||
end
|
||||
|
||||
class Group < Unary # :nodoc:
|
||||
def type; :GROUP; end
|
||||
end
|
||||
|
||||
class Star < Unary # :nodoc:
|
||||
def type; :STAR; end
|
||||
end
|
||||
|
||||
class Binary < Node # :nodoc:
|
||||
attr_accessor :right
|
||||
|
||||
def initialize(left, right)
|
||||
super(left)
|
||||
@right = right
|
||||
end
|
||||
|
||||
def children; [left, right] end
|
||||
end
|
||||
|
||||
class Cat < Binary # :nodoc:
|
||||
def type; :CAT; end
|
||||
end
|
||||
|
||||
class Or < Node # :nodoc:
|
||||
attr_reader :children
|
||||
|
||||
def initialize(children)
|
||||
@children = children
|
||||
end
|
||||
|
||||
def type; :OR; end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
206
actionpack/lib/action_dispatch/journey/parser.rb
Normal file
206
actionpack/lib/action_dispatch/journey/parser.rb
Normal file
@@ -0,0 +1,206 @@
|
||||
#
|
||||
# DO NOT MODIFY!!!!
|
||||
# This file is automatically generated by Racc 1.4.9
|
||||
# from Racc grammer file "".
|
||||
#
|
||||
|
||||
require 'racc/parser.rb'
|
||||
|
||||
|
||||
require 'action_dispatch/journey/parser_extras'
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Parser < Racc::Parser # :nodoc:
|
||||
##### State transition tables begin ###
|
||||
|
||||
racc_action_table = [
|
||||
17, 21, 13, 15, 14, 7, nil, 16, 8, 19,
|
||||
13, 15, 14, 7, 23, 16, 8, 19, 13, 15,
|
||||
14, 7, nil, 16, 8, 13, 15, 14, 7, nil,
|
||||
16, 8, 13, 15, 14, 7, nil, 16, 8 ]
|
||||
|
||||
racc_action_check = [
|
||||
1, 17, 1, 1, 1, 1, nil, 1, 1, 1,
|
||||
20, 20, 20, 20, 20, 20, 20, 20, 7, 7,
|
||||
7, 7, nil, 7, 7, 19, 19, 19, 19, nil,
|
||||
19, 19, 0, 0, 0, 0, nil, 0, 0 ]
|
||||
|
||||
racc_action_pointer = [
|
||||
30, 0, nil, nil, nil, nil, nil, 16, nil, nil,
|
||||
nil, nil, nil, nil, nil, nil, nil, 1, nil, 23,
|
||||
8, nil, nil, nil ]
|
||||
|
||||
racc_action_default = [
|
||||
-18, -18, -2, -3, -4, -5, -6, -18, -9, -10,
|
||||
-11, -12, -13, -14, -15, -16, -17, -18, -1, -18,
|
||||
-18, 24, -8, -7 ]
|
||||
|
||||
racc_goto_table = [
|
||||
18, 1, nil, nil, nil, nil, nil, nil, 20, nil,
|
||||
nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ]
|
||||
|
||||
racc_goto_check = [
|
||||
2, 1, nil, nil, nil, nil, nil, nil, 1, nil,
|
||||
nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ]
|
||||
|
||||
racc_goto_pointer = [
|
||||
nil, 1, -1, nil, nil, nil, nil, nil, nil, nil,
|
||||
nil ]
|
||||
|
||||
racc_goto_default = [
|
||||
nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
|
||||
12 ]
|
||||
|
||||
racc_reduce_table = [
|
||||
0, 0, :racc_error,
|
||||
2, 11, :_reduce_1,
|
||||
1, 11, :_reduce_2,
|
||||
1, 11, :_reduce_none,
|
||||
1, 12, :_reduce_none,
|
||||
1, 12, :_reduce_none,
|
||||
1, 12, :_reduce_none,
|
||||
3, 15, :_reduce_7,
|
||||
3, 13, :_reduce_8,
|
||||
1, 16, :_reduce_9,
|
||||
1, 14, :_reduce_none,
|
||||
1, 14, :_reduce_none,
|
||||
1, 14, :_reduce_none,
|
||||
1, 14, :_reduce_none,
|
||||
1, 19, :_reduce_14,
|
||||
1, 17, :_reduce_15,
|
||||
1, 18, :_reduce_16,
|
||||
1, 20, :_reduce_17 ]
|
||||
|
||||
racc_reduce_n = 18
|
||||
|
||||
racc_shift_n = 24
|
||||
|
||||
racc_token_table = {
|
||||
false => 0,
|
||||
:error => 1,
|
||||
:SLASH => 2,
|
||||
:LITERAL => 3,
|
||||
:SYMBOL => 4,
|
||||
:LPAREN => 5,
|
||||
:RPAREN => 6,
|
||||
:DOT => 7,
|
||||
:STAR => 8,
|
||||
:OR => 9 }
|
||||
|
||||
racc_nt_base = 10
|
||||
|
||||
racc_use_result_var = true
|
||||
|
||||
Racc_arg = [
|
||||
racc_action_table,
|
||||
racc_action_check,
|
||||
racc_action_default,
|
||||
racc_action_pointer,
|
||||
racc_goto_table,
|
||||
racc_goto_check,
|
||||
racc_goto_default,
|
||||
racc_goto_pointer,
|
||||
racc_nt_base,
|
||||
racc_reduce_table,
|
||||
racc_token_table,
|
||||
racc_shift_n,
|
||||
racc_reduce_n,
|
||||
racc_use_result_var ]
|
||||
|
||||
Racc_token_to_s_table = [
|
||||
"$end",
|
||||
"error",
|
||||
"SLASH",
|
||||
"LITERAL",
|
||||
"SYMBOL",
|
||||
"LPAREN",
|
||||
"RPAREN",
|
||||
"DOT",
|
||||
"STAR",
|
||||
"OR",
|
||||
"$start",
|
||||
"expressions",
|
||||
"expression",
|
||||
"or",
|
||||
"terminal",
|
||||
"group",
|
||||
"star",
|
||||
"symbol",
|
||||
"literal",
|
||||
"slash",
|
||||
"dot" ]
|
||||
|
||||
Racc_debug_parser = false
|
||||
|
||||
##### State transition tables end #####
|
||||
|
||||
# reduce 0 omitted
|
||||
|
||||
def _reduce_1(val, _values, result)
|
||||
result = Cat.new(val.first, val.last)
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_2(val, _values, result)
|
||||
result = val.first
|
||||
result
|
||||
end
|
||||
|
||||
# reduce 3 omitted
|
||||
|
||||
# reduce 4 omitted
|
||||
|
||||
# reduce 5 omitted
|
||||
|
||||
# reduce 6 omitted
|
||||
|
||||
def _reduce_7(val, _values, result)
|
||||
result = Group.new(val[1])
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_8(val, _values, result)
|
||||
result = Or.new([val.first, val.last])
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_9(val, _values, result)
|
||||
result = Star.new(Symbol.new(val.last))
|
||||
result
|
||||
end
|
||||
|
||||
# reduce 10 omitted
|
||||
|
||||
# reduce 11 omitted
|
||||
|
||||
# reduce 12 omitted
|
||||
|
||||
# reduce 13 omitted
|
||||
|
||||
def _reduce_14(val, _values, result)
|
||||
result = Slash.new('/')
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_15(val, _values, result)
|
||||
result = Symbol.new(val.first)
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_16(val, _values, result)
|
||||
result = Literal.new(val.first)
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_17(val, _values, result)
|
||||
result = Dot.new(val.first)
|
||||
result
|
||||
end
|
||||
|
||||
def _reduce_none(val, _values, result)
|
||||
val[0]
|
||||
end
|
||||
|
||||
end # class Parser
|
||||
end # module Journey
|
||||
end # module ActionDispatch
|
||||
47
actionpack/lib/action_dispatch/journey/parser.y
Normal file
47
actionpack/lib/action_dispatch/journey/parser.y
Normal file
@@ -0,0 +1,47 @@
|
||||
class ActionDispatch::Journey::Parser
|
||||
|
||||
token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
|
||||
|
||||
rule
|
||||
expressions
|
||||
: expressions expression { result = Cat.new(val.first, val.last) }
|
||||
| expression { result = val.first }
|
||||
| or
|
||||
;
|
||||
expression
|
||||
: terminal
|
||||
| group
|
||||
| star
|
||||
;
|
||||
group
|
||||
: LPAREN expressions RPAREN { result = Group.new(val[1]) }
|
||||
;
|
||||
or
|
||||
: expressions OR expression { result = Or.new([val.first, val.last]) }
|
||||
;
|
||||
star
|
||||
: STAR { result = Star.new(Symbol.new(val.last)) }
|
||||
;
|
||||
terminal
|
||||
: symbol
|
||||
| literal
|
||||
| slash
|
||||
| dot
|
||||
;
|
||||
slash
|
||||
: SLASH { result = Slash.new('/') }
|
||||
;
|
||||
symbol
|
||||
: SYMBOL { result = Symbol.new(val.first) }
|
||||
;
|
||||
literal
|
||||
: LITERAL { result = Literal.new(val.first) }
|
||||
dot
|
||||
: DOT { result = Dot.new(val.first) }
|
||||
;
|
||||
|
||||
end
|
||||
|
||||
---- header
|
||||
|
||||
require 'action_dispatch/journey/parser_extras'
|
||||
23
actionpack/lib/action_dispatch/journey/parser_extras.rb
Normal file
23
actionpack/lib/action_dispatch/journey/parser_extras.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
require 'action_dispatch/journey/scanner'
|
||||
require 'action_dispatch/journey/nodes/node'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Parser < Racc::Parser # :nodoc:
|
||||
include Journey::Nodes
|
||||
|
||||
def initialize
|
||||
@scanner = Scanner.new
|
||||
end
|
||||
|
||||
def parse(string)
|
||||
@scanner.scan_setup(string)
|
||||
do_parse
|
||||
end
|
||||
|
||||
def next_token
|
||||
@scanner.next_token
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
196
actionpack/lib/action_dispatch/journey/path/pattern.rb
Normal file
196
actionpack/lib/action_dispatch/journey/path/pattern.rb
Normal file
@@ -0,0 +1,196 @@
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module Path # :nodoc:
|
||||
class Pattern # :nodoc:
|
||||
attr_reader :spec, :requirements, :anchored
|
||||
|
||||
def initialize(strexp)
|
||||
parser = Journey::Parser.new
|
||||
|
||||
@anchored = true
|
||||
|
||||
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
|
||||
@optional_names = nil
|
||||
@required_names = nil
|
||||
@re = nil
|
||||
@offsets = nil
|
||||
end
|
||||
|
||||
def ast
|
||||
@spec.grep(Nodes::Symbol).each do |node|
|
||||
re = @requirements[node.to_sym]
|
||||
node.regexp = re if re
|
||||
end
|
||||
|
||||
@spec.grep(Nodes::Star).each do |node|
|
||||
node = node.left
|
||||
node.regexp = @requirements[node.to_sym] || /(.+)/
|
||||
end
|
||||
|
||||
@spec
|
||||
end
|
||||
|
||||
def names
|
||||
@names ||= spec.grep(Nodes::Symbol).map { |n| n.name }
|
||||
end
|
||||
|
||||
def required_names
|
||||
@required_names ||= names - optional_names
|
||||
end
|
||||
|
||||
def optional_names
|
||||
@optional_names ||= spec.grep(Nodes::Group).map { |group|
|
||||
group.grep(Nodes::Symbol)
|
||||
}.flatten.map { |n| n.name }.uniq
|
||||
end
|
||||
|
||||
class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
|
||||
attr_reader :offsets
|
||||
|
||||
def initialize(matchers)
|
||||
@matchers = matchers
|
||||
@capture_count = [0]
|
||||
end
|
||||
|
||||
def visit(node)
|
||||
super
|
||||
@capture_count
|
||||
end
|
||||
|
||||
def visit_SYMBOL(node)
|
||||
node = node.to_sym
|
||||
|
||||
if @matchers.key?(node)
|
||||
re = /#{@matchers[node]}|/
|
||||
@capture_count.push((re.match('').length - 1) + (@capture_count.last || 0))
|
||||
else
|
||||
@capture_count << (@capture_count.last || 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
|
||||
def initialize(separator, matchers)
|
||||
@separator = separator
|
||||
@matchers = matchers
|
||||
@separator_re = "([^#{separator}]+)"
|
||||
super()
|
||||
end
|
||||
|
||||
def accept(node)
|
||||
%r{\A#{visit node}\Z}
|
||||
end
|
||||
|
||||
def visit_CAT(node)
|
||||
[visit(node.left), visit(node.right)].join
|
||||
end
|
||||
|
||||
def visit_SYMBOL(node)
|
||||
node = node.to_sym
|
||||
|
||||
return @separator_re unless @matchers.key?(node)
|
||||
|
||||
re = @matchers[node]
|
||||
"(#{re})"
|
||||
end
|
||||
|
||||
def visit_GROUP(node)
|
||||
"(?:#{visit node.left})?"
|
||||
end
|
||||
|
||||
def visit_LITERAL(node)
|
||||
Regexp.escape(node.left)
|
||||
end
|
||||
alias :visit_DOT :visit_LITERAL
|
||||
|
||||
def visit_SLASH(node)
|
||||
node.left
|
||||
end
|
||||
|
||||
def visit_STAR(node)
|
||||
re = @matchers[node.left.to_sym] || '.+'
|
||||
"(#{re})"
|
||||
end
|
||||
end
|
||||
|
||||
class UnanchoredRegexp < AnchoredRegexp # :nodoc:
|
||||
def accept(node)
|
||||
%r{\A#{visit node}}
|
||||
end
|
||||
end
|
||||
|
||||
class MatchData # :nodoc:
|
||||
attr_reader :names
|
||||
|
||||
def initialize(names, offsets, match)
|
||||
@names = names
|
||||
@offsets = offsets
|
||||
@match = match
|
||||
end
|
||||
|
||||
def captures
|
||||
(length - 1).times.map { |i| self[i + 1] }
|
||||
end
|
||||
|
||||
def [](x)
|
||||
idx = @offsets[x - 1] + x
|
||||
@match[idx]
|
||||
end
|
||||
|
||||
def length
|
||||
@offsets.length
|
||||
end
|
||||
|
||||
def post_match
|
||||
@match.post_match
|
||||
end
|
||||
|
||||
def to_s
|
||||
@match.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def match(other)
|
||||
return unless match = to_regexp.match(other)
|
||||
MatchData.new(names, offsets, match)
|
||||
end
|
||||
alias :=~ :match
|
||||
|
||||
def source
|
||||
to_regexp.source
|
||||
end
|
||||
|
||||
def to_regexp
|
||||
@re ||= regexp_visitor.new(@separators, @requirements).accept spec
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def regexp_visitor
|
||||
@anchored ? AnchoredRegexp : UnanchoredRegexp
|
||||
end
|
||||
|
||||
def offsets
|
||||
return @offsets if @offsets
|
||||
|
||||
viz = RegexpOffsets.new(@requirements)
|
||||
@offsets = viz.accept(spec)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
120
actionpack/lib/action_dispatch/journey/route.rb
Normal file
120
actionpack/lib/action_dispatch/journey/route.rb
Normal file
@@ -0,0 +1,120 @@
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Route # :nodoc:
|
||||
attr_reader :app, :path, :defaults, :name
|
||||
|
||||
attr_reader :constraints
|
||||
alias :conditions :constraints
|
||||
|
||||
attr_accessor :precedence
|
||||
|
||||
##
|
||||
# +path+ is a path constraint.
|
||||
# +constraints+ is a hash of constraints to be applied to this route.
|
||||
def initialize(name, app, path, constraints, defaults = {})
|
||||
@name = name
|
||||
@app = app
|
||||
@path = path
|
||||
|
||||
@constraints = constraints
|
||||
@defaults = defaults
|
||||
@required_defaults = nil
|
||||
@required_parts = nil
|
||||
@parts = nil
|
||||
@decorated_ast = nil
|
||||
@precedence = 0
|
||||
end
|
||||
|
||||
def ast
|
||||
@decorated_ast ||= begin
|
||||
decorated_ast = path.ast
|
||||
decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self }
|
||||
decorated_ast
|
||||
end
|
||||
end
|
||||
|
||||
def requirements # :nodoc:
|
||||
# needed for rails `rake routes`
|
||||
path.requirements.merge(@defaults).delete_if { |_,v|
|
||||
/.+?/ == v
|
||||
}
|
||||
end
|
||||
|
||||
def segments
|
||||
path.names
|
||||
end
|
||||
|
||||
def required_keys
|
||||
required_parts + required_defaults.keys
|
||||
end
|
||||
|
||||
def score(constraints)
|
||||
required_keys = path.required_names
|
||||
supplied_keys = constraints.map { |k,v| v && k.to_s }.compact
|
||||
|
||||
return -1 unless (required_keys - supplied_keys).empty?
|
||||
|
||||
score = (supplied_keys & path.names).length
|
||||
score + (required_defaults.length * 2)
|
||||
end
|
||||
|
||||
def parts
|
||||
@parts ||= segments.map { |n| n.to_sym }
|
||||
end
|
||||
alias :segment_keys :parts
|
||||
|
||||
def format(path_options)
|
||||
path_options.delete_if do |key, value|
|
||||
value.to_s == defaults[key].to_s && !required_parts.include?(key)
|
||||
end
|
||||
|
||||
Visitors::Formatter.new(path_options).accept(path.spec)
|
||||
end
|
||||
|
||||
def optimized_path
|
||||
Visitors::OptimizedPath.new.accept(path.spec)
|
||||
end
|
||||
|
||||
def optional_parts
|
||||
path.optional_names.map { |n| n.to_sym }
|
||||
end
|
||||
|
||||
def required_parts
|
||||
@required_parts ||= path.required_names.map { |n| n.to_sym }
|
||||
end
|
||||
|
||||
def required_default?(key)
|
||||
(constraints[:required_defaults] || []).include?(key)
|
||||
end
|
||||
|
||||
def required_defaults
|
||||
@required_defaults ||= @defaults.dup.delete_if do |k,_|
|
||||
parts.include?(k) || !required_default?(k)
|
||||
end
|
||||
end
|
||||
|
||||
def matches?(request)
|
||||
constraints.all? do |method, value|
|
||||
next true unless request.respond_to?(method)
|
||||
|
||||
case value
|
||||
when Regexp, String
|
||||
value === request.send(method).to_s
|
||||
when Array
|
||||
value.include?(request.send(method))
|
||||
else
|
||||
value === request.send(method)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ip
|
||||
constraints[:ip] || //
|
||||
end
|
||||
|
||||
def verb
|
||||
constraints[:request_method_string] || //
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
163
actionpack/lib/action_dispatch/journey/router.rb
Normal file
163
actionpack/lib/action_dispatch/journey/router.rb
Normal file
@@ -0,0 +1,163 @@
|
||||
require 'action_dispatch/journey/router/utils'
|
||||
require 'action_dispatch/journey/router/strexp'
|
||||
require 'action_dispatch/journey/routes'
|
||||
require 'action_dispatch/journey/formatter'
|
||||
|
||||
before = $-w
|
||||
$-w = false
|
||||
require 'action_dispatch/journey/parser'
|
||||
$-w = before
|
||||
|
||||
require 'action_dispatch/journey/route'
|
||||
require 'action_dispatch/journey/path/pattern'
|
||||
require 'active_support/core_ext/array/sort_by'
|
||||
require 'active_support/core_ext/array/select'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Router # :nodoc:
|
||||
# :nodoc:
|
||||
VERSION = '2.0.0'
|
||||
|
||||
class NullReq # :nodoc:
|
||||
attr_reader :env
|
||||
def initialize(env)
|
||||
@env = env
|
||||
end
|
||||
|
||||
def request_method_string
|
||||
env['REQUEST_METHOD']
|
||||
end
|
||||
|
||||
def path_info
|
||||
env['PATH_INFO']
|
||||
end
|
||||
|
||||
def ip
|
||||
env['REMOTE_ADDR']
|
||||
end
|
||||
|
||||
def [](k); env[k]; end
|
||||
end
|
||||
|
||||
attr_reader :request_class, :formatter
|
||||
attr_accessor :routes
|
||||
|
||||
def initialize(routes, options)
|
||||
@options = options
|
||||
@params_key = options[:parameters_key]
|
||||
@request_class = options[:request_class] || NullReq
|
||||
@routes = routes
|
||||
end
|
||||
|
||||
def call(env)
|
||||
env['PATH_INFO'] = Utils.normalize_path(env['PATH_INFO'])
|
||||
|
||||
find_routes(env).each do |match, parameters, route|
|
||||
script_name, path_info, set_params = env.values_at('SCRIPT_NAME',
|
||||
'PATH_INFO',
|
||||
@params_key)
|
||||
|
||||
unless route.path.anchored
|
||||
env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/')
|
||||
env['PATH_INFO'] = match.post_match
|
||||
end
|
||||
|
||||
env[@params_key] = (set_params || {}).merge parameters
|
||||
|
||||
status, headers, body = route.app.call(env)
|
||||
|
||||
if 'pass' == headers['X-Cascade']
|
||||
env['SCRIPT_NAME'] = script_name
|
||||
env['PATH_INFO'] = path_info
|
||||
env[@params_key] = set_params
|
||||
next
|
||||
end
|
||||
|
||||
return [status, headers, body]
|
||||
end
|
||||
|
||||
raise ActionController::RoutingError, "No route matches"
|
||||
end
|
||||
|
||||
def recognize(req)
|
||||
find_routes(req.env).each do |match, parameters, route|
|
||||
unless route.path.anchored
|
||||
req.env['SCRIPT_NAME'] = match.to_s
|
||||
req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1')
|
||||
end
|
||||
|
||||
yield(route, nil, parameters)
|
||||
end
|
||||
end
|
||||
|
||||
def visualizer
|
||||
tt = GTG::Builder.new(ast).transition_table
|
||||
groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s }
|
||||
asts = groups.values.map { |v| v.first }
|
||||
tt.visualizer(asts)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def partitioned_routes
|
||||
routes.partitioned_routes
|
||||
end
|
||||
|
||||
def ast
|
||||
routes.ast
|
||||
end
|
||||
|
||||
def simulator
|
||||
routes.simulator
|
||||
end
|
||||
|
||||
def custom_routes
|
||||
partitioned_routes.last
|
||||
end
|
||||
|
||||
def filter_routes(path)
|
||||
return [] unless ast
|
||||
data = simulator.match(path)
|
||||
data ? data.memos : []
|
||||
end
|
||||
|
||||
def find_routes env
|
||||
req = request_class.new(env)
|
||||
|
||||
routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
|
||||
r.path.match(req.path_info)
|
||||
}
|
||||
routes.concat get_routes_as_head(routes)
|
||||
|
||||
routes.sort_by!(&:precedence).select! { |r| r.matches?(req) }
|
||||
|
||||
routes.map! { |r|
|
||||
match_data = r.path.match(req.path_info)
|
||||
match_names = match_data.names.map { |n| n.to_sym }
|
||||
match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) }
|
||||
info = Hash[match_names.zip(match_values).find_all { |_, y| y }]
|
||||
|
||||
[match_data, r.defaults.merge(info), r]
|
||||
}
|
||||
end
|
||||
|
||||
def get_routes_as_head(routes)
|
||||
precedence = (routes.map(&:precedence).max || 0) + 1
|
||||
routes = routes.select { |r|
|
||||
r.verb === "GET" && !(r.verb === "HEAD")
|
||||
}.map! { |r|
|
||||
Route.new(r.name,
|
||||
r.app,
|
||||
r.path,
|
||||
r.conditions.merge(:request_method_string => "HEAD"),
|
||||
r.defaults).tap do |route|
|
||||
route.precedence = r.precedence + precedence
|
||||
end
|
||||
}
|
||||
routes.flatten!
|
||||
routes
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
actionpack/lib/action_dispatch/journey/router/strexp.rb
Normal file
24
actionpack/lib/action_dispatch/journey/router/strexp.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Router # :nodoc:
|
||||
class Strexp # :nodoc:
|
||||
class << self
|
||||
alias :compile :new
|
||||
end
|
||||
|
||||
attr_reader :path, :requirements, :separators, :anchor
|
||||
|
||||
def initialize(path, requirements, separators, anchor = true)
|
||||
@path = path
|
||||
@requirements = requirements
|
||||
@separators = separators
|
||||
@anchor = anchor
|
||||
end
|
||||
|
||||
def names
|
||||
@path.scan(/:\w+/).map { |s| s.tr(':', '') }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
54
actionpack/lib/action_dispatch/journey/router/utils.rb
Normal file
54
actionpack/lib/action_dispatch/journey/router/utils.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
require 'uri'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Router # :nodoc:
|
||||
class Utils # :nodoc:
|
||||
# Normalizes URI path.
|
||||
#
|
||||
# Strips off trailing slash and ensures there is a leading slash.
|
||||
#
|
||||
# normalize_path("/foo") # => "/foo"
|
||||
# normalize_path("/foo/") # => "/foo"
|
||||
# normalize_path("foo") # => "/foo"
|
||||
# normalize_path("") # => "/"
|
||||
def self.normalize_path(path)
|
||||
path = "/#{path}"
|
||||
path.squeeze!('/')
|
||||
path.sub!(%r{/+\Z}, '')
|
||||
path = '/' if path == ''
|
||||
path
|
||||
end
|
||||
|
||||
# URI path and fragment escaping
|
||||
# http://tools.ietf.org/html/rfc3986
|
||||
module UriEscape # :nodoc:
|
||||
# Symbol captures can generate multiple path segments, so include /.
|
||||
reserved_segment = '/'
|
||||
reserved_fragment = '/?'
|
||||
reserved_pchar = ':@&=+$,;%'
|
||||
|
||||
safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}"
|
||||
safe_segment = "#{safe_pchar}#{reserved_segment}"
|
||||
safe_fragment = "#{safe_pchar}#{reserved_fragment}"
|
||||
UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze
|
||||
UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze
|
||||
end
|
||||
|
||||
Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
||||
|
||||
def self.escape_path(path)
|
||||
Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT)
|
||||
end
|
||||
|
||||
def self.escape_fragment(fragment)
|
||||
Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT)
|
||||
end
|
||||
|
||||
def self.unescape_uri(uri)
|
||||
Parser.unescape(uri)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
75
actionpack/lib/action_dispatch/journey/routes.rb
Normal file
75
actionpack/lib/action_dispatch/journey/routes.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
# The Routing table. Contains all routes for a system. Routes can be
|
||||
# added to the table by calling Routes#add_route.
|
||||
class Routes # :nodoc:
|
||||
include Enumerable
|
||||
|
||||
attr_reader :routes, :named_routes
|
||||
|
||||
def initialize
|
||||
@routes = []
|
||||
@named_routes = {}
|
||||
@ast = nil
|
||||
@partitioned_routes = nil
|
||||
@simulator = nil
|
||||
end
|
||||
|
||||
def length
|
||||
routes.length
|
||||
end
|
||||
alias :size :length
|
||||
|
||||
def last
|
||||
routes.last
|
||||
end
|
||||
|
||||
def each(&block)
|
||||
routes.each(&block)
|
||||
end
|
||||
|
||||
def clear
|
||||
routes.clear
|
||||
end
|
||||
|
||||
def partitioned_routes
|
||||
@partitioned_routes ||= routes.partition do |r|
|
||||
r.path.anchored && r.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
|
||||
end
|
||||
end
|
||||
|
||||
def ast
|
||||
@ast ||= begin
|
||||
asts = partitioned_routes.first.map(&:ast)
|
||||
Nodes::Or.new(asts) unless asts.empty?
|
||||
end
|
||||
end
|
||||
|
||||
def simulator
|
||||
@simulator ||= begin
|
||||
gtg = GTG::Builder.new(ast).transition_table
|
||||
GTG::Simulator.new(gtg)
|
||||
end
|
||||
end
|
||||
|
||||
# Add a route to the routing table.
|
||||
def add_route(app, path, conditions, defaults, name = nil)
|
||||
route = Route.new(name, app, path, conditions, defaults)
|
||||
|
||||
route.precedence = routes.length
|
||||
routes << route
|
||||
named_routes[name] = route if name && !named_routes[name]
|
||||
clear_cache!
|
||||
route
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def clear_cache!
|
||||
@ast = nil
|
||||
@partitioned_routes = nil
|
||||
@simulator = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
61
actionpack/lib/action_dispatch/journey/scanner.rb
Normal file
61
actionpack/lib/action_dispatch/journey/scanner.rb
Normal file
@@ -0,0 +1,61 @@
|
||||
require 'strscan'
|
||||
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
class Scanner # :nodoc:
|
||||
def initialize
|
||||
@ss = nil
|
||||
end
|
||||
|
||||
def scan_setup(str)
|
||||
@ss = StringScanner.new(str)
|
||||
end
|
||||
|
||||
def eos?
|
||||
@ss.eos?
|
||||
end
|
||||
|
||||
def pos
|
||||
@ss.pos
|
||||
end
|
||||
|
||||
def pre_match
|
||||
@ss.pre_match
|
||||
end
|
||||
|
||||
def next_token
|
||||
return if @ss.eos?
|
||||
|
||||
until token = scan || @ss.eos?; end
|
||||
token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scan
|
||||
case
|
||||
# /
|
||||
when text = @ss.scan(/\//)
|
||||
[:SLASH, text]
|
||||
when text = @ss.scan(/\*\w+/)
|
||||
[:STAR, text]
|
||||
when text = @ss.scan(/\(/)
|
||||
[:LPAREN, text]
|
||||
when text = @ss.scan(/\)/)
|
||||
[:RPAREN, text]
|
||||
when text = @ss.scan(/\|/)
|
||||
[:OR, text]
|
||||
when text = @ss.scan(/\./)
|
||||
[:DOT, text]
|
||||
when text = @ss.scan(/:\w+/)
|
||||
[:SYMBOL, text]
|
||||
when text = @ss.scan(/[\w%\-~]+/)
|
||||
[:LITERAL, text]
|
||||
# any char
|
||||
when text = @ss.scan(/./)
|
||||
[:LITERAL, text]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
197
actionpack/lib/action_dispatch/journey/visitors.rb
Normal file
197
actionpack/lib/action_dispatch/journey/visitors.rb
Normal file
@@ -0,0 +1,197 @@
|
||||
# encoding: utf-8
|
||||
module ActionDispatch
|
||||
module Journey # :nodoc:
|
||||
module Visitors # :nodoc:
|
||||
class Visitor # :nodoc:
|
||||
DISPATCH_CACHE = Hash.new { |h,k|
|
||||
h[k] = "visit_#{k}"
|
||||
}
|
||||
|
||||
def accept(node)
|
||||
visit(node)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visit node
|
||||
send(DISPATCH_CACHE[node.type], node)
|
||||
end
|
||||
|
||||
def binary(node)
|
||||
visit(node.left)
|
||||
visit(node.right)
|
||||
end
|
||||
def visit_CAT(n); binary(n); end
|
||||
|
||||
def nary(node)
|
||||
node.children.each { |c| visit(c) }
|
||||
end
|
||||
def visit_OR(n); nary(n); end
|
||||
|
||||
def unary(node)
|
||||
visit(node.left)
|
||||
end
|
||||
def visit_GROUP(n); unary(n); end
|
||||
def visit_STAR(n); unary(n); end
|
||||
|
||||
def terminal(node); end
|
||||
%w{ LITERAL SYMBOL SLASH DOT }.each do |t|
|
||||
class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__
|
||||
end
|
||||
end
|
||||
|
||||
# Loop through the requirements AST
|
||||
class Each < Visitor # :nodoc:
|
||||
attr_reader :block
|
||||
|
||||
def initialize(block)
|
||||
@block = block
|
||||
end
|
||||
|
||||
def visit(node)
|
||||
super
|
||||
block.call(node)
|
||||
end
|
||||
end
|
||||
|
||||
class String < Visitor # :nodoc:
|
||||
private
|
||||
|
||||
def binary(node)
|
||||
[visit(node.left), visit(node.right)].join
|
||||
end
|
||||
|
||||
def nary(node)
|
||||
node.children.map { |c| visit(c) }.join '|'
|
||||
end
|
||||
|
||||
def terminal(node)
|
||||
node.left
|
||||
end
|
||||
|
||||
def visit_GROUP(node)
|
||||
"(#{visit(node.left)})"
|
||||
end
|
||||
end
|
||||
|
||||
class OptimizedPath < String # :nodoc:
|
||||
private
|
||||
|
||||
def visit_GROUP(node)
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
# Used for formatting urls (url_for)
|
||||
class Formatter < Visitor # :nodoc:
|
||||
attr_reader :options, :consumed
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
@consumed = {}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visit_GROUP(node)
|
||||
if consumed == options
|
||||
nil
|
||||
else
|
||||
route = visit(node.left)
|
||||
route.include?("\0") ? nil : route
|
||||
end
|
||||
end
|
||||
|
||||
def terminal(node)
|
||||
node.left
|
||||
end
|
||||
|
||||
def binary(node)
|
||||
[visit(node.left), visit(node.right)].join
|
||||
end
|
||||
|
||||
def nary(node)
|
||||
node.children.map { |c| visit(c) }.join
|
||||
end
|
||||
|
||||
def visit_SYMBOL(node)
|
||||
key = node.to_sym
|
||||
|
||||
if value = options[key]
|
||||
consumed[key] = value
|
||||
Router::Utils.escape_path(value)
|
||||
else
|
||||
"\0"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Dot < Visitor # :nodoc:
|
||||
def initialize
|
||||
@nodes = []
|
||||
@edges = []
|
||||
end
|
||||
|
||||
def accept(node)
|
||||
super
|
||||
<<-eodot
|
||||
digraph parse_tree {
|
||||
size="8,5"
|
||||
node [shape = none];
|
||||
edge [dir = none];
|
||||
#{@nodes.join "\n"}
|
||||
#{@edges.join("\n")}
|
||||
}
|
||||
eodot
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def binary(node)
|
||||
node.children.each do |c|
|
||||
@edges << "#{node.object_id} -> #{c.object_id};"
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def nary(node)
|
||||
node.children.each do |c|
|
||||
@edges << "#{node.object_id} -> #{c.object_id};"
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
def unary(node)
|
||||
@edges << "#{node.object_id} -> #{node.left.object_id};"
|
||||
super
|
||||
end
|
||||
|
||||
def visit_GROUP(node)
|
||||
@nodes << "#{node.object_id} [label=\"()\"];"
|
||||
super
|
||||
end
|
||||
|
||||
def visit_CAT(node)
|
||||
@nodes << "#{node.object_id} [label=\"○\"];"
|
||||
super
|
||||
end
|
||||
|
||||
def visit_STAR(node)
|
||||
@nodes << "#{node.object_id} [label=\"*\"];"
|
||||
super
|
||||
end
|
||||
|
||||
def visit_OR(node)
|
||||
@nodes << "#{node.object_id} [label=\"|\"];"
|
||||
super
|
||||
end
|
||||
|
||||
def terminal(node)
|
||||
value = node.left
|
||||
|
||||
@nodes << "#{node.object_id} [label=\"#{value}\"];"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
actionpack/lib/action_dispatch/journey/visualizer/fsm.css
Normal file
34
actionpack/lib/action_dispatch/journey/visualizer/fsm.css
Normal file
@@ -0,0 +1,34 @@
|
||||
body {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, Sans-Serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.0em; font-weight: bold; text-align: center;
|
||||
color: white; background-color: black;
|
||||
padding: 5px 0;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
text-align: center;
|
||||
display: none;
|
||||
font-size: 0.5em;
|
||||
}
|
||||
|
||||
div#chart-2 {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.clearfix {display: inline-block; }
|
||||
.input { overflow: show;}
|
||||
.instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em}
|
||||
.instruction p { padding: 0 0 5px; }
|
||||
.instruction li { padding: 0 10px 5px; }
|
||||
|
||||
.form { background: #EEE; padding: 20px 30px; border-radius: 5px; margin-left: auto; margin-right: auto; width: 500px; margin-bottom: 20px}
|
||||
.form p, .form form { text-align: center }
|
||||
.form form {padding: 0 10px 5px; }
|
||||
.form .fun_routes { font-size: 0.9em;}
|
||||
.form .fun_routes a { margin: 0 5px 0 0; }
|
||||
|
||||
134
actionpack/lib/action_dispatch/journey/visualizer/fsm.js
Normal file
134
actionpack/lib/action_dispatch/journey/visualizer/fsm.js
Normal file
@@ -0,0 +1,134 @@
|
||||
function tokenize(input, callback) {
|
||||
while(input.length > 0) {
|
||||
callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]);
|
||||
input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, '');
|
||||
}
|
||||
}
|
||||
|
||||
var graph = d3.select("#chart-2 svg");
|
||||
var svg_edges = {};
|
||||
var svg_nodes = {};
|
||||
|
||||
graph.selectAll("g.edge").each(function() {
|
||||
var node = d3.select(this);
|
||||
var index = node.select("title").text().split("->");
|
||||
var left = parseInt(index[0]);
|
||||
var right = parseInt(index[1]);
|
||||
|
||||
if(!svg_edges[left]) { svg_edges[left] = {} }
|
||||
svg_edges[left][right] = node;
|
||||
});
|
||||
|
||||
graph.selectAll("g.node").each(function() {
|
||||
var node = d3.select(this);
|
||||
var index = parseInt(node.select("title").text());
|
||||
svg_nodes[index] = node;
|
||||
});
|
||||
|
||||
function reset_graph() {
|
||||
for(var key in svg_edges) {
|
||||
for(var mkey in svg_edges[key]) {
|
||||
var node = svg_edges[key][mkey];
|
||||
var path = node.select("path");
|
||||
var arrow = node.select("polygon");
|
||||
path.style("stroke", "black");
|
||||
arrow.style("stroke", "black").style("fill", "black");
|
||||
}
|
||||
}
|
||||
|
||||
for(var key in svg_nodes) {
|
||||
var node = svg_nodes[key];
|
||||
node.select('ellipse').style("fill", "white");
|
||||
node.select('polygon').style("fill", "white");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function highlight_edge(from, to) {
|
||||
var node = svg_edges[from][to];
|
||||
var path = node.select("path");
|
||||
var arrow = node.select("polygon");
|
||||
|
||||
path
|
||||
.transition().duration(500)
|
||||
.style("stroke", "green");
|
||||
|
||||
arrow
|
||||
.transition().duration(500)
|
||||
.style("stroke", "green").style("fill", "green");
|
||||
}
|
||||
|
||||
function highlight_state(index, color) {
|
||||
if(!color) { color = "green"; }
|
||||
|
||||
svg_nodes[index].select('ellipse')
|
||||
.style("fill", "white")
|
||||
.transition().duration(500)
|
||||
.style("fill", color);
|
||||
}
|
||||
|
||||
function highlight_finish(index) {
|
||||
svg_nodes[index].select('polygon')
|
||||
.style("fill", "while")
|
||||
.transition().duration(500)
|
||||
.style("fill", "blue");
|
||||
}
|
||||
|
||||
function match(input) {
|
||||
reset_graph();
|
||||
var table = tt();
|
||||
var states = [0];
|
||||
var regexp_states = table['regexp_states'];
|
||||
var string_states = table['string_states'];
|
||||
var accepting = table['accepting'];
|
||||
|
||||
highlight_state(0);
|
||||
|
||||
tokenize(input, function(token) {
|
||||
var new_states = [];
|
||||
for(var key in states) {
|
||||
var state = states[key];
|
||||
|
||||
if(string_states[state] && string_states[state][token]) {
|
||||
var new_state = string_states[state][token];
|
||||
highlight_edge(state, new_state);
|
||||
highlight_state(new_state);
|
||||
new_states.push(new_state);
|
||||
}
|
||||
|
||||
if(regexp_states[state]) {
|
||||
for(var key in regexp_states[state]) {
|
||||
var re = new RegExp("^" + key + "$");
|
||||
if(re.test(token)) {
|
||||
var new_state = regexp_states[state][key];
|
||||
highlight_edge(state, new_state);
|
||||
highlight_state(new_state);
|
||||
new_states.push(new_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(new_states.length == 0) {
|
||||
return;
|
||||
}
|
||||
states = new_states;
|
||||
});
|
||||
|
||||
for(var key in states) {
|
||||
var state = states[key];
|
||||
if(accepting[state]) {
|
||||
for(var mkey in svg_edges[state]) {
|
||||
if(!regexp_states[mkey] && !string_states[mkey]) {
|
||||
highlight_edge(state, mkey);
|
||||
highlight_finish(mkey);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
highlight_state(state, "red");
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title><%= title %></title>
|
||||
<link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/reset.css" type="text/css">
|
||||
<style>
|
||||
<% stylesheets.each do |style| %>
|
||||
<%= style %>
|
||||
<% end %>
|
||||
</style>
|
||||
<script src="https://raw.github.com/gist/1706081/df464722a05c3c2bec450b7b5c8240d9c31fa52d/d3.min.js" type="text/javascript"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="wrapper">
|
||||
<h1>Routes FSM with NFA simulation</h1>
|
||||
<div class="instruction form">
|
||||
<p>
|
||||
Type a route in to the box and click "simulate".
|
||||
</p>
|
||||
<form onsubmit="return match(this.route.value);">
|
||||
<input type="text" size="30" name="route" value="/articles/new" />
|
||||
<button>simulate</button>
|
||||
<input type="reset" value="reset" onclick="return reset_graph();"/>
|
||||
</form>
|
||||
<p class="fun_routes">
|
||||
Some fun routes to try:
|
||||
<% fun_routes.each do |path| %>
|
||||
<a href="#" onclick="document.forms[0].elements[0].value=this.text.replace(/^\s+|\s+$/g,''); return match(this.text.replace(/^\s+|\s+$/g,''));">
|
||||
<%= path %>
|
||||
</a>
|
||||
<% end %>
|
||||
</p>
|
||||
</div>
|
||||
<div class='chart' id='chart-2'>
|
||||
<%= svg %>
|
||||
</div>
|
||||
<div class="instruction">
|
||||
<p>
|
||||
This is a FSM for a system that has the following routes:
|
||||
</p>
|
||||
<ul>
|
||||
<% paths.each do |route| %>
|
||||
<li><%= route %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<% javascripts.each do |js| %>
|
||||
<script><%= js %></script>
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
@@ -768,7 +768,11 @@ module ActionView
|
||||
options = options.stringify_keys
|
||||
tag_value = options.delete("value")
|
||||
name_and_id = options.dup
|
||||
name_and_id["id"] = name_and_id["for"]
|
||||
if name_and_id.has_key?("for")
|
||||
name_and_id["id"] = name_and_id["for"]
|
||||
else
|
||||
name_and_id.delete("id")
|
||||
end
|
||||
add_default_name_and_id_for_value(tag_value, name_and_id)
|
||||
options.delete("index")
|
||||
options["for"] ||= name_and_id["id"]
|
||||
@@ -928,15 +932,15 @@ module ActionView
|
||||
|
||||
def add_default_name_and_id(options)
|
||||
if options.has_key?("index")
|
||||
options["name"] ||= tag_name_with_index(options["index"])
|
||||
options["id"] ||= tag_id_with_index(options["index"])
|
||||
options["name"] = tag_name_with_index(options["index"]) unless options.has_key?("name")
|
||||
options["id"] = tag_id_with_index(options["index"]) unless options.has_key?("id")
|
||||
options.delete("index")
|
||||
elsif defined?(@auto_index)
|
||||
options["name"] ||= tag_name_with_index(@auto_index)
|
||||
options["id"] ||= tag_id_with_index(@auto_index)
|
||||
options["name"] = tag_name_with_index(@auto_index) unless options.has_key?("name")
|
||||
options["id"] = tag_id_with_index(@auto_index) unless options.has_key?("id")
|
||||
else
|
||||
options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
|
||||
options["id"] ||= tag_id
|
||||
options["name"] = tag_name + (options.has_key?('multiple') ? '[]' : '') unless options.has_key?("name")
|
||||
options["id"] = tag_id unless options.has_key?("id")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -85,11 +85,13 @@ module ActionView
|
||||
separator = '' if precision == 0
|
||||
|
||||
begin
|
||||
format.gsub(/%n/, number_with_precision(number,
|
||||
value = number_with_precision(number,
|
||||
:precision => precision,
|
||||
:delimiter => delimiter,
|
||||
:separator => separator)
|
||||
).gsub(/%u/, unit).html_safe
|
||||
value = ERB::Util.html_escape(value) if value
|
||||
unit = ERB::Util.html_escape(unit)
|
||||
format.gsub(/%n/, value).gsub(/%u/, unit).html_safe
|
||||
rescue
|
||||
number
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#require 'action_view/helpers/javascript_helper'
|
||||
require 'active_support/concern'
|
||||
|
||||
module ActionView
|
||||
module Helpers #:nodoc:
|
||||
@@ -9,6 +10,25 @@ module ActionView
|
||||
module UrlHelper
|
||||
include JavaScriptHelper
|
||||
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include ActionController::Routing::UrlFor
|
||||
include TagHelper
|
||||
|
||||
def url_options
|
||||
return super unless @controller.respond_to?(:url_options)
|
||||
@controller.url_options
|
||||
end
|
||||
|
||||
def _routes_context
|
||||
@controller
|
||||
end
|
||||
|
||||
def optimize_routes_generation?
|
||||
@controller.respond_to?(:optimize_routes_generation?, true) ?
|
||||
@controller.optimize_routes_generation? : super
|
||||
end
|
||||
|
||||
# Returns the URL for the set of +options+ provided. This takes the
|
||||
# same options as +url_for+ in Action Controller (see the
|
||||
# documentation for ActionController::Base#url_for). Note that by default
|
||||
@@ -67,20 +87,18 @@ module ActionView
|
||||
# # if request.env["HTTP_REFERER"] is not set or is blank
|
||||
# # => javascript:history.back()
|
||||
def url_for(options = {})
|
||||
options ||= {}
|
||||
url = case options
|
||||
case options
|
||||
when String
|
||||
options
|
||||
when Hash
|
||||
options = { :only_path => options[:host].nil? }.update(options.symbolize_keys)
|
||||
@controller.send(:url_for, options)
|
||||
when nil, Hash
|
||||
options ||= {}
|
||||
options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys)
|
||||
super
|
||||
when :back
|
||||
@controller.request.env["HTTP_REFERER"] || 'javascript:history.back()'
|
||||
else
|
||||
polymorphic_path(options)
|
||||
end
|
||||
|
||||
url
|
||||
end
|
||||
|
||||
# Creates a link tag of the given +name+ using a URL created by the set
|
||||
|
||||
@@ -17,7 +17,7 @@ module ActionView
|
||||
src << "@output_buffer.safe_append='"
|
||||
src << "\n" * @newline_pending if @newline_pending > 0
|
||||
src << escape_text(text)
|
||||
src << "';"
|
||||
src << "'.freeze;"
|
||||
|
||||
@newline_pending = 0
|
||||
end
|
||||
@@ -63,7 +63,7 @@ module ActionView
|
||||
|
||||
def flush_newline_if_pending(src)
|
||||
if @newline_pending > 0
|
||||
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';"
|
||||
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
|
||||
@newline_pending = 0
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,7 +41,6 @@ module ActionView
|
||||
include ActionController::TestCase::Assertions
|
||||
include ActionController::TestProcess
|
||||
|
||||
include ActionController::PolymorphicRoutes
|
||||
include ActionController::RecordIdentifier
|
||||
|
||||
include ActionView::Helpers
|
||||
|
||||
@@ -622,6 +622,19 @@ class FragmentCachingTest < ActionController::TestCase
|
||||
assert_equal 'generated till now -> fragment content', buffer
|
||||
end
|
||||
|
||||
def test_fragment_for_bytesize
|
||||
buffer = "\xC4\x8D"
|
||||
buffer.force_encoding('ASCII-8BIT')
|
||||
|
||||
@controller.fragment_for(buffer, 'bytesize') do
|
||||
buffer.force_encoding('UTF-8')
|
||||
buffer << "abc"
|
||||
end
|
||||
|
||||
assert_equal Encoding::UTF_8, buffer.encoding
|
||||
assert_equal "abc", @store.read('views/bytesize')
|
||||
end
|
||||
|
||||
def test_html_safety
|
||||
assert_nil @store.read('views/name')
|
||||
content = 'value'.html_safe
|
||||
|
||||
@@ -45,7 +45,7 @@ class DispatcherTest < Test::Unit::TestCase
|
||||
def test_rebuilds_middleware_stack_on_every_request_if_in_loading_mode
|
||||
dispatcher = create_dispatcher(false)
|
||||
dispatcher.instance_variable_set(:"@app", lambda { |env| })
|
||||
dispatcher.expects(:build_middleware_stack).twice
|
||||
dispatcher.expects(:build_middleware_stack).never
|
||||
dispatcher.call(nil)
|
||||
Reloader.default_lock.unlock
|
||||
dispatcher.call(nil)
|
||||
|
||||
@@ -1,297 +0,0 @@
|
||||
require 'abstract_unit'
|
||||
|
||||
class Article
|
||||
attr_reader :id
|
||||
def save; @id = 1 end
|
||||
def new_record?; @id.nil? end
|
||||
def name
|
||||
model = self.class.name.downcase
|
||||
@id.nil? ? "new #{model}" : "#{model} ##{@id}"
|
||||
end
|
||||
end
|
||||
|
||||
class Response < Article
|
||||
def post_id; 1 end
|
||||
end
|
||||
|
||||
class Tag < Article
|
||||
def response_id; 1 end
|
||||
end
|
||||
|
||||
class Tax
|
||||
attr_reader :id
|
||||
def save; @id = 1 end
|
||||
def new_record?; @id.nil? end
|
||||
def name
|
||||
model = self.class.name.downcase
|
||||
@id.nil? ? "new #{model}" : "#{model} ##{@id}"
|
||||
end
|
||||
end
|
||||
|
||||
class Fax < Tax
|
||||
def store_id; 1 end
|
||||
end
|
||||
|
||||
# TODO: test nested models
|
||||
class Response::Nested < Response; end
|
||||
|
||||
class PolymorphicRoutesTest < ActiveSupport::TestCase
|
||||
include ActionController::PolymorphicRoutes
|
||||
|
||||
def setup
|
||||
@article = Article.new
|
||||
@response = Response.new
|
||||
@tax = Tax.new
|
||||
@fax = Fax.new
|
||||
end
|
||||
|
||||
def test_with_record
|
||||
@article.save
|
||||
expects(:article_url).with(@article)
|
||||
polymorphic_url(@article)
|
||||
end
|
||||
|
||||
def test_with_new_record
|
||||
expects(:articles_url).with()
|
||||
@article.expects(:new_record?).returns(true)
|
||||
polymorphic_url(@article)
|
||||
end
|
||||
|
||||
def test_with_record_and_action
|
||||
expects(:new_article_url).with()
|
||||
@article.expects(:new_record?).never
|
||||
polymorphic_url(@article, :action => 'new')
|
||||
end
|
||||
|
||||
def test_url_helper_prefixed_with_new
|
||||
expects(:new_article_url).with()
|
||||
new_polymorphic_url(@article)
|
||||
end
|
||||
|
||||
def test_url_helper_prefixed_with_edit
|
||||
@article.save
|
||||
expects(:edit_article_url).with(@article)
|
||||
edit_polymorphic_url(@article)
|
||||
end
|
||||
|
||||
def test_url_helper_prefixed_with_edit_with_url_options
|
||||
@article.save
|
||||
expects(:edit_article_url).with(@article, :param1 => '10')
|
||||
edit_polymorphic_url(@article, :param1 => '10')
|
||||
end
|
||||
|
||||
def test_url_helper_with_url_options
|
||||
@article.save
|
||||
expects(:article_url).with(@article, :param1 => '10')
|
||||
polymorphic_url(@article, :param1 => '10')
|
||||
end
|
||||
|
||||
def test_formatted_url_helper_is_deprecated
|
||||
expects(:articles_url).with(:format => :pdf)
|
||||
assert_deprecated do
|
||||
formatted_polymorphic_url([@article, :pdf])
|
||||
end
|
||||
end
|
||||
|
||||
def test_format_option
|
||||
@article.save
|
||||
expects(:article_url).with(@article, :format => :pdf)
|
||||
polymorphic_url(@article, :format => :pdf)
|
||||
end
|
||||
|
||||
def test_format_option_with_url_options
|
||||
@article.save
|
||||
expects(:article_url).with(@article, :format => :pdf, :param1 => '10')
|
||||
polymorphic_url(@article, :format => :pdf, :param1 => '10')
|
||||
end
|
||||
|
||||
def test_id_and_format_option
|
||||
@article.save
|
||||
expects(:article_url).with(:id => @article, :format => :pdf)
|
||||
polymorphic_url(:id => @article, :format => :pdf)
|
||||
end
|
||||
|
||||
def test_with_nested
|
||||
@response.save
|
||||
expects(:article_response_url).with(@article, @response)
|
||||
polymorphic_url([@article, @response])
|
||||
end
|
||||
|
||||
def test_with_nested_unsaved
|
||||
expects(:article_responses_url).with(@article)
|
||||
polymorphic_url([@article, @response])
|
||||
end
|
||||
|
||||
def test_new_with_array_and_namespace
|
||||
expects(:new_admin_article_url).with()
|
||||
polymorphic_url([:admin, @article], :action => 'new')
|
||||
end
|
||||
|
||||
def test_unsaved_with_array_and_namespace
|
||||
expects(:admin_articles_url).with()
|
||||
polymorphic_url([:admin, @article])
|
||||
end
|
||||
|
||||
def test_nested_unsaved_with_array_and_namespace
|
||||
@article.save
|
||||
expects(:admin_article_url).with(@article)
|
||||
polymorphic_url([:admin, @article])
|
||||
expects(:admin_article_responses_url).with(@article)
|
||||
polymorphic_url([:admin, @article, @response])
|
||||
end
|
||||
|
||||
def test_nested_with_array_and_namespace
|
||||
@response.save
|
||||
expects(:admin_article_response_url).with(@article, @response)
|
||||
polymorphic_url([:admin, @article, @response])
|
||||
|
||||
# a ridiculously long named route tests correct ordering of namespaces and nesting:
|
||||
@tag = Tag.new
|
||||
@tag.save
|
||||
expects(:site_admin_article_response_tag_url).with(@article, @response, @tag)
|
||||
polymorphic_url([:site, :admin, @article, @response, @tag])
|
||||
end
|
||||
|
||||
def test_nesting_with_array_ending_in_singleton_resource
|
||||
expects(:article_response_url).with(@article)
|
||||
polymorphic_url([@article, :response])
|
||||
end
|
||||
|
||||
def test_nesting_with_array_containing_singleton_resource
|
||||
@tag = Tag.new
|
||||
@tag.save
|
||||
expects(:article_response_tag_url).with(@article, @tag)
|
||||
polymorphic_url([@article, :response, @tag])
|
||||
end
|
||||
|
||||
def test_nesting_with_array_containing_namespace_and_singleton_resource
|
||||
@tag = Tag.new
|
||||
@tag.save
|
||||
expects(:admin_article_response_tag_url).with(@article, @tag)
|
||||
polymorphic_url([:admin, @article, :response, @tag])
|
||||
end
|
||||
|
||||
def test_nesting_with_array_containing_singleton_resource_and_format
|
||||
@tag = Tag.new
|
||||
@tag.save
|
||||
expects(:article_response_tag_url).with(@article, @tag, :format => :pdf)
|
||||
polymorphic_url([@article, :response, @tag], :format => :pdf)
|
||||
end
|
||||
|
||||
def test_nesting_with_array_containing_singleton_resource_and_format_option
|
||||
@tag = Tag.new
|
||||
@tag.save
|
||||
expects(:article_response_tag_url).with(@article, @tag, :format => :pdf)
|
||||
polymorphic_url([@article, :response, @tag], :format => :pdf)
|
||||
end
|
||||
|
||||
def test_nesting_with_array_containing_nil
|
||||
expects(:article_response_url).with(@article)
|
||||
polymorphic_url([@article, nil, :response])
|
||||
end
|
||||
|
||||
def test_with_array_containing_single_object
|
||||
@article.save
|
||||
expects(:article_url).with(@article)
|
||||
polymorphic_url([nil, @article])
|
||||
end
|
||||
|
||||
def test_with_array_containing_single_name
|
||||
@article.save
|
||||
expects(:articles_url)
|
||||
polymorphic_url([:articles])
|
||||
end
|
||||
|
||||
# TODO: Needs to be updated to correctly know about whether the object is in a hash or not
|
||||
def xtest_with_hash
|
||||
expects(:article_url).with(@article)
|
||||
@article.save
|
||||
polymorphic_url(:id => @article)
|
||||
end
|
||||
|
||||
def test_polymorphic_path_accepts_options
|
||||
expects(:new_article_path).with()
|
||||
polymorphic_path(@article, :action => :new)
|
||||
end
|
||||
|
||||
def test_polymorphic_path_does_not_modify_arguments
|
||||
expects(:admin_article_responses_url).with(@article)
|
||||
path = [:admin, @article, @response]
|
||||
assert_no_difference 'path.size' do
|
||||
polymorphic_url(path)
|
||||
end
|
||||
end
|
||||
|
||||
# Tests for names where .plural.singular doesn't round-trip
|
||||
def test_with_irregular_plural_record
|
||||
@tax.save
|
||||
expects(:taxis_url).with(@tax)
|
||||
polymorphic_url(@tax)
|
||||
end
|
||||
|
||||
def test_with_irregular_plural_new_record
|
||||
expects(:taxes_url).with()
|
||||
@tax.expects(:new_record?).returns(true)
|
||||
polymorphic_url(@tax)
|
||||
end
|
||||
|
||||
def test_with_irregular_plural_record_and_action
|
||||
expects(:new_taxis_url).with()
|
||||
@tax.expects(:new_record?).never
|
||||
polymorphic_url(@tax, :action => 'new')
|
||||
end
|
||||
|
||||
def test_irregular_plural_url_helper_prefixed_with_new
|
||||
expects(:new_taxis_url).with()
|
||||
new_polymorphic_url(@tax)
|
||||
end
|
||||
|
||||
def test_irregular_plural_url_helper_prefixed_with_edit
|
||||
@tax.save
|
||||
expects(:edit_taxis_url).with(@tax)
|
||||
edit_polymorphic_url(@tax)
|
||||
end
|
||||
|
||||
def test_with_nested_irregular_plurals
|
||||
@fax.save
|
||||
expects(:taxis_faxis_url).with(@tax, @fax)
|
||||
polymorphic_url([@tax, @fax])
|
||||
end
|
||||
|
||||
def test_with_nested_unsaved_irregular_plurals
|
||||
expects(:taxis_faxes_url).with(@tax)
|
||||
polymorphic_url([@tax, @fax])
|
||||
end
|
||||
|
||||
def test_new_with_irregular_plural_array_and_namespace
|
||||
expects(:new_admin_taxis_url).with()
|
||||
polymorphic_url([:admin, @tax], :action => 'new')
|
||||
end
|
||||
|
||||
def test_unsaved_with_irregular_plural_array_and_namespace
|
||||
expects(:admin_taxes_url).with()
|
||||
polymorphic_url([:admin, @tax])
|
||||
end
|
||||
|
||||
def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource
|
||||
expects(:taxis_faxis_url).with(@tax)
|
||||
polymorphic_url([@tax, :faxis])
|
||||
end
|
||||
|
||||
def test_with_array_containing_single_irregular_plural_object
|
||||
@tax.save
|
||||
expects(:taxis_url).with(@tax)
|
||||
polymorphic_url([nil, @tax])
|
||||
end
|
||||
|
||||
def test_with_array_containing_single_name_irregular_plural
|
||||
@tax.save
|
||||
expects(:taxes_url)
|
||||
polymorphic_url([:taxes])
|
||||
end
|
||||
|
||||
def test_with_array_containing_symbols
|
||||
expects(:new_article_url).with()
|
||||
polymorphic_url([:new, :article])
|
||||
end
|
||||
end
|
||||
@@ -5,7 +5,6 @@ class BaseRackTest < ActiveSupport::TestCase
|
||||
@env = {
|
||||
"HTTP_MAX_FORWARDS" => "10",
|
||||
"SERVER_NAME" => "glu.ttono.us",
|
||||
"FCGI_ROLE" => "RESPONDER",
|
||||
"AUTH_TYPE" => "Basic",
|
||||
"HTTP_X_FORWARDED_HOST" => "glu.ttono.us",
|
||||
"HTTP_ACCEPT_CHARSET" => "UTF-8",
|
||||
|
||||
@@ -175,6 +175,10 @@ class FormHelperTest < ActionView::TestCase
|
||||
I18n.locale = old_locale
|
||||
end
|
||||
|
||||
def test_label_with_for_attribute_as_nil
|
||||
assert_dom_equal('<label>Title</label>', label(:post, :title, nil, :for => nil))
|
||||
end
|
||||
|
||||
def test_label_with_for_attribute_as_symbol
|
||||
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for"))
|
||||
end
|
||||
@@ -274,6 +278,11 @@ class FormHelperTest < ActionView::TestCase
|
||||
hidden_field("post", "title", :value => "Something Else")
|
||||
end
|
||||
|
||||
def test_text_field_with_id_as_nil
|
||||
assert_dom_equal '<input name="post[title]" type="hidden" value="Hello World" />',
|
||||
hidden_field("post", "title", :id => nil)
|
||||
end
|
||||
|
||||
def test_check_box
|
||||
assert_dom_equal(
|
||||
'<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
|
||||
|
||||
@@ -24,9 +24,10 @@ class NumberHelperTest < ActionView::TestCase
|
||||
assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506))
|
||||
assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0}))
|
||||
assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1}))
|
||||
assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}))
|
||||
assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => raw("£"), :separator => ",", :delimiter => ""}))
|
||||
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}))
|
||||
assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))
|
||||
assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"}))
|
||||
assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => raw("Kč"), :format => "%n %u"}))
|
||||
#assert_equal("$x.", number_to_currency("x")) # fails due to API consolidation
|
||||
assert_equal("$x", number_to_currency("x"))
|
||||
assert_nil number_to_currency(nil)
|
||||
|
||||
30
activesupport/build_marshalled_tzinfo_data.rb
Normal file
30
activesupport/build_marshalled_tzinfo_data.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env ruby
|
||||
Dir.chdir(File.expand_path("..", __FILE__))
|
||||
$: << File.expand_path("../lib", __FILE__)
|
||||
require "active_support"
|
||||
|
||||
ActiveSupport::TimeZone.all
|
||||
|
||||
def flatten_constants(mod, ary = [])
|
||||
ary << mod
|
||||
mod.constants.each do |const|
|
||||
flatten_constants(mod.const_get(const), ary)
|
||||
end
|
||||
ary
|
||||
end
|
||||
|
||||
defns = flatten_constants(TZInfo::Definitions).select { |mod|
|
||||
defined?(mod.get)
|
||||
}.map { |tz|
|
||||
tz.get
|
||||
}
|
||||
|
||||
file = "lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.dump"
|
||||
data = Marshal.dump(defns)
|
||||
Marshal.load(data)
|
||||
File.open(file, "wb") do |f|
|
||||
require "pry"
|
||||
pry binding
|
||||
f.write(data)
|
||||
end
|
||||
puts "Wrote #{data.size} bytes to #{file}"
|
||||
@@ -131,4 +131,4 @@ module ActiveSupport
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
unless Array.method_defined? :select!
|
||||
class Array
|
||||
def select!
|
||||
return to_enum(:select!) unless block_given?
|
||||
reject!{|elem| ! yield elem}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,9 @@
|
||||
unless Array.method_defined? :sort_by!
|
||||
class Array
|
||||
def sort_by!
|
||||
return to_enum(:sort_by!) unless block_given?
|
||||
raise "can't modify frozen array" if frozen?
|
||||
replace sort_by{|e| yield e}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,3 +3,4 @@ require 'active_support/core_ext/class/inheritable_attributes'
|
||||
require 'active_support/core_ext/class/removal'
|
||||
require 'active_support/core_ext/class/delegating_attributes'
|
||||
require 'active_support/core_ext/class/attribute'
|
||||
require 'active_support/core_ext/class/subclasses'
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
require 'active_support/core_ext/module/anonymous'
|
||||
require 'active_support/core_ext/module/reachable'
|
||||
|
||||
class Class #:nodoc:
|
||||
# Rubinius
|
||||
if defined?(Class.__subclasses__)
|
||||
alias :subclasses :__subclasses__
|
||||
|
||||
def descendants
|
||||
descendants = []
|
||||
__subclasses__.each do |k|
|
||||
descendants << k
|
||||
descendants.concat k.descendants
|
||||
end
|
||||
descendants
|
||||
end
|
||||
else # MRI
|
||||
begin
|
||||
ObjectSpace.each_object(Class.new) {}
|
||||
|
||||
def descendants
|
||||
descendants = []
|
||||
ObjectSpace.each_object(class << self; self; end) do |k|
|
||||
descendants.unshift k unless k == self
|
||||
end
|
||||
descendants
|
||||
end
|
||||
rescue StandardError # JRuby
|
||||
def descendants
|
||||
descendants = []
|
||||
ObjectSpace.each_object(Class) do |k|
|
||||
descendants.unshift k if k < self
|
||||
end
|
||||
descendants.uniq!
|
||||
descendants
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an array with the direct children of +self+.
|
||||
#
|
||||
# Integer.subclasses # => [Bignum, Fixnum]
|
||||
def subclasses
|
||||
subclasses, chain = [], descendants
|
||||
chain.each do |k|
|
||||
subclasses << k unless chain.any? { |c| c > k }
|
||||
end
|
||||
subclasses
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,6 +9,4 @@ class Hash #:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Hash::ReverseMerge
|
||||
include ActiveSupport::CoreExtensions::Hash::Conversions
|
||||
include ActiveSupport::CoreExtensions::Hash::Diff
|
||||
include ActiveSupport::CoreExtensions::Hash::Slice
|
||||
include ActiveSupport::CoreExtensions::Hash::Except
|
||||
end
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
require 'set'
|
||||
class Hash
|
||||
# Return a hash that includes everything but the given keys. This is useful for
|
||||
# limiting a set of parameters to everything but a few known toggles:
|
||||
#
|
||||
# @person.update(params[:person].except(:admin))
|
||||
def except(*keys)
|
||||
dup.except!(*keys)
|
||||
end
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
# Return a hash that includes everything but the given keys. This is useful for
|
||||
# limiting a set of parameters to everything but a few known toggles:
|
||||
#
|
||||
# @person.update_attributes(params[:person].except(:admin))
|
||||
module Except
|
||||
# Returns a new hash without the given keys.
|
||||
def except(*keys)
|
||||
dup.except!(*keys)
|
||||
end
|
||||
|
||||
# Replaces the hash without the given keys.
|
||||
def except!(*keys)
|
||||
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
# Replaces the hash without the given keys.
|
||||
def except!(*keys)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
unless Hash.method_defined? :keep_if
|
||||
class Hash
|
||||
def keep_if
|
||||
return to_enum(:keep_if) unless block_given?
|
||||
delete_if{|key, value| ! yield key, value}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +1,40 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
# Slice a hash to include only the given keys. This is useful for
|
||||
# limiting an options hash to valid keys before passing to a method:
|
||||
#
|
||||
# def search(criteria = {})
|
||||
# assert_valid_keys(:mass, :velocity, :time)
|
||||
# end
|
||||
#
|
||||
# search(options.slice(:mass, :velocity, :time))
|
||||
#
|
||||
# If you have an array of keys you want to limit to, you should splat them:
|
||||
#
|
||||
# valid_keys = [:mass, :velocity, :time]
|
||||
# search(options.slice(*valid_keys))
|
||||
module Slice
|
||||
# Returns a new hash with only the given keys.
|
||||
def slice(*keys)
|
||||
keys = keys.map { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
||||
hash = self.class.new
|
||||
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
||||
hash
|
||||
end
|
||||
class Hash
|
||||
# Slice a hash to include only the given keys. This is useful for
|
||||
# limiting an options hash to valid keys before passing to a method:
|
||||
#
|
||||
# def search(criteria = {})
|
||||
# criteria.assert_valid_keys(:mass, :velocity, :time)
|
||||
# end
|
||||
#
|
||||
# search(options.slice(:mass, :velocity, :time))
|
||||
#
|
||||
# If you have an array of keys you want to limit to, you should splat them:
|
||||
#
|
||||
# valid_keys = [:mass, :velocity, :time]
|
||||
# search(options.slice(*valid_keys))
|
||||
def slice(*keys)
|
||||
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
||||
keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
|
||||
end
|
||||
|
||||
# Replaces the hash with only the given keys.
|
||||
# Returns a hash contained the removed key/value pairs
|
||||
# {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4}
|
||||
def slice!(*keys)
|
||||
keys = keys.map { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
||||
omit = slice(*(self.keys - keys))
|
||||
hash = slice(*keys)
|
||||
replace(hash)
|
||||
omit
|
||||
end
|
||||
end
|
||||
end
|
||||
# Replaces the hash with only the given keys.
|
||||
# Returns a hash containing the removed key/value pairs.
|
||||
#
|
||||
# { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
|
||||
# # => {:c=>3, :d=>4}
|
||||
def slice!(*keys)
|
||||
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
||||
omit = slice(*self.keys - keys)
|
||||
hash = slice(*keys)
|
||||
replace(hash)
|
||||
omit
|
||||
end
|
||||
|
||||
# Removes and returns the key/value pairs matching the given keys.
|
||||
#
|
||||
# { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
|
||||
# { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
|
||||
def extract!(*keys)
|
||||
keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
require 'active_support/core_ext/object/blank'
|
||||
|
||||
class Module
|
||||
# A module may or may not have a name.
|
||||
#
|
||||
# module M; end
|
||||
# M.name # => "M"
|
||||
#
|
||||
# m = Module.new
|
||||
# m.name # => ""
|
||||
#
|
||||
# A module gets a name when it is first assigned to a constant. Either
|
||||
# via the +module+ or +class+ keyword or by an explicit assignment:
|
||||
#
|
||||
# m = Module.new # creates an anonymous module
|
||||
# M = m # => m gets a name here as a side-effect
|
||||
# m.name # => "M"
|
||||
#
|
||||
def anonymous?
|
||||
# Uses blank? because the name of an anonymous class is an empty
|
||||
# string in 1.8, and nil in 1.9.
|
||||
name.blank?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
require 'active_support/core_ext/module/anonymous'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
class Module
|
||||
def reachable? #:nodoc:
|
||||
!anonymous? && name.constantize.equal?(self)
|
||||
rescue NameError
|
||||
false
|
||||
end
|
||||
end
|
||||
@@ -3,4 +3,9 @@ class Module
|
||||
remove_method(method)
|
||||
rescue NameError
|
||||
end
|
||||
|
||||
def redefine_method(method, &block)
|
||||
remove_possible_method(method)
|
||||
define_method(method, &block)
|
||||
end
|
||||
end
|
||||
50
activesupport/lib/active_support/core_ext/object/to_param.rb
Normal file
50
activesupport/lib/active_support/core_ext/object/to_param.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
|
||||
class Object
|
||||
# Alias of <tt>to_s</tt>.
|
||||
def to_param
|
||||
to_s
|
||||
end
|
||||
end
|
||||
|
||||
class NilClass
|
||||
def to_param
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass
|
||||
def to_param
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class FalseClass
|
||||
def to_param
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class Array
|
||||
# Calls <tt>to_param</tt> on all its elements and joins the result with
|
||||
# slashes. This is used by <tt>url_for</tt> in Action Pack.
|
||||
def to_param
|
||||
collect { |e| e.to_param }.join '/'
|
||||
end
|
||||
end
|
||||
|
||||
class Hash
|
||||
# Converts a hash into a string suitable for use as a URL query string. An optional <tt>namespace</tt> can be
|
||||
# passed to enclose the param names (see example below). The string pairs "key=value" that conform the query
|
||||
# string are sorted lexicographically in ascending order.
|
||||
#
|
||||
# ==== Examples
|
||||
# { :name => 'David', :nationality => 'Danish' }.to_param # => "name=David&nationality=Danish"
|
||||
#
|
||||
# { :name => 'David', :nationality => 'Danish' }.to_param('user') # => "user[name]=David&user[nationality]=Danish"
|
||||
def to_param(namespace = nil)
|
||||
collect do |key, value|
|
||||
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
|
||||
end.sort * '&'
|
||||
end
|
||||
end
|
||||
27
activesupport/lib/active_support/core_ext/object/to_query.rb
Normal file
27
activesupport/lib/active_support/core_ext/object/to_query.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
require 'active_support/core_ext/object/to_param'
|
||||
|
||||
class Object
|
||||
# Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the
|
||||
# param name.
|
||||
#
|
||||
# Note: This method is defined as a default implementation for all Objects for Hash#to_query to work.
|
||||
def to_query(key)
|
||||
require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
|
||||
"#{CGI.escape(key.to_s).gsub(/%(5B|5D)/n) { [$1].pack('H*') }}=#{CGI.escape(to_param.to_s)}"
|
||||
end
|
||||
end
|
||||
|
||||
class Array
|
||||
# Converts an array into a string suitable for use as a URL query string,
|
||||
# using the given +key+ as the param name.
|
||||
#
|
||||
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies[]=Rails&hobbies[]=coding"
|
||||
def to_query(key)
|
||||
prefix = "#{key}[]"
|
||||
collect { |value| value.to_query(prefix) }.join '&'
|
||||
end
|
||||
end
|
||||
|
||||
class Hash
|
||||
alias_method :to_query, :to_param
|
||||
end
|
||||
36
activesupport/lib/active_support/core_ext/object/try.rb
Normal file
36
activesupport/lib/active_support/core_ext/object/try.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
class Object
|
||||
# Invokes the method identified by the symbol +method+, passing it any arguments
|
||||
# and/or the block specified, just like the regular Ruby <tt>Object#send</tt> does.
|
||||
#
|
||||
# *Unlike* that method however, a +NoMethodError+ exception will *not* be raised
|
||||
# and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass.
|
||||
#
|
||||
# ==== Examples
|
||||
#
|
||||
# Without try
|
||||
# @person && @person.name
|
||||
# or
|
||||
# @person ? @person.name : nil
|
||||
#
|
||||
# With try
|
||||
# @person.try(:name)
|
||||
#
|
||||
# +try+ also accepts arguments and/or a block, for the method it is trying
|
||||
# Person.try(:find, 1)
|
||||
# @people.try(:collect) {|p| p.name}
|
||||
#--
|
||||
# This method definition below is for rdoc purposes only. The alias_method call
|
||||
# below overrides it as an optimization since +try+ behaves like +Object#send+,
|
||||
# unless called on +NilClass+.
|
||||
def try(method, *args, &block)
|
||||
send(method, *args, &block)
|
||||
end
|
||||
remove_method :try
|
||||
alias_method :try, :__send__
|
||||
end
|
||||
|
||||
class NilClass #:nodoc:
|
||||
def try(*args)
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
require 'active_support/option_merger'
|
||||
|
||||
class Object
|
||||
# An elegant way to factor duplication out of options passed to a series of
|
||||
# method calls. Each method called in the block, with the block variable as
|
||||
# the receiver, will have its options merged with the default +options+ hash
|
||||
# provided. Each method called on the block variable must take an options
|
||||
# hash as its final argument.
|
||||
#
|
||||
# with_options :order => 'created_at', :class_name => 'Comment' do |post|
|
||||
# post.has_many :comments, :conditions => ['approved = ?', true], :dependent => :delete_all
|
||||
# post.has_many :unapproved_comments, :conditions => ['approved = ?', false]
|
||||
# post.has_many :all_comments
|
||||
# end
|
||||
#
|
||||
# Can also be used with an explicit receiver:
|
||||
#
|
||||
# map.with_options :controller => "people" do |people|
|
||||
# people.connect "/people", :action => "index"
|
||||
# people.connect "/people/:id", :action => "show"
|
||||
# end
|
||||
#
|
||||
def with_options(options)
|
||||
yield ActiveSupport::OptionMerger.new(self, options)
|
||||
end
|
||||
end
|
||||
5
activesupport/lib/active_support/core_ext/regexp.rb
Normal file
5
activesupport/lib/active_support/core_ext/regexp.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class Regexp #:nodoc:
|
||||
def multiline?
|
||||
options & MULTILINE == MULTILINE
|
||||
end
|
||||
end
|
||||
26
activesupport/lib/active_support/core_ext/string/strip.rb
Normal file
26
activesupport/lib/active_support/core_ext/string/strip.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
require 'active_support/core_ext/object/try'
|
||||
|
||||
class String
|
||||
# Strips indentation in heredocs.
|
||||
#
|
||||
# For example in
|
||||
#
|
||||
# if options[:usage]
|
||||
# puts <<-USAGE.strip_heredoc
|
||||
# This command does such and such.
|
||||
#
|
||||
# Supported options are:
|
||||
# -h This message
|
||||
# ...
|
||||
# USAGE
|
||||
# end
|
||||
#
|
||||
# the user would see the usage message aligned against the left margin.
|
||||
#
|
||||
# Technically, it looks for the least indented line in the whole string, and removes
|
||||
# that amount of leading whitespace.
|
||||
def strip_heredoc
|
||||
indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0
|
||||
gsub(/^[ \t]{#{indent}}/, '')
|
||||
end
|
||||
end
|
||||
@@ -1,8 +1,9 @@
|
||||
# encoding: utf-8
|
||||
|
||||
if RUBY_VERSION >= '1.9'
|
||||
require 'uri'
|
||||
|
||||
str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
|
||||
str.force_encoding(Encoding::UTF_8) if str.respond_to?(:force_encoding)
|
||||
|
||||
unless str == URI::DEFAULT_PARSER.unescape(URI::DEFAULT_PARSER.escape(str))
|
||||
URI::Parser.class_eval do
|
||||
@@ -14,3 +15,11 @@ if RUBY_VERSION >= '1.9'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module URI
|
||||
class << self
|
||||
def parser
|
||||
@parser ||= URI.const_defined?(:Parser) ? URI::Parser.new : URI
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
require 'thread'
|
||||
require 'set'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module Dependencies #:nodoc:
|
||||
|
||||
36
activesupport/lib/active_support/file_update_checker.rb
Normal file
36
activesupport/lib/active_support/file_update_checker.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
module ActiveSupport
|
||||
# This class is responsible to track files and invoke the given block
|
||||
# whenever one of these files are changed. For example, this class
|
||||
# is used by Rails to reload the I18n framework whenever they are
|
||||
# changed upon a new request.
|
||||
#
|
||||
# i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do
|
||||
# I18n.reload!
|
||||
# end
|
||||
#
|
||||
# ActionDispatch::Callbacks.to_prepare do
|
||||
# i18n_reloader.execute_if_updated
|
||||
# end
|
||||
#
|
||||
class FileUpdateChecker
|
||||
attr_reader :paths, :last_update_at
|
||||
|
||||
def initialize(paths, calculate=false, &block)
|
||||
@paths = paths
|
||||
@block = block
|
||||
@last_update_at = calculate ? updated_at : nil
|
||||
end
|
||||
|
||||
def updated_at
|
||||
paths.map { |path| File.stat(path).mtime }.max
|
||||
end
|
||||
|
||||
def execute_if_updated
|
||||
current_update_at = self.updated_at
|
||||
if @last_update_at != current_update_at
|
||||
@last_update_at = current_update_at
|
||||
@block.call
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,11 +14,7 @@ rescue Gem::LoadError
|
||||
$:.unshift "#{File.dirname(__FILE__)}/vendor/memcache-client-1.7.4"
|
||||
end
|
||||
|
||||
begin
|
||||
gem 'tzinfo', '~> 0.3.12'
|
||||
rescue Gem::LoadError
|
||||
$:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.12"
|
||||
end
|
||||
$:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.12"
|
||||
|
||||
begin
|
||||
gem 'i18n', '>= 0.4.1'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#--
|
||||
# Copyright (c) 2005-2006 Philip Ross
|
||||
#
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
@@ -30,4 +30,5 @@ require 'tzinfo/timezone'
|
||||
# require 'tzinfo/tzdataparser'
|
||||
# require 'tzinfo/timezone_proxy'
|
||||
require 'tzinfo/data_timezone'
|
||||
require 'tzinfo/linked_timezone'
|
||||
require 'tzinfo/linked_timezone'
|
||||
require 'tzinfo/definitions'
|
||||
|
||||
BIN
activesupport/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.dump
vendored
Normal file
BIN
activesupport/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.dump
vendored
Normal file
Binary file not shown.
30
activesupport/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.rb
vendored
Normal file
30
activesupport/lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.rb
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
require "tzinfo/data_timezone_info"
|
||||
require "tzinfo/linked_timezone_info"
|
||||
require "tzinfo/timezone_definition"
|
||||
|
||||
module TZInfo
|
||||
module Definitions
|
||||
def self.load_all!
|
||||
return true if @loaded
|
||||
@loaded = true
|
||||
|
||||
defns = Marshal.load(File.read(File.expand_path("../definitions.dump", __FILE__)))
|
||||
|
||||
defns.each do |defn|
|
||||
tz_mod = defn.instance_variable_get(:@identifier).split("/").reduce(TZInfo::Definitions) { |mod, name|
|
||||
if mod.const_defined?(name)
|
||||
mod.const_get(name)
|
||||
else
|
||||
mod.const_set(name, Module.new)
|
||||
end
|
||||
}
|
||||
|
||||
def tz_mod.get
|
||||
@timezone
|
||||
end
|
||||
|
||||
tz_mod.instance_variable_set(:@timezone, defn)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,13 +1,13 @@
|
||||
#--
|
||||
# Copyright (c) 2005-2006 Philip Ross
|
||||
#
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in all
|
||||
# copies or substantial portions of the Software.
|
||||
#
|
||||
@@ -24,31 +24,32 @@ require 'date'
|
||||
# require 'tzinfo/country'
|
||||
require 'tzinfo/time_or_datetime'
|
||||
require 'tzinfo/timezone_period'
|
||||
require 'tzinfo/definitions'
|
||||
|
||||
module TZInfo
|
||||
# Indicate a specified time in a local timezone has more than one
|
||||
# possible time in UTC. This happens when switching from daylight savings time
|
||||
# possible time in UTC. This happens when switching from daylight savings time
|
||||
# to normal time where the clocks are rolled back. Thrown by period_for_local
|
||||
# and local_to_utc when using an ambiguous time and not specifying any
|
||||
# and local_to_utc when using an ambiguous time and not specifying any
|
||||
# means to resolve the ambiguity.
|
||||
class AmbiguousTime < StandardError
|
||||
end
|
||||
|
||||
|
||||
# Thrown to indicate that no TimezonePeriod matching a given time could be found.
|
||||
class PeriodNotFound < StandardError
|
||||
end
|
||||
|
||||
|
||||
# Thrown by Timezone#get if the identifier given is not valid.
|
||||
class InvalidTimezoneIdentifier < StandardError
|
||||
end
|
||||
|
||||
|
||||
# Thrown if an attempt is made to use a timezone created with Timezone.new(nil).
|
||||
class UnknownTimezone < StandardError
|
||||
end
|
||||
|
||||
|
||||
# Timezone is the base class of all timezones. It provides a factory method
|
||||
# get to access timezones by identifier. Once a specific Timezone has been
|
||||
# retrieved, DateTimes, Times and timestamps can be converted between the UTC
|
||||
# retrieved, DateTimes, Times and timestamps can be converted between the UTC
|
||||
# and the local time for the zone. For example:
|
||||
#
|
||||
# tz = TZInfo::Timezone.get('America/New_York')
|
||||
@@ -56,42 +57,41 @@ module TZInfo
|
||||
# puts tz.local_to_utc(Time.utc(2005,8,29,11,35,0)).to_s
|
||||
# puts tz.utc_to_local(1125315300).to_s
|
||||
#
|
||||
# Each time conversion method returns an object of the same type it was
|
||||
# Each time conversion method returns an object of the same type it was
|
||||
# passed.
|
||||
#
|
||||
# The timezone information all comes from the tz database
|
||||
# (see http://www.twinsun.com/tz/tz-link.htm)
|
||||
class Timezone
|
||||
include Comparable
|
||||
|
||||
|
||||
# Cache of loaded zones by identifier to avoid using require if a zone
|
||||
# has already been loaded.
|
||||
@@loaded_zones = {}
|
||||
|
||||
|
||||
# Whether the timezones index has been loaded yet.
|
||||
@@index_loaded = false
|
||||
|
||||
# Returns a timezone by its identifier (e.g. "Europe/London",
|
||||
|
||||
# Returns a timezone by its identifier (e.g. "Europe/London",
|
||||
# "America/Chicago" or "UTC").
|
||||
#
|
||||
# Raises InvalidTimezoneIdentifier if the timezone couldn't be found.
|
||||
def self.get(identifier)
|
||||
instance = @@loaded_zones[identifier]
|
||||
unless instance
|
||||
|
||||
unless instance
|
||||
raise InvalidTimezoneIdentifier, 'Invalid identifier' if identifier !~ /^[A-z0-9\+\-_]+(\/[A-z0-9\+\-_]+)*$/
|
||||
identifier = identifier.gsub(/-/, '__m__').gsub(/\+/, '__p__')
|
||||
begin
|
||||
# Use a temporary variable to avoid an rdoc warning
|
||||
file = "tzinfo/definitions/#{identifier}"
|
||||
require file
|
||||
|
||||
TZInfo::Definitions.load_all!
|
||||
|
||||
m = Definitions
|
||||
identifier.split(/\//).each {|part|
|
||||
m = m.const_get(part)
|
||||
}
|
||||
|
||||
|
||||
info = m.get
|
||||
|
||||
|
||||
# Could make Timezone subclasses register an interest in an info
|
||||
# type. Since there are currently only two however, there isn't
|
||||
# much point.
|
||||
@@ -102,35 +102,35 @@ module TZInfo
|
||||
else
|
||||
raise InvalidTimezoneIdentifier, "No handler for info type #{info.class}"
|
||||
end
|
||||
|
||||
@@loaded_zones[instance.identifier] = instance
|
||||
|
||||
@@loaded_zones[instance.identifier] = instance
|
||||
rescue LoadError, NameError => e
|
||||
raise InvalidTimezoneIdentifier, e.message
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
instance
|
||||
end
|
||||
|
||||
|
||||
# Returns a proxy for the Timezone with the given identifier. The proxy
|
||||
# will cause the real timezone to be loaded when an attempt is made to
|
||||
# find a period or convert a time. get_proxy will not validate the
|
||||
# identifier. If an invalid identifier is specified, no exception will be
|
||||
# raised until the proxy is used.
|
||||
# will cause the real timezone to be loaded when an attempt is made to
|
||||
# find a period or convert a time. get_proxy will not validate the
|
||||
# identifier. If an invalid identifier is specified, no exception will be
|
||||
# raised until the proxy is used.
|
||||
def self.get_proxy(identifier)
|
||||
TimezoneProxy.new(identifier)
|
||||
end
|
||||
|
||||
# If identifier is nil calls super(), otherwise calls get. An identfier
|
||||
|
||||
# If identifier is nil calls super(), otherwise calls get. An identfier
|
||||
# should always be passed in when called externally.
|
||||
def self.new(identifier = nil)
|
||||
if identifier
|
||||
if identifier
|
||||
get(identifier)
|
||||
else
|
||||
super()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns an array containing all the available Timezones.
|
||||
#
|
||||
# Returns TimezoneProxy objects to avoid the overhead of loading Timezone
|
||||
@@ -138,14 +138,14 @@ module TZInfo
|
||||
def self.all
|
||||
get_proxies(all_identifiers)
|
||||
end
|
||||
|
||||
# Returns an array containing the identifiers of all the available
|
||||
|
||||
# Returns an array containing the identifiers of all the available
|
||||
# Timezones.
|
||||
def self.all_identifiers
|
||||
load_index
|
||||
Indexes::Timezones.timezones
|
||||
end
|
||||
|
||||
|
||||
# Returns an array containing all the available Timezones that are based
|
||||
# on data (are not links to other Timezones).
|
||||
#
|
||||
@@ -154,44 +154,44 @@ module TZInfo
|
||||
def self.all_data_zones
|
||||
get_proxies(all_data_zone_identifiers)
|
||||
end
|
||||
|
||||
# Returns an array containing the identifiers of all the available
|
||||
|
||||
# Returns an array containing the identifiers of all the available
|
||||
# Timezones that are based on data (are not links to other Timezones)..
|
||||
def self.all_data_zone_identifiers
|
||||
load_index
|
||||
Indexes::Timezones.data_timezones
|
||||
end
|
||||
|
||||
|
||||
# Returns an array containing all the available Timezones that are links
|
||||
# to other Timezones.
|
||||
#
|
||||
# Returns TimezoneProxy objects to avoid the overhead of loading Timezone
|
||||
# definitions until a conversion is actually required.
|
||||
def self.all_linked_zones
|
||||
get_proxies(all_linked_zone_identifiers)
|
||||
get_proxies(all_linked_zone_identifiers)
|
||||
end
|
||||
|
||||
# Returns an array containing the identifiers of all the available
|
||||
|
||||
# Returns an array containing the identifiers of all the available
|
||||
# Timezones that are links to other Timezones.
|
||||
def self.all_linked_zone_identifiers
|
||||
load_index
|
||||
Indexes::Timezones.linked_timezones
|
||||
end
|
||||
|
||||
|
||||
# Returns all the Timezones defined for all Countries. This is not the
|
||||
# complete set of Timezones as some are not country specific (e.g.
|
||||
# complete set of Timezones as some are not country specific (e.g.
|
||||
# 'Etc/GMT').
|
||||
#
|
||||
#
|
||||
# Returns TimezoneProxy objects to avoid the overhead of loading Timezone
|
||||
# definitions until a conversion is actually required.
|
||||
# definitions until a conversion is actually required.
|
||||
def self.all_country_zones
|
||||
Country.all_codes.inject([]) {|zones,country|
|
||||
zones += Country.get(country).zones
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
# Returns all the zone identifiers defined for all Countries. This is not the
|
||||
# complete set of zone identifiers as some are not country specific (e.g.
|
||||
# complete set of zone identifiers as some are not country specific (e.g.
|
||||
# 'Etc/GMT'). You can obtain a Timezone instance for a given identifier
|
||||
# with the get method.
|
||||
def self.all_country_zone_identifiers
|
||||
@@ -199,8 +199,8 @@ module TZInfo
|
||||
zones += Country.get(country).zone_identifiers
|
||||
}
|
||||
end
|
||||
|
||||
# Returns all US Timezone instances. A shortcut for
|
||||
|
||||
# Returns all US Timezone instances. A shortcut for
|
||||
# TZInfo::Country.get('US').zones.
|
||||
#
|
||||
# Returns TimezoneProxy objects to avoid the overhead of loading Timezone
|
||||
@@ -208,35 +208,35 @@ module TZInfo
|
||||
def self.us_zones
|
||||
Country.get('US').zones
|
||||
end
|
||||
|
||||
# Returns all US zone identifiers. A shortcut for
|
||||
|
||||
# Returns all US zone identifiers. A shortcut for
|
||||
# TZInfo::Country.get('US').zone_identifiers.
|
||||
def self.us_zone_identifiers
|
||||
Country.get('US').zone_identifiers
|
||||
end
|
||||
|
||||
|
||||
# The identifier of the timezone, e.g. "Europe/Paris".
|
||||
def identifier
|
||||
raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
|
||||
end
|
||||
|
||||
|
||||
# An alias for identifier.
|
||||
def name
|
||||
# Don't use alias, as identifier gets overridden.
|
||||
identifier
|
||||
end
|
||||
|
||||
|
||||
# Returns a friendlier version of the identifier.
|
||||
def to_s
|
||||
friendly_identifier
|
||||
end
|
||||
|
||||
|
||||
# Returns internal object state as a programmer-readable string.
|
||||
def inspect
|
||||
"#<#{self.class}: #{identifier}>"
|
||||
end
|
||||
|
||||
# Returns a friendlier version of the identifier. Set skip_first_part to
|
||||
|
||||
# Returns a friendlier version of the identifier. Set skip_first_part to
|
||||
# omit the first part of the identifier (typically a region name) where
|
||||
# there is more than one part.
|
||||
#
|
||||
@@ -245,13 +245,13 @@ module TZInfo
|
||||
# Timezone.get('Europe/Paris').friendly_identifier(false) #=> "Europe - Paris"
|
||||
# Timezone.get('Europe/Paris').friendly_identifier(true) #=> "Paris"
|
||||
# Timezone.get('America/Indiana/Knox').friendly_identifier(false) #=> "America - Knox, Indiana"
|
||||
# Timezone.get('America/Indiana/Knox').friendly_identifier(true) #=> "Knox, Indiana"
|
||||
# Timezone.get('America/Indiana/Knox').friendly_identifier(true) #=> "Knox, Indiana"
|
||||
def friendly_identifier(skip_first_part = false)
|
||||
parts = identifier.split('/')
|
||||
if parts.empty?
|
||||
# shouldn't happen
|
||||
identifier
|
||||
elsif parts.length == 1
|
||||
elsif parts.length == 1
|
||||
parts[0]
|
||||
else
|
||||
if skip_first_part
|
||||
@@ -259,47 +259,47 @@ module TZInfo
|
||||
else
|
||||
result = parts[0] + ' - '
|
||||
end
|
||||
|
||||
|
||||
parts[1, parts.length - 1].reverse_each {|part|
|
||||
part.gsub!(/_/, ' ')
|
||||
|
||||
|
||||
if part.index(/[a-z]/)
|
||||
# Missing a space if a lower case followed by an upper case and the
|
||||
# name isn't McXxxx.
|
||||
part.gsub!(/([^M][a-z])([A-Z])/, '\1 \2')
|
||||
part.gsub!(/([M][a-bd-z])([A-Z])/, '\1 \2')
|
||||
|
||||
|
||||
# Missing an apostrophe if two consecutive upper case characters.
|
||||
part.gsub!(/([A-Z])([A-Z])/, '\1\'\2')
|
||||
end
|
||||
|
||||
|
||||
result << part
|
||||
result << ', '
|
||||
}
|
||||
|
||||
|
||||
result.slice!(result.length - 2, 2)
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns the TimezonePeriod for the given UTC time. utc can either be
|
||||
# a DateTime, Time or integer timestamp (Time.to_i). Any timezone
|
||||
# information in utc is ignored (it is treated as a UTC time).
|
||||
def period_for_utc(utc)
|
||||
raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
|
||||
# a DateTime, Time or integer timestamp (Time.to_i). Any timezone
|
||||
# information in utc is ignored (it is treated as a UTC time).
|
||||
def period_for_utc(utc)
|
||||
raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
|
||||
end
|
||||
|
||||
|
||||
# Returns the set of TimezonePeriod instances that are valid for the given
|
||||
# local time as an array. If you just want a single period, use
|
||||
# local time as an array. If you just want a single period, use
|
||||
# period_for_local instead and specify how ambiguities should be resolved.
|
||||
# Returns an empty array if no periods are found for the given time.
|
||||
def periods_for_local(local)
|
||||
raise UnknownTimezone, 'TZInfo::Timezone constructed directly'
|
||||
end
|
||||
|
||||
|
||||
# Returns the TimezonePeriod for the given local time. local can either be
|
||||
# a DateTime, Time or integer timestamp (Time.to_i). Any timezone
|
||||
# information in local is ignored (it is treated as a time in the current
|
||||
# a DateTime, Time or integer timestamp (Time.to_i). Any timezone
|
||||
# information in local is ignored (it is treated as a time in the current
|
||||
# timezone).
|
||||
#
|
||||
# Warning: There are local times that have no equivalent UTC times (e.g.
|
||||
@@ -312,70 +312,70 @@ module TZInfo
|
||||
#
|
||||
# In the second case (more than one equivalent UTC time), an AmbiguousTime
|
||||
# exception will be raised unless the optional dst parameter or block
|
||||
# handles the ambiguity.
|
||||
# handles the ambiguity.
|
||||
#
|
||||
# If the ambiguity is due to a transition from daylight savings time to
|
||||
# standard time, the dst parameter can be used to select whether the
|
||||
# standard time, the dst parameter can be used to select whether the
|
||||
# daylight savings time or local time is used. For example,
|
||||
#
|
||||
# Timezone.get('America/New_York').period_for_local(DateTime.new(2004,10,31,1,30,0))
|
||||
#
|
||||
# would raise an AmbiguousTime exception.
|
||||
#
|
||||
# Specifying dst=true would the daylight savings period from April to
|
||||
# Specifying dst=true would the daylight savings period from April to
|
||||
# October 2004. Specifying dst=false would return the standard period
|
||||
# from October 2004 to April 2005.
|
||||
#
|
||||
# If the dst parameter does not resolve the ambiguity, and a block is
|
||||
# If the dst parameter does not resolve the ambiguity, and a block is
|
||||
# specified, it is called. The block must take a single parameter - an
|
||||
# array of the periods that need to be resolved. The block can select and
|
||||
# return a single period or return nil or an empty array
|
||||
# to cause an AmbiguousTime exception to be raised.
|
||||
def period_for_local(local, dst = nil)
|
||||
def period_for_local(local, dst = nil)
|
||||
results = periods_for_local(local)
|
||||
|
||||
|
||||
if results.empty?
|
||||
raise PeriodNotFound
|
||||
elsif results.size < 2
|
||||
results.first
|
||||
else
|
||||
# ambiguous result try to resolve
|
||||
|
||||
|
||||
if !dst.nil?
|
||||
matches = results.find_all {|period| period.dst? == dst}
|
||||
results = matches if !matches.empty?
|
||||
results = matches if !matches.empty?
|
||||
end
|
||||
|
||||
|
||||
if results.size < 2
|
||||
results.first
|
||||
else
|
||||
# still ambiguous, try the block
|
||||
|
||||
|
||||
if block_given?
|
||||
results = yield results
|
||||
end
|
||||
|
||||
|
||||
if results.is_a?(TimezonePeriod)
|
||||
results
|
||||
elsif results && results.size == 1
|
||||
results.first
|
||||
else
|
||||
else
|
||||
raise AmbiguousTime, "#{local} is an ambiguous local time."
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Converts a time in UTC to the local timezone. utc can either be
|
||||
# a DateTime, Time or timestamp (Time.to_i). The returned time has the same
|
||||
# type as utc. Any timezone information in utc is ignored (it is treated as
|
||||
# type as utc. Any timezone information in utc is ignored (it is treated as
|
||||
# a UTC time).
|
||||
def utc_to_local(utc)
|
||||
TimeOrDateTime.wrap(utc) {|wrapped|
|
||||
period_for_utc(wrapped).to_local(wrapped)
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
# Converts a time in the local timezone to UTC. local can either be
|
||||
# a DateTime, Time or timestamp (Time.to_i). The returned time has the same
|
||||
# type as local. Any timezone information in local is ignored (it is treated
|
||||
@@ -391,10 +391,10 @@ module TZInfo
|
||||
#
|
||||
# In the second case (more than one equivalent UTC time), an AmbiguousTime
|
||||
# exception will be raised unless the optional dst parameter or block
|
||||
# handles the ambiguity.
|
||||
# handles the ambiguity.
|
||||
#
|
||||
# If the ambiguity is due to a transition from daylight savings time to
|
||||
# standard time, the dst parameter can be used to select whether the
|
||||
# standard time, the dst parameter can be used to select whether the
|
||||
# daylight savings time or local time is used. For example,
|
||||
#
|
||||
# Timezone.get('America/New_York').local_to_utc(DateTime.new(2004,10,31,1,30,0))
|
||||
@@ -404,7 +404,7 @@ module TZInfo
|
||||
# Specifying dst=true would return 2004-10-31 5:30:00. Specifying dst=false
|
||||
# would return 2004-10-31 6:30:00.
|
||||
#
|
||||
# If the dst parameter does not resolve the ambiguity, and a block is
|
||||
# If the dst parameter does not resolve the ambiguity, and a block is
|
||||
# specified, it is called. The block must take a single parameter - an
|
||||
# array of the periods that need to be resolved. The block can return a
|
||||
# single period to use to convert the time or return nil or an empty array
|
||||
@@ -416,21 +416,21 @@ module TZInfo
|
||||
else
|
||||
period = period_for_local(wrapped, dst)
|
||||
end
|
||||
|
||||
|
||||
period.to_utc(wrapped)
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
# Returns the current time in the timezone as a Time.
|
||||
def now
|
||||
utc_to_local(Time.now.utc)
|
||||
end
|
||||
|
||||
# Returns the TimezonePeriod for the current time.
|
||||
|
||||
# Returns the TimezonePeriod for the current time.
|
||||
def current_period
|
||||
period_for_utc(Time.now.utc)
|
||||
end
|
||||
|
||||
|
||||
# Returns the current Time and TimezonePeriod as an array. The first element
|
||||
# is the time, the second element is the period.
|
||||
def current_period_and_time
|
||||
@@ -438,19 +438,19 @@ module TZInfo
|
||||
period = period_for_utc(utc)
|
||||
[period.to_local(utc), period]
|
||||
end
|
||||
|
||||
|
||||
alias :current_time_and_period :current_period_and_time
|
||||
|
||||
# Converts a time in UTC to local time and returns it as a string
|
||||
# according to the given format. The formatting is identical to
|
||||
# Converts a time in UTC to local time and returns it as a string
|
||||
# according to the given format. The formatting is identical to
|
||||
# Time.strftime and DateTime.strftime, except %Z is replaced with the
|
||||
# timezone abbreviation for the specified time (for example, EST or EDT).
|
||||
def strftime(format, utc = Time.now.utc)
|
||||
# timezone abbreviation for the specified time (for example, EST or EDT).
|
||||
def strftime(format, utc = Time.now.utc)
|
||||
period = period_for_utc(utc)
|
||||
local = period.to_local(utc)
|
||||
local = period.to_local(utc)
|
||||
local = Time.at(local).utc unless local.kind_of?(Time) || local.kind_of?(DateTime)
|
||||
abbreviation = period.abbreviation.to_s.gsub(/%/, '%%')
|
||||
|
||||
|
||||
format = format.gsub(/(.?)%Z/) do
|
||||
if $1 == '%'
|
||||
# return %%Z so the real strftime treats it as a literal %Z too
|
||||
@@ -459,50 +459,50 @@ module TZInfo
|
||||
"#$1#{abbreviation}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local.strftime(format)
|
||||
end
|
||||
|
||||
|
||||
# Compares two Timezones based on their identifier. Returns -1 if tz is less
|
||||
# than self, 0 if tz is equal to self and +1 if tz is greater than self.
|
||||
def <=>(tz)
|
||||
identifier <=> tz.identifier
|
||||
end
|
||||
|
||||
# Returns true if and only if the identifier of tz is equal to the
|
||||
|
||||
# Returns true if and only if the identifier of tz is equal to the
|
||||
# identifier of this Timezone.
|
||||
def eql?(tz)
|
||||
self == tz
|
||||
end
|
||||
|
||||
|
||||
# Returns a hash of this Timezone.
|
||||
def hash
|
||||
identifier.hash
|
||||
end
|
||||
|
||||
|
||||
# Dumps this Timezone for marshalling.
|
||||
def _dump(limit)
|
||||
identifier
|
||||
end
|
||||
|
||||
|
||||
# Loads a marshalled Timezone.
|
||||
def self._load(data)
|
||||
Timezone.get(data)
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
# Loads in the index of timezones if it hasn't already been loaded.
|
||||
def self.load_index
|
||||
unless @@index_loaded
|
||||
require 'tzinfo/indexes/timezones'
|
||||
@@index_loaded = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns an array of proxies corresponding to the given array of
|
||||
|
||||
# Returns an array of proxies corresponding to the given array of
|
||||
# identifiers.
|
||||
def self.get_proxies(identifiers)
|
||||
identifiers.collect {|identifier| get_proxy(identifier)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user