mirror of
https://github.com/github/rails.git
synced 2026-01-12 16:19:01 -05:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d72818356 | ||
|
|
221477dc21 | ||
|
|
975155c110 | ||
|
|
2931987892 | ||
|
|
e3290b98dd | ||
|
|
20088080a5 | ||
|
|
24e348489d | ||
|
|
ba4f4f8a01 |
@@ -5,3 +5,4 @@ gem install sqlite3 -v=1.3.7
|
||||
gem install rack -v=1.4.5
|
||||
gem install erubis -v=2.7.0
|
||||
gem install json -v=1.8.0
|
||||
gem install i18n -v=0.6.9
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.3.14.github35
|
||||
2.3.14.github36
|
||||
|
||||
@@ -280,6 +280,7 @@ 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, Request, Response, Http::Headers, UrlRewriter]
|
||||
[Base, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
|
||||
end
|
||||
|
||||
autoload :Base, 'action_controller/base'
|
||||
@@ -57,6 +57,7 @@ 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'
|
||||
@@ -77,6 +78,7 @@ 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
|
||||
|
||||
@@ -59,9 +59,19 @@ module ActionController
|
||||
def assert_redirected_to(options = {}, message=nil)
|
||||
clean_backtrace do
|
||||
assert_response(:redirect, message)
|
||||
return true if options == @response.location
|
||||
return true if options == @response.redirected_to
|
||||
|
||||
redirected_to_after_normalisation = normalize_argument_to_redirection(@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)
|
||||
options_after_normalisation = normalize_argument_to_redirection(options)
|
||||
|
||||
if redirected_to_after_normalisation != options_after_normalisation
|
||||
|
||||
@@ -1,15 +1,61 @@
|
||||
require 'set'
|
||||
require 'action_controller/metal/url_for'
|
||||
require 'action_controller/metal/exceptions'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class UnknownAction < ActionControllerError #:nodoc:
|
||||
class ActionControllerError < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
class SessionRestoreError < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class RenderError < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
class SessionRestoreError < ActionControllerError #:nodoc:
|
||||
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
|
||||
end
|
||||
|
||||
class DoubleRenderError < ActionControllerError #:nodoc:
|
||||
@@ -28,6 +74,9 @@ 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.
|
||||
@@ -201,7 +250,6 @@ 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.
|
||||
@@ -493,6 +541,93 @@ 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
|
||||
@@ -900,6 +1035,27 @@ 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+.
|
||||
@@ -1206,8 +1362,7 @@ 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 -
|
||||
_routes.named_routes.helper_names
|
||||
hidden_actions
|
||||
end
|
||||
|
||||
def reset_variables_added_to_assigns
|
||||
|
||||
@@ -44,7 +44,7 @@ module ActionController
|
||||
# Run prepare callbacks before every request in development mode
|
||||
run_prepare_callbacks
|
||||
|
||||
ActionController::Routing.routes_reloader.execute_if_updated
|
||||
Routing::Routes.reload
|
||||
end
|
||||
|
||||
def cleanup_application
|
||||
|
||||
@@ -77,7 +77,6 @@ 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,6 +1,7 @@
|
||||
require 'stringio'
|
||||
require 'uri'
|
||||
require 'active_support/test_case'
|
||||
require 'action_controller/rack_lint_patch'
|
||||
|
||||
module ActionController
|
||||
module Integration #:nodoc:
|
||||
@@ -90,9 +91,12 @@ module ActionController
|
||||
|
||||
unless defined? @named_routes_configured
|
||||
# install the named routes in this session instance.
|
||||
class << self
|
||||
include ActionController::Routing::Routes.url_helpers
|
||||
end
|
||||
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 }
|
||||
@named_routes_configured = true
|
||||
end
|
||||
end
|
||||
@@ -126,7 +130,7 @@ module ActionController
|
||||
# performed on the location header.
|
||||
def follow_redirect!
|
||||
raise "not a redirect! #{@status} #{@status_message}" unless redirect?
|
||||
get(headers['location'])
|
||||
get(interpret_uri(headers['location']))
|
||||
status
|
||||
end
|
||||
|
||||
@@ -252,15 +256,14 @@ module ActionController
|
||||
|
||||
# Performs the actual request.
|
||||
def process(method, path, parameters = nil, headers = nil)
|
||||
data = requestify(parameters) if !parameters.blank?
|
||||
data = requestify(parameters)
|
||||
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"] = query || data
|
||||
env["QUERY_STRING"] = data
|
||||
data = nil
|
||||
end
|
||||
|
||||
@@ -340,10 +343,6 @@ 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
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
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
|
||||
@@ -1,22 +0,0 @@
|
||||
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
|
||||
189
actionpack/lib/action_controller/polymorphic_routes.rb
Normal file
189
actionpack/lib/action_controller/polymorphic_routes.rb
Normal file
@@ -0,0 +1,189 @@
|
||||
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
|
||||
36
actionpack/lib/action_controller/rack_lint_patch.rb
Normal file
36
actionpack/lib/action_controller/rack_lint_patch.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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,10 +35,6 @@ 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
|
||||
@@ -312,10 +308,6 @@ 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
|
||||
@@ -340,10 +332,6 @@ 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] || '')
|
||||
|
||||
@@ -1,17 +1,42 @@
|
||||
require 'active_support/core_ext/object/to_param'
|
||||
require 'active_support/core_ext/regexp'
|
||||
require 'active_support/file_update_checker'
|
||||
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'
|
||||
|
||||
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:
|
||||
#
|
||||
# AppName::Application.routes.draw do
|
||||
# ActionController::Routing::Routes.draw do |map|
|
||||
# Pattern 1 tells some request to go to one place
|
||||
# Pattern 2 tell them to go to another
|
||||
# ...
|
||||
@@ -24,49 +49,60 @@ module ActionController
|
||||
#
|
||||
# Other names simply map to a parameter as in the case of <tt>:id</tt>.
|
||||
#
|
||||
# == Resources
|
||||
# == Route priority
|
||||
#
|
||||
# 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:
|
||||
# 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.
|
||||
#
|
||||
# resources :photos
|
||||
# Within blocks, the empty pattern is at the highest priority.
|
||||
# In practice this works out nicely:
|
||||
#
|
||||
# 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
|
||||
# ActionController::Routing::Routes.draw do |map|
|
||||
# map.with_options :controller => 'blog' do |blog|
|
||||
# blog.show '', :action => 'list'
|
||||
# end
|
||||
# map.connect ':controller/:action/:view'
|
||||
# end
|
||||
#
|
||||
# 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:
|
||||
# In this case, invoking blog controller (with an URL like '/blog/')
|
||||
# without parameters will activate the 'list' action by default.
|
||||
#
|
||||
# namespace "admin" do
|
||||
# resources :posts, :comments
|
||||
# == 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'
|
||||
# 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 by passing an <tt>:as</tt> option,
|
||||
# Routes can be named with the syntax <tt>map.name_of_route options</tt>,
|
||||
# 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
|
||||
# match '/login' => 'accounts#login', :as => 'login'
|
||||
# map.login 'login', :controller => 'accounts', :action => 'login'
|
||||
#
|
||||
# # With render, redirect_to, tests, etc.
|
||||
# redirect_to login_url
|
||||
@@ -75,26 +111,32 @@ module ActionController
|
||||
#
|
||||
# redirect_to show_item_path(:id => 25)
|
||||
#
|
||||
# Use <tt>root</tt> as a shorthand to name a route for the root path "/".
|
||||
# Use <tt>map.root</tt> as a shorthand to name a route for the root path "".
|
||||
#
|
||||
# # In routes.rb
|
||||
# root :to => 'blogs#index'
|
||||
# map.root :controller => 'blogs'
|
||||
#
|
||||
# # 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 # => ''
|
||||
#
|
||||
# Note: when using +controller+, the route is simply named after the
|
||||
# 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
|
||||
# method you call on the block parameter rather than map.
|
||||
#
|
||||
# # In routes.rb
|
||||
# controller :blog do
|
||||
# match 'blog/show' => :list
|
||||
# match 'blog/delete' => :delete
|
||||
# match 'blog/edit/:id' => :edit
|
||||
# map.with_options :controller => 'blog' do |blog|
|
||||
# blog.show '', :action => 'list'
|
||||
# blog.delete 'delete/:id', :action => 'delete',
|
||||
# blog.edit 'edit/:id', :action => 'edit'
|
||||
# end
|
||||
#
|
||||
# # provides named routes for show, delete, and edit
|
||||
@@ -104,11 +146,12 @@ module ActionController
|
||||
#
|
||||
# Routes can generate pretty URLs. For example:
|
||||
#
|
||||
# match '/articles/:year/:month/:day' => 'articles#find_by_id', :constraints => {
|
||||
# :year => /\d{4}/,
|
||||
# :month => /\d{1,2}/,
|
||||
# :day => /\d{1,2}/
|
||||
# }
|
||||
# map.connect 'articles/:year/:month/:day',
|
||||
# :controller => 'articles',
|
||||
# :action => 'find_by_date',
|
||||
# :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
|
||||
@@ -118,105 +161,64 @@ module ActionController
|
||||
# == Regular Expressions and parameters
|
||||
# You can specify a regular expression to define a format for a parameter.
|
||||
#
|
||||
# controller 'geocode' do
|
||||
# match 'geocode/:postalcode' => :show, :constraints => {
|
||||
# :postalcode => /\d{5}(-\d{4})?/
|
||||
# }
|
||||
# map.geocode 'geocode/:postalcode', :controller => 'geocode',
|
||||
# :action => 'show', :postalcode => /\d{5}(-\d{4})?/
|
||||
#
|
||||
# Constraints can include the 'ignorecase' and 'extended syntax' regular
|
||||
# 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
|
||||
# expression modifiers:
|
||||
#
|
||||
# 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', :postalcode => /hx\d\d\s\d[a-z]{2}/i
|
||||
#
|
||||
# controller 'geocode' do
|
||||
# match 'geocode/:postalcode' => :show, :constraints => {
|
||||
# :postalcode => /# Postcode format
|
||||
# \d{5} #Prefix
|
||||
# (-\d{4})? #Suffix
|
||||
# /x
|
||||
# }
|
||||
# end
|
||||
# map.geocode 'geocode/:postalcode', :controller => 'geocode',
|
||||
# :action => 'show',:requirements => {
|
||||
# :postalcode => /# Postcode format
|
||||
# \d{5} #Prefix
|
||||
# (-\d{4})? #Suffix
|
||||
# /x
|
||||
# }
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# == Default route
|
||||
# == Route globbing
|
||||
#
|
||||
# Consider the following route, which you will find commented out at the
|
||||
# bottom of your generated <tt>config/routes.rb</tt>:
|
||||
# Specifying <tt>*[string]</tt> as part of a rule like:
|
||||
#
|
||||
# match ':controller(/:action(/:id(.:format)))'
|
||||
# map.connect '*path' , :controller => 'blog' , :action => 'unrecognized?'
|
||||
#
|
||||
# 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>.
|
||||
# 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.
|
||||
#
|
||||
# Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end
|
||||
# up with:
|
||||
# == Route conditions
|
||||
#
|
||||
# params = { :controller => 'blog',
|
||||
# :action => 'edit',
|
||||
# :id => '22'
|
||||
# }
|
||||
# With conditions you can define restrictions on routes. Currently the only valid condition is <tt>:method</tt>.
|
||||
#
|
||||
# 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.
|
||||
# * <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.
|
||||
#
|
||||
# == HTTP Methods
|
||||
# Example:
|
||||
#
|
||||
# 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
|
||||
# map.connect 'post/:id', :controller => 'posts', :action => 'show',
|
||||
# :conditions => { :method => :get }
|
||||
# map.connect 'post/:id', :controller => 'posts', :action => 'create_comment',
|
||||
# :conditions => { :method => :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:
|
||||
#
|
||||
# Rails.application.reload_routes!
|
||||
# ActionController::Routing::Routes.reload
|
||||
#
|
||||
# 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>.
|
||||
@@ -260,34 +262,127 @@ module ActionController
|
||||
#
|
||||
# == View a list of all your routes
|
||||
#
|
||||
# rake routes
|
||||
#
|
||||
# Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>.
|
||||
# Run <tt>rake routes</tt>.
|
||||
#
|
||||
module Routing
|
||||
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'
|
||||
SEPARATORS = %w( / . ? )
|
||||
|
||||
SEPARATORS = %w( / . ? ) #:nodoc:
|
||||
HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
|
||||
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
|
||||
|
||||
Routes = RouteSet.new
|
||||
|
||||
def self.routes_reloader
|
||||
@routes_reloader ||= ActiveSupport::FileUpdateChecker.new([]){ reload_routes! }
|
||||
end
|
||||
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.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
|
||||
alias_method_chain :inflections, :route_reloading
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
197
actionpack/lib/action_controller/routing/builder.rb
Normal file
197
actionpack/lib/action_controller/routing/builder.rb
Normal file
@@ -0,0 +1,197 @@
|
||||
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
|
||||
@@ -1,506 +0,0 @@
|
||||
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
|
||||
@@ -1,240 +0,0 @@
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
130
actionpack/lib/action_controller/routing/optimisations.rb
Normal file
130
actionpack/lib/action_controller/routing/optimisations.rb
Normal file
@@ -0,0 +1,130 @@
|
||||
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
|
||||
@@ -1,183 +0,0 @@
|
||||
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
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
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
|
||||
@@ -1,147 +0,0 @@
|
||||
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
|
||||
269
actionpack/lib/action_controller/routing/route.rb
Normal file
269
actionpack/lib/action_controller/routing/route.rb
Normal file
@@ -0,0 +1,269 @@
|
||||
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
49
actionpack/lib/action_controller/routing/routing_ext.rb
Normal file
49
actionpack/lib/action_controller/routing/routing_ext.rb
Normal file
@@ -0,0 +1,49 @@
|
||||
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
|
||||
343
actionpack/lib/action_controller/routing/segments.rb
Normal file
343
actionpack/lib/action_controller/routing/segments.rb
Normal file
@@ -0,0 +1,343 @@
|
||||
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
|
||||
@@ -1,169 +0,0 @@
|
||||
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,7 +36,6 @@ 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
|
||||
@@ -45,6 +44,11 @@ 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)
|
||||
@@ -87,32 +91,23 @@ module ActionController #:nodoc:
|
||||
@path || super()
|
||||
end
|
||||
|
||||
def assign_parameters(routes, controller_path, action, parameters = {})
|
||||
def assign_parameters(controller_path, action, parameters = {})
|
||||
parameters = parameters.symbolize_keys.merge(:controller => controller_path, :action => action)
|
||||
extra_keys = routes.extra_keys(parameters)
|
||||
extra_keys = ActionController::Routing::Routes.extra_keys(parameters)
|
||||
non_path_parameters = get? ? query_parameters : request_parameters
|
||||
parameters.each do |key, 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
|
||||
if value.is_a? Fixnum
|
||||
value = value.to_s
|
||||
elsif value.is_a? Array
|
||||
value = ActionController::Routing::PathSegment::Result.new(value)
|
||||
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
|
||||
@@ -425,11 +420,9 @@ 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(@routes @controller @request @response).each do |iv_name|
|
||||
%w(@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
|
||||
@@ -441,8 +434,10 @@ module ActionController #:nodoc:
|
||||
@html_document = nil
|
||||
@request.env['REQUEST_METHOD'] = http_method
|
||||
|
||||
@request.action = action.to_s
|
||||
|
||||
parameters ||= {}
|
||||
@request.assign_parameters(@routes, @controller.class.controller_path, action.to_s, parameters)
|
||||
@request.assign_parameters(@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
|
||||
@@ -487,20 +482,13 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def build_request_uri(action, parameters)
|
||||
@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)
|
||||
unless @request.env['REQUEST_URI']
|
||||
options = @controller.__send__(:rewrite_options, parameters)
|
||||
options.update(:only_path => true, :action => action)
|
||||
|
||||
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
|
||||
url = ActionController::UrlRewriter.new(@request, parameters)
|
||||
@request.set_REQUEST_URI(url.rewrite(options))
|
||||
end
|
||||
end
|
||||
|
||||
def html_document
|
||||
|
||||
@@ -1,6 +1,166 @@
|
||||
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]
|
||||
@@ -55,7 +215,7 @@ module ActionController
|
||||
RESERVED_OPTIONS.each { |k| options.delete(k) }
|
||||
|
||||
# Generates the query string, too
|
||||
Routing::Routes.url_for({:host => @request.host_with_port, :protocol => @request.protocol}.merge(options))
|
||||
Routing::Routes.generate(options, @request.symbolized_path_parameters)
|
||||
end
|
||||
|
||||
def rewrite_authentication(options)
|
||||
|
||||
@@ -1,211 +0,0 @@
|
||||
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
|
||||
@@ -1,5 +0,0 @@
|
||||
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'
|
||||
@@ -1,5 +0,0 @@
|
||||
module Rack # :nodoc:
|
||||
Mount = ActionDispatch::Journey::Router
|
||||
Mount::RouteSet = ActionDispatch::Journey::Router
|
||||
Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern
|
||||
end
|
||||
@@ -1,146 +0,0 @@
|
||||
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
|
||||
@@ -1,162 +0,0 @@
|
||||
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
|
||||
@@ -1,44 +0,0 @@
|
||||
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
|
||||
@@ -1,156 +0,0 @@
|
||||
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
|
||||
@@ -1,76 +0,0 @@
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
# 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
|
||||
@@ -1,47 +0,0 @@
|
||||
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
|
||||
@@ -1,163 +0,0 @@
|
||||
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
|
||||
@@ -1,124 +0,0 @@
|
||||
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
|
||||
@@ -1,206 +0,0 @@
|
||||
#
|
||||
# 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
|
||||
@@ -1,47 +0,0 @@
|
||||
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'
|
||||
@@ -1,23 +0,0 @@
|
||||
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
|
||||
@@ -1,196 +0,0 @@
|
||||
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
|
||||
@@ -1,120 +0,0 @@
|
||||
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
|
||||
@@ -1,163 +0,0 @@
|
||||
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
|
||||
@@ -1,24 +0,0 @@
|
||||
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
|
||||
@@ -1,54 +0,0 @@
|
||||
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
|
||||
@@ -1,75 +0,0 @@
|
||||
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
|
||||
@@ -1,61 +0,0 @@
|
||||
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
|
||||
@@ -1,197 +0,0 @@
|
||||
# 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
|
||||
@@ -1,34 +0,0 @@
|
||||
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; }
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<!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>
|
||||
@@ -1,5 +1,4 @@
|
||||
#require 'action_view/helpers/javascript_helper'
|
||||
require 'active_support/concern'
|
||||
|
||||
module ActionView
|
||||
module Helpers #:nodoc:
|
||||
@@ -10,25 +9,6 @@ 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
|
||||
@@ -87,18 +67,20 @@ module ActionView
|
||||
# # if request.env["HTTP_REFERER"] is not set or is blank
|
||||
# # => javascript:history.back()
|
||||
def url_for(options = {})
|
||||
case options
|
||||
options ||= {}
|
||||
url = case options
|
||||
when String
|
||||
options
|
||||
when nil, Hash
|
||||
options ||= {}
|
||||
options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys)
|
||||
super
|
||||
when Hash
|
||||
options = { :only_path => options[:host].nil? }.update(options.symbolize_keys)
|
||||
@controller.send(:url_for, options)
|
||||
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
|
||||
|
||||
@@ -41,6 +41,7 @@ module ActionView
|
||||
include ActionController::TestCase::Assertions
|
||||
include ActionController::TestProcess
|
||||
|
||||
include ActionController::PolymorphicRoutes
|
||||
include ActionController::RecordIdentifier
|
||||
|
||||
include ActionView::Helpers
|
||||
|
||||
297
actionpack/test/controller/polymorphic_routes_test.rb
Normal file
297
actionpack/test/controller/polymorphic_routes_test.rb
Normal file
@@ -0,0 +1,297 @@
|
||||
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
|
||||
@@ -9,8 +9,8 @@ module RenderTestCases
|
||||
|
||||
# Reload and register danish language for testing
|
||||
I18n.reload!
|
||||
I18n.backend.store_translations 'da', {}
|
||||
I18n.backend.store_translations 'pt-BR', {}
|
||||
I18n.backend.store_translations 'da', 'da' => {}
|
||||
I18n.backend.store_translations 'pt-BR', 'pt-BR' => {}
|
||||
|
||||
# Ensure original are still the same since we are reindexing view paths
|
||||
assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort
|
||||
|
||||
@@ -131,4 +131,4 @@ module ActiveSupport
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
unless Array.method_defined? :select!
|
||||
class Array
|
||||
def select!
|
||||
return to_enum(:select!) unless block_given?
|
||||
reject!{|elem| ! yield elem}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
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,4 +3,3 @@ 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'
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
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,4 +9,6 @@ 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,15 +1,25 @@
|
||||
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
|
||||
require 'set'
|
||||
|
||||
# Replaces the hash without the given keys.
|
||||
def except!(*keys)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
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
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
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 @@
|
||||
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
|
||||
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
|
||||
|
||||
# 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) }
|
||||
# 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
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
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
|
||||
@@ -1,10 +0,0 @@
|
||||
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,9 +3,4 @@ class Module
|
||||
remove_method(method)
|
||||
rescue NameError
|
||||
end
|
||||
|
||||
def redefine_method(method, &block)
|
||||
remove_possible_method(method)
|
||||
define_method(method, &block)
|
||||
end
|
||||
end
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
|
||||
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
|
||||
@@ -1,27 +0,0 @@
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
@@ -1,5 +0,0 @@
|
||||
class Regexp #:nodoc:
|
||||
def multiline?
|
||||
options & MULTILINE == MULTILINE
|
||||
end
|
||||
end
|
||||
@@ -1,26 +0,0 @@
|
||||
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,9 +1,8 @@
|
||||
# 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
|
||||
@@ -15,11 +14,3 @@ 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,5 +1,4 @@
|
||||
require 'thread'
|
||||
require 'set'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module Dependencies #:nodoc:
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
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
|
||||
@@ -16,11 +16,6 @@ end
|
||||
|
||||
$:.unshift "#{File.dirname(__FILE__)}/vendor/tzinfo-0.3.12"
|
||||
|
||||
begin
|
||||
gem 'i18n', '>= 0.4.1'
|
||||
rescue Gem::LoadError
|
||||
$:.unshift "#{File.dirname(__FILE__)}/vendor/i18n-0.4.1"
|
||||
end
|
||||
require 'i18n'
|
||||
|
||||
module I18n
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
# Authors:: Sven Fuchs (http://www.artweb-design.de),
|
||||
# Joshua Harvey (http://www.workingwithrails.com/person/759-joshua-harvey),
|
||||
# Stephan Soller (http://www.arkanis-development.de/),
|
||||
# Saimon Moore (http://saimonmoore.net),
|
||||
# Matt Aimonetti (http://railsontherun.com/)
|
||||
# Copyright:: Copyright (c) 2008 The Ruby i18n Team
|
||||
# License:: MIT
|
||||
require 'i18n/exceptions'
|
||||
require 'i18n/core_ext/string/interpolate'
|
||||
|
||||
module I18n
|
||||
autoload :Backend, 'i18n/backend'
|
||||
autoload :Config, 'i18n/config'
|
||||
autoload :Gettext, 'i18n/gettext'
|
||||
autoload :Locale, 'i18n/locale'
|
||||
|
||||
class << self
|
||||
# Gets I18n configuration object.
|
||||
def config
|
||||
Thread.current[:i18n_config] ||= I18n::Config.new
|
||||
end
|
||||
|
||||
# Sets I18n configuration object.
|
||||
def config=(value)
|
||||
Thread.current[:i18n_config] = value
|
||||
end
|
||||
|
||||
# Write methods which delegates to the configuration object
|
||||
%w(locale backend default_locale available_locales default_separator
|
||||
exception_handler load_path).each do |method|
|
||||
module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1
|
||||
def #{method}
|
||||
config.#{method}
|
||||
end
|
||||
|
||||
def #{method}=(value)
|
||||
config.#{method} = (value)
|
||||
end
|
||||
DELEGATORS
|
||||
end
|
||||
|
||||
# Tells the backend to reload translations. Used in situations like the
|
||||
# Rails development environment. Backends can implement whatever strategy
|
||||
# is useful.
|
||||
def reload!
|
||||
config.backend.reload!
|
||||
end
|
||||
|
||||
# Translates, pluralizes and interpolates a given key using a given locale,
|
||||
# scope, and default, as well as interpolation values.
|
||||
#
|
||||
# *LOOKUP*
|
||||
#
|
||||
# Translation data is organized as a nested hash using the upper-level keys
|
||||
# as namespaces. <em>E.g.</em>, ActionView ships with the translation:
|
||||
# <tt>:date => {:formats => {:short => "%b %d"}}</tt>.
|
||||
#
|
||||
# Translations can be looked up at any level of this hash using the key argument
|
||||
# and the scope option. <em>E.g.</em>, in this example <tt>I18n.t :date</tt>
|
||||
# returns the whole translations hash <tt>{:formats => {:short => "%b %d"}}</tt>.
|
||||
#
|
||||
# Key can be either a single key or a dot-separated key (both Strings and Symbols
|
||||
# work). <em>E.g.</em>, the short format can be looked up using both:
|
||||
# I18n.t 'date.formats.short'
|
||||
# I18n.t :'date.formats.short'
|
||||
#
|
||||
# Scope can be either a single key, a dot-separated key or an array of keys
|
||||
# or dot-separated keys. Keys and scopes can be combined freely. So these
|
||||
# examples will all look up the same short date format:
|
||||
# I18n.t 'date.formats.short'
|
||||
# I18n.t 'formats.short', :scope => 'date'
|
||||
# I18n.t 'short', :scope => 'date.formats'
|
||||
# I18n.t 'short', :scope => %w(date formats)
|
||||
#
|
||||
# *INTERPOLATION*
|
||||
#
|
||||
# Translations can contain interpolation variables which will be replaced by
|
||||
# values passed to #translate as part of the options hash, with the keys matching
|
||||
# the interpolation variable names.
|
||||
#
|
||||
# <em>E.g.</em>, with a translation <tt>:foo => "foo %{bar}"</tt> the option
|
||||
# value for the key +bar+ will be interpolated into the translation:
|
||||
# I18n.t :foo, :bar => 'baz' # => 'foo baz'
|
||||
#
|
||||
# *PLURALIZATION*
|
||||
#
|
||||
# Translation data can contain pluralized translations. Pluralized translations
|
||||
# are arrays of singluar/plural versions of translations like <tt>['Foo', 'Foos']</tt>.
|
||||
#
|
||||
# Note that <tt>I18n::Backend::Simple</tt> only supports an algorithm for English
|
||||
# pluralization rules. Other algorithms can be supported by custom backends.
|
||||
#
|
||||
# This returns the singular version of a pluralized translation:
|
||||
# I18n.t :foo, :count => 1 # => 'Foo'
|
||||
#
|
||||
# These both return the plural version of a pluralized translation:
|
||||
# I18n.t :foo, :count => 0 # => 'Foos'
|
||||
# I18n.t :foo, :count => 2 # => 'Foos'
|
||||
#
|
||||
# The <tt>:count</tt> option can be used both for pluralization and interpolation.
|
||||
# <em>E.g.</em>, with the translation
|
||||
# <tt>:foo => ['%{count} foo', '%{count} foos']</tt>, count will
|
||||
# be interpolated to the pluralized translation:
|
||||
# I18n.t :foo, :count => 1 # => '1 foo'
|
||||
#
|
||||
# *DEFAULTS*
|
||||
#
|
||||
# This returns the translation for <tt>:foo</tt> or <tt>default</tt> if no translation was found:
|
||||
# I18n.t :foo, :default => 'default'
|
||||
#
|
||||
# This returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt> if no
|
||||
# translation for <tt>:foo</tt> was found:
|
||||
# I18n.t :foo, :default => :bar
|
||||
#
|
||||
# Returns the translation for <tt>:foo</tt> or the translation for <tt>:bar</tt>
|
||||
# or <tt>default</tt> if no translations for <tt>:foo</tt> and <tt>:bar</tt> were found.
|
||||
# I18n.t :foo, :default => [:bar, 'default']
|
||||
#
|
||||
# *BULK LOOKUP*
|
||||
#
|
||||
# This returns an array with the translations for <tt>:foo</tt> and <tt>:bar</tt>.
|
||||
# I18n.t [:foo, :bar]
|
||||
#
|
||||
# Can be used with dot-separated nested keys:
|
||||
# I18n.t [:'baz.foo', :'baz.bar']
|
||||
#
|
||||
# Which is the same as using a scope option:
|
||||
# I18n.t [:foo, :bar], :scope => :baz
|
||||
#
|
||||
# *LAMBDAS*
|
||||
#
|
||||
# Both translations and defaults can be given as Ruby lambdas. Lambdas will be
|
||||
# called and passed the key and options.
|
||||
#
|
||||
# E.g. assuming the key <tt>:salutation</tt> resolves to:
|
||||
# lambda { |key, options| options[:gender] == 'm' ? "Mr. %{options[:name]}" : "Mrs. %{options[:name]}" }
|
||||
#
|
||||
# Then <tt>I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith".
|
||||
#
|
||||
# It is recommended to use/implement lambdas in an "idempotent" way. E.g. when
|
||||
# a cache layer is put in front of I18n.translate it will generate a cache key
|
||||
# from the argument values passed to #translate. Therefor your lambdas should
|
||||
# always return the same translations/values per unique combination of argument
|
||||
# values.
|
||||
def translate(*args)
|
||||
options = args.pop if args.last.is_a?(Hash)
|
||||
key = args.shift
|
||||
locale = options && options.delete(:locale) || config.locale
|
||||
raises = options && options.delete(:raise)
|
||||
config.backend.translate(locale, key, options || {})
|
||||
rescue I18n::ArgumentError => exception
|
||||
raise exception if raises
|
||||
handle_exception(exception, locale, key, options)
|
||||
end
|
||||
alias :t :translate
|
||||
|
||||
def translate!(key, options = {})
|
||||
translate(key, options.merge( :raise => true ))
|
||||
end
|
||||
alias :t! :translate!
|
||||
|
||||
# Transliterates UTF-8 characters to ASCII. By default this method will
|
||||
# transliterate only Latin strings to an ASCII approximation:
|
||||
#
|
||||
# I18n.transliterate("Ærøskøbing")
|
||||
# # => "AEroskobing"
|
||||
#
|
||||
# I18n.transliterate("日本語")
|
||||
# # => "???"
|
||||
#
|
||||
# It's also possible to add support for per-locale transliterations. I18n
|
||||
# expects transliteration rules to be stored at
|
||||
# <tt>i18n.transliterate.rule</tt>.
|
||||
#
|
||||
# Transliteration rules can either be a Hash or a Proc. Procs must accept a
|
||||
# single string argument. Hash rules inherit the default transliteration
|
||||
# rules, while Procs do not.
|
||||
#
|
||||
# *Examples*
|
||||
#
|
||||
# Setting a Hash in <locale>.yml:
|
||||
#
|
||||
# i18n:
|
||||
# transliterate:
|
||||
# rule:
|
||||
# ü: "ue"
|
||||
# ö: "oe"
|
||||
#
|
||||
# Setting a Hash using Ruby:
|
||||
#
|
||||
# store_translations(:de, :i18n => {
|
||||
# :transliterate => {
|
||||
# :rule => {
|
||||
# "ü" => "ue",
|
||||
# "ö" => "oe"
|
||||
# }
|
||||
# }
|
||||
# )
|
||||
#
|
||||
# Setting a Proc:
|
||||
#
|
||||
# translit = lambda {|string| MyTransliterator.transliterate(string) }
|
||||
# store_translations(:xx, :i18n => {:transliterate => {:rule => translit})
|
||||
#
|
||||
# Transliterating strings:
|
||||
#
|
||||
# I18n.locale = :en
|
||||
# I18n.transliterate("Jürgen") # => "Jurgen"
|
||||
# I18n.locale = :de
|
||||
# I18n.transliterate("Jürgen") # => "Juergen"
|
||||
# I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen"
|
||||
# I18n.transliterate("Jürgen", :locale => :de) # => "Juergen"
|
||||
def transliterate(*args)
|
||||
options = args.pop if args.last.is_a?(Hash)
|
||||
key = args.shift
|
||||
locale = options && options.delete(:locale) || config.locale
|
||||
raises = options && options.delete(:raise)
|
||||
replacement = options && options.delete(:replacement)
|
||||
config.backend.transliterate(locale, key, replacement)
|
||||
rescue I18n::ArgumentError => exception
|
||||
raise exception if raises
|
||||
handle_exception(exception, locale, key, options)
|
||||
end
|
||||
|
||||
# Localizes certain objects, such as dates and numbers to local formatting.
|
||||
def localize(object, options = {})
|
||||
locale = options.delete(:locale) || config.locale
|
||||
format = options.delete(:format) || :default
|
||||
config.backend.localize(locale, object, format, options)
|
||||
end
|
||||
alias :l :localize
|
||||
|
||||
# Executes block with given I18n.locale set.
|
||||
def with_locale(tmp_locale = nil)
|
||||
if tmp_locale
|
||||
current_locale = self.locale
|
||||
self.locale = tmp_locale
|
||||
end
|
||||
yield
|
||||
ensure
|
||||
self.locale = current_locale if tmp_locale
|
||||
end
|
||||
|
||||
|
||||
# Merges the given locale, key and scope into a single array of keys.
|
||||
# Splits keys that contain dots into multiple keys. Makes sure all
|
||||
# keys are Symbols.
|
||||
def normalize_keys(locale, key, scope, separator = nil)
|
||||
separator ||= I18n.default_separator
|
||||
|
||||
keys = []
|
||||
keys.concat normalize_key(locale, separator)
|
||||
keys.concat normalize_key(scope, separator)
|
||||
keys.concat normalize_key(key, separator)
|
||||
keys
|
||||
end
|
||||
|
||||
# making these private until Ruby 1.9.2 can send to protected methods again
|
||||
# see http://redmine.ruby-lang.org/repositories/revision/ruby-19?rev=24280
|
||||
private
|
||||
|
||||
# Handles exceptions raised in the backend. All exceptions except for
|
||||
# MissingTranslationData exceptions are re-raised. When a MissingTranslationData
|
||||
# was caught and the option :raise is not set the handler returns an error
|
||||
# message string containing the key/scope.
|
||||
def default_exception_handler(exception, locale, key, options)
|
||||
return exception.message if MissingTranslationData === exception
|
||||
raise exception
|
||||
end
|
||||
|
||||
# Any exceptions thrown in translate will be sent to the @@exception_handler
|
||||
# which can be a Symbol, a Proc or any other Object.
|
||||
#
|
||||
# If exception_handler is a Symbol then it will simply be sent to I18n as
|
||||
# a method call. A Proc will simply be called. In any other case the
|
||||
# method #call will be called on the exception_handler object.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# I18n.exception_handler = :default_exception_handler # this is the default
|
||||
# I18n.default_exception_handler(exception, locale, key, options) # will be called like this
|
||||
#
|
||||
# I18n.exception_handler = lambda { |*args| ... } # a lambda
|
||||
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
|
||||
#
|
||||
# I18n.exception_handler = I18nExceptionHandler.new # an object
|
||||
# I18n.exception_handler.call(exception, locale, key, options) # will be called like this
|
||||
def handle_exception(exception, locale, key, options)
|
||||
case config.exception_handler
|
||||
when Symbol
|
||||
send(config.exception_handler, exception, locale, key, options)
|
||||
else
|
||||
config.exception_handler.call(exception, locale, key, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Deprecated. Will raise a warning in future versions and then finally be
|
||||
# removed. Use I18n.normalize_keys instead.
|
||||
def normalize_translation_keys(locale, key, scope, separator = nil)
|
||||
normalize_keys(locale, key, scope, separator)
|
||||
end
|
||||
|
||||
def normalize_key(key, separator)
|
||||
normalized_key_cache[separator][key] ||=
|
||||
case key
|
||||
when Array
|
||||
key.map { |k| normalize_key(k, separator) }.flatten
|
||||
else
|
||||
keys = key.to_s.split(separator)
|
||||
keys.delete('')
|
||||
keys.map!{ |k| k.to_sym }
|
||||
keys
|
||||
end
|
||||
end
|
||||
|
||||
def normalized_key_cache
|
||||
@normalized_key_cache ||= Hash.new { |h,k| h[k] = {} }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +0,0 @@
|
||||
module I18n
|
||||
module Backend
|
||||
autoload :ActiveRecord, 'i18n/backend/active_record'
|
||||
autoload :Base, 'i18n/backend/base'
|
||||
autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler'
|
||||
autoload :Cache, 'i18n/backend/cache'
|
||||
autoload :Cascade, 'i18n/backend/cascade'
|
||||
autoload :Chain, 'i18n/backend/chain'
|
||||
autoload :Cldr, 'i18n/backend/cldr'
|
||||
autoload :Fallbacks, 'i18n/backend/fallbacks'
|
||||
autoload :Flatten, 'i18n/backend/flatten'
|
||||
autoload :Gettext, 'i18n/backend/gettext'
|
||||
autoload :KeyValue, 'i18n/backend/key_value'
|
||||
autoload :Memoize, 'i18n/backend/memoize'
|
||||
autoload :Metadata, 'i18n/backend/metadata'
|
||||
autoload :Pluralization, 'i18n/backend/pluralization'
|
||||
autoload :Simple, 'i18n/backend/simple'
|
||||
autoload :Transliterator, 'i18n/backend/transliterator'
|
||||
end
|
||||
end
|
||||
@@ -1,61 +0,0 @@
|
||||
require 'i18n/backend/base'
|
||||
require 'i18n/backend/active_record/translation'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
class ActiveRecord
|
||||
autoload :Missing, 'i18n/backend/active_record/missing'
|
||||
autoload :StoreProcs, 'i18n/backend/active_record/store_procs'
|
||||
autoload :Translation, 'i18n/backend/active_record/translation'
|
||||
|
||||
module Implementation
|
||||
include Base, Flatten
|
||||
|
||||
def available_locales
|
||||
begin
|
||||
Translation.available_locales
|
||||
rescue ::ActiveRecord::StatementInvalid
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = {})
|
||||
escape = options.fetch(:escape, true)
|
||||
flatten_translations(locale, data, escape, false).each do |key, value|
|
||||
Translation.locale(locale).lookup(expand_keys(key)).delete_all
|
||||
Translation.create(:locale => locale.to_s, :key => key.to_s, :value => value)
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def lookup(locale, key, scope = [], options = {})
|
||||
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
||||
result = Translation.locale(locale).lookup(key).all
|
||||
|
||||
if result.empty?
|
||||
nil
|
||||
elsif result.first.key == key
|
||||
result.first.value
|
||||
else
|
||||
chop_range = (key.size + FLATTEN_SEPARATOR.size)..-1
|
||||
result = result.inject({}) do |hash, r|
|
||||
hash[r.key.slice(chop_range)] = r.value
|
||||
hash
|
||||
end
|
||||
result.deep_symbolize_keys
|
||||
end
|
||||
end
|
||||
|
||||
# For a key :'foo.bar.baz' return ['foo', 'foo.bar', 'foo.bar.baz']
|
||||
def expand_keys(key)
|
||||
key.to_s.split(FLATTEN_SEPARATOR).inject([]) do |keys, key|
|
||||
keys << [keys.last, key].compact.join(FLATTEN_SEPARATOR)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,65 +0,0 @@
|
||||
# This extension stores translation stub records for missing translations to
|
||||
# the database.
|
||||
#
|
||||
# This is useful if you have a web based translation tool. It will populate
|
||||
# the database with untranslated keys as the application is being used. A
|
||||
# translator can then go through these and add missing translations.
|
||||
#
|
||||
# Example usage:
|
||||
#
|
||||
# I18n::Backend::Chain.send(:include, I18n::Backend::ActiveRecord::Missing)
|
||||
# I18n.backend = I18nChainBackend.new(I18n::Backend::ActiveRecord.new, I18n::Backend::Simple.new)
|
||||
#
|
||||
# Stub records for pluralizations will also be created for each key defined
|
||||
# in i18n.plural.keys.
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# # en.yml
|
||||
# en:
|
||||
# i18n:
|
||||
# plural:
|
||||
# keys: [:zero, :one, :other]
|
||||
#
|
||||
# # pl.yml
|
||||
# pl:
|
||||
# i18n:
|
||||
# plural:
|
||||
# keys: [:zero, :one, :few, :other]
|
||||
#
|
||||
# It will also persist interpolation keys in Translation#interpolations so
|
||||
# translators will be able to review and use them.
|
||||
module I18n
|
||||
module Backend
|
||||
class ActiveRecord
|
||||
module Missing
|
||||
def store_default_translations(locale, key, options = {})
|
||||
count, scope, default, separator = options.values_at(:count, *Base::RESERVED_KEYS)
|
||||
separator ||= I18n.default_separator
|
||||
|
||||
keys = I18n.normalize_keys(locale, key, scope, separator)[1..-1]
|
||||
key = keys.join(separator || I18n.default_separator)
|
||||
|
||||
unless ActiveRecord::Translation.locale(locale).lookup(key).exists?
|
||||
interpolations = options.reject { |name, value| Base::RESERVED_KEYS.include?(name) }.keys
|
||||
keys = count ? I18n.t('i18n.plural.keys', :locale => locale).map { |k| [key, k].join(separator) } : [key]
|
||||
keys.each { |key| store_default_translation(locale, key, interpolations) }
|
||||
end
|
||||
end
|
||||
|
||||
def store_default_translation(locale, key, interpolations)
|
||||
translation = ActiveRecord::Translation.new :locale => locale.to_s, :key => key
|
||||
translation.interpolations = interpolations
|
||||
translation.save
|
||||
end
|
||||
|
||||
def translate(locale, key, options = {})
|
||||
super
|
||||
rescue I18n::MissingTranslationData => e
|
||||
self.store_default_translations(locale, key, options)
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
# This module is intended to be mixed into the ActiveRecord backend to allow
|
||||
# storing Ruby Procs as translation values in the database.
|
||||
#
|
||||
# I18n.backend = I18n::Backend::ActiveRecord.new
|
||||
# I18n::Backend::ActiveRecord::Translation.send(:include, I18n::Backend::ActiveRecord::StoreProcs)
|
||||
#
|
||||
# The StoreProcs module requires the ParseTree and ruby2ruby gems and therefor
|
||||
# was extracted from the original backend.
|
||||
#
|
||||
# ParseTree is not compatible with Ruby 1.9.
|
||||
|
||||
begin
|
||||
require 'ruby2ruby'
|
||||
require 'parse_tree'
|
||||
require 'parse_tree_extensions'
|
||||
rescue LoadError => e
|
||||
puts "can't use StoreProcs because: #{e.message}"
|
||||
end
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
class ActiveRecord
|
||||
module StoreProcs
|
||||
def value=(v)
|
||||
case v
|
||||
when Proc
|
||||
write_attribute(:value, v.to_ruby)
|
||||
write_attribute(:is_proc, true)
|
||||
else
|
||||
write_attribute(:value, v)
|
||||
end
|
||||
end
|
||||
|
||||
Translation.send(:include, self) if method(:to_s).respond_to?(:to_ruby)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,93 +0,0 @@
|
||||
require 'active_record'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# ActiveRecord model used to store actual translations to the database.
|
||||
#
|
||||
# This model expects a table like the following to be already set up in
|
||||
# your the database:
|
||||
#
|
||||
# create_table :translations do |t|
|
||||
# t.string :locale
|
||||
# t.string :key
|
||||
# t.text :value
|
||||
# t.text :interpolations
|
||||
# t.boolean :is_proc, :default => false
|
||||
# end
|
||||
#
|
||||
# This model supports to named scopes :locale and :lookup. The :locale
|
||||
# scope simply adds a condition for a given locale:
|
||||
#
|
||||
# I18n::Backend::ActiveRecord::Translation.locale(:en).all
|
||||
# # => all translation records that belong to the :en locale
|
||||
#
|
||||
# The :lookup scope adds a condition for looking up all translations
|
||||
# that either start with the given keys (joined by an optionally given
|
||||
# separator or I18n.default_separator) or that exactly have this key.
|
||||
#
|
||||
# # with translations present for :"foo.bar" and :"foo.baz"
|
||||
# I18n::Backend::ActiveRecord::Translation.lookup(:foo)
|
||||
# # => an array with both translation records :"foo.bar" and :"foo.baz"
|
||||
#
|
||||
# I18n::Backend::ActiveRecord::Translation.lookup([:foo, :bar])
|
||||
# I18n::Backend::ActiveRecord::Translation.lookup(:"foo.bar")
|
||||
# # => an array with the translation record :"foo.bar"
|
||||
#
|
||||
# When the StoreProcs module was mixed into this model then Procs will
|
||||
# be stored to the database as Ruby code and evaluated when :value is
|
||||
# called.
|
||||
#
|
||||
# Translation = I18n::Backend::ActiveRecord::Translation
|
||||
# Translation.create \
|
||||
# :locale => 'en'
|
||||
# :key => 'foo'
|
||||
# :value => lambda { |key, options| 'FOO' }
|
||||
# Translation.find_by_locale_and_key('en', 'foo').value
|
||||
# # => 'FOO'
|
||||
class ActiveRecord
|
||||
class Translation < ::ActiveRecord::Base
|
||||
set_table_name 'translations'
|
||||
attr_protected :is_proc, :interpolations
|
||||
|
||||
serialize :value
|
||||
serialize :interpolations, Array
|
||||
|
||||
scope_method = ::ActiveRecord::VERSION::MAJOR == 2 ? :named_scope : :scope
|
||||
|
||||
send scope_method, :locale, lambda { |locale|
|
||||
{ :conditions => { :locale => locale.to_s } }
|
||||
}
|
||||
|
||||
send scope_method, :lookup, lambda { |keys, *separator|
|
||||
column_name = connection.quote_column_name('key')
|
||||
keys = Array(keys).map! { |key| key.to_s }
|
||||
|
||||
unless separator.empty?
|
||||
warn "[DEPRECATION] Giving a separator to Translation.lookup is deprecated. " <<
|
||||
"You can change the internal separator by overwriting FLATTEN_SEPARATOR."
|
||||
end
|
||||
|
||||
namespace = "#{keys.last}#{I18n::Backend::Flatten::FLATTEN_SEPARATOR}%"
|
||||
{ :conditions => ["#{column_name} IN (?) OR #{column_name} LIKE ?", keys, namespace] }
|
||||
}
|
||||
|
||||
def self.available_locales
|
||||
Translation.find(:all, :select => 'DISTINCT locale').map { |t| t.locale.to_sym }
|
||||
end
|
||||
|
||||
def interpolates?(key)
|
||||
self.interpolations.include?(key) if self.interpolations
|
||||
end
|
||||
|
||||
def value
|
||||
if is_proc
|
||||
Kernel.eval(read_attribute(:value))
|
||||
else
|
||||
value = read_attribute(:value)
|
||||
value == 'f' ? false : value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,237 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
require 'yaml'
|
||||
require 'i18n/core_ext/hash'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Base
|
||||
include I18n::Backend::Transliterator
|
||||
|
||||
RESERVED_KEYS = [:scope, :default, :separator, :resolve]
|
||||
RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/
|
||||
DEPRECATED_INTERPOLATION_SYNTAX_PATTERN = /(\\)?\{\{([^\}]+)\}\}/
|
||||
INTERPOLATION_SYNTAX_PATTERN = /%\{([^\}]+)\}/
|
||||
|
||||
# Accepts a list of paths to translation files. Loads translations from
|
||||
# plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml
|
||||
# for details.
|
||||
def load_translations(*filenames)
|
||||
filenames = I18n.load_path.flatten if filenames.empty?
|
||||
filenames.each { |filename| load_file(filename) }
|
||||
end
|
||||
|
||||
# This method receives a locale, a data hash and options for storing translations.
|
||||
# Should be implemented
|
||||
def store_translations(locale, data, options = {})
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def translate(locale, key, options = {})
|
||||
raise InvalidLocale.new(locale) unless locale
|
||||
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
|
||||
|
||||
entry = key && lookup(locale, key, options[:scope], options)
|
||||
|
||||
if options.empty?
|
||||
entry = resolve(locale, key, entry, options)
|
||||
else
|
||||
count, default = options.values_at(:count, :default)
|
||||
values = options.except(*RESERVED_KEYS)
|
||||
entry = entry.nil? && default ?
|
||||
default(locale, key, default, options) : resolve(locale, key, entry, options)
|
||||
end
|
||||
|
||||
raise(I18n::MissingTranslationData.new(locale, key, options)) if entry.nil?
|
||||
entry = entry.dup if entry.is_a?(String)
|
||||
|
||||
entry = pluralize(locale, entry, count) if count
|
||||
entry = interpolate(locale, entry, values) if values
|
||||
entry
|
||||
end
|
||||
|
||||
# Acts the same as +strftime+, but uses a localized version of the
|
||||
# format string. Takes a key from the date/time formats translations as
|
||||
# a format argument (<em>e.g.</em>, <tt>:short</tt> in <tt>:'date.formats'</tt>).
|
||||
def localize(locale, object, format = :default, options = {})
|
||||
raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime)
|
||||
|
||||
if Symbol === format
|
||||
key = format
|
||||
type = object.respond_to?(:sec) ? 'time' : 'date'
|
||||
format = I18n.t(:"#{type}.formats.#{key}", options.merge(:raise => true, :object => object, :locale => locale))
|
||||
end
|
||||
|
||||
# format = resolve(locale, object, format, options)
|
||||
format = format.to_s.gsub(/%[aAbBp]/) do |match|
|
||||
case match
|
||||
when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday]
|
||||
when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday]
|
||||
when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon]
|
||||
when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon]
|
||||
when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour
|
||||
end
|
||||
end
|
||||
|
||||
object.strftime(format)
|
||||
end
|
||||
|
||||
# Returns an array of locales for which translations are available
|
||||
# ignoring the reserved translation meta data key :i18n.
|
||||
def available_locales
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def reload!
|
||||
@skip_syntax_deprecation = false
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# The method which actually looks up for the translation in the store.
|
||||
def lookup(locale, key, scope = [], options = {})
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
# Evaluates defaults.
|
||||
# If given subject is an Array, it walks the array and returns the
|
||||
# first translation that can be resolved. Otherwise it tries to resolve
|
||||
# the translation directly.
|
||||
def default(locale, object, subject, options = {})
|
||||
options = options.dup.reject { |key, value| key == :default }
|
||||
case subject
|
||||
when Array
|
||||
subject.each do |item|
|
||||
result = resolve(locale, object, item, options) and return result
|
||||
end and nil
|
||||
else
|
||||
resolve(locale, object, subject, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Resolves a translation.
|
||||
# If the given subject is a Symbol, it will be translated with the
|
||||
# given options. If it is a Proc then it will be evaluated. All other
|
||||
# subjects will be returned directly.
|
||||
def resolve(locale, object, subject, options = nil)
|
||||
return subject if options[:resolve] == false
|
||||
case subject
|
||||
when Symbol
|
||||
I18n.translate(subject, (options || {}).merge(:locale => locale, :raise => true))
|
||||
when Proc
|
||||
date_or_time = options.delete(:object) || object
|
||||
resolve(locale, object, subject.call(date_or_time, options), options = {})
|
||||
else
|
||||
subject
|
||||
end
|
||||
rescue MissingTranslationData
|
||||
nil
|
||||
end
|
||||
|
||||
# Picks a translation from an array according to English pluralization
|
||||
# rules. It will pick the first translation if count is not equal to 1
|
||||
# and the second translation if it is equal to 1. Other backends can
|
||||
# implement more flexible or complex pluralization rules.
|
||||
def pluralize(locale, entry, count)
|
||||
return entry unless entry.is_a?(Hash) && count
|
||||
|
||||
key = :zero if count == 0 && entry.has_key?(:zero)
|
||||
key ||= count == 1 ? :one : :other
|
||||
raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key)
|
||||
entry[key]
|
||||
end
|
||||
|
||||
# Interpolates values into a given string.
|
||||
#
|
||||
# interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X'
|
||||
# # => "file test.txt opened by %{user}"
|
||||
#
|
||||
# Note that you have to double escape the <tt>\\</tt> when you want to escape
|
||||
# the <tt>{{...}}</tt> key in a string (once for the string and once for the
|
||||
# interpolation).
|
||||
def interpolate(locale, string, values = {})
|
||||
return string unless string.is_a?(::String) && !values.empty?
|
||||
original_values = values.dup
|
||||
|
||||
preserve_encoding(string) do
|
||||
string = string.gsub(DEPRECATED_INTERPOLATION_SYNTAX_PATTERN) do
|
||||
escaped, key = $1, $2.to_sym
|
||||
if escaped
|
||||
"{{#{key}}}"
|
||||
else
|
||||
warn_syntax_deprecation!
|
||||
"%{#{key}}"
|
||||
end
|
||||
end
|
||||
|
||||
keys = string.scan(INTERPOLATION_SYNTAX_PATTERN).flatten
|
||||
return string if keys.empty?
|
||||
|
||||
values.each do |key, value|
|
||||
if keys.include?(key.to_s)
|
||||
value = value.call(values) if interpolate_lambda?(value, string, key)
|
||||
value = value.to_s unless value.is_a?(::String)
|
||||
values[key] = value
|
||||
else
|
||||
values.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
string % values
|
||||
end
|
||||
rescue KeyError => e
|
||||
if string =~ RESERVED_KEYS_PATTERN
|
||||
raise ReservedInterpolationKey.new($1.to_sym, string)
|
||||
else
|
||||
raise MissingInterpolationArgument.new(original_values, string)
|
||||
end
|
||||
end
|
||||
|
||||
def preserve_encoding(string)
|
||||
if string.respond_to?(:encoding)
|
||||
encoding = string.encoding
|
||||
result = yield
|
||||
result.force_encoding(encoding) if result.respond_to?(:force_encoding)
|
||||
result
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# returns true when the given value responds to :call and the key is
|
||||
# an interpolation placeholder in the given string
|
||||
def interpolate_lambda?(object, string, key)
|
||||
object.respond_to?(:call) && string =~ /%\{#{key}\}|%\<#{key}>.*?\d*\.?\d*[bBdiouxXeEfgGcps]\}/
|
||||
end
|
||||
|
||||
# Loads a single translations file by delegating to #load_rb or
|
||||
# #load_yml depending on the file extension and directly merges the
|
||||
# data to the existing translations. Raises I18n::UnknownFileType
|
||||
# for all other file extensions.
|
||||
def load_file(filename)
|
||||
type = File.extname(filename).tr('.', '').downcase
|
||||
raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true)
|
||||
data = send(:"load_#{type}", filename) # TODO raise a meaningful exception if this does not yield a Hash
|
||||
data.each { |locale, d| store_translations(locale, d) }
|
||||
end
|
||||
|
||||
# Loads a plain Ruby translations file. eval'ing the file must yield
|
||||
# a Hash containing translation data with locales as toplevel keys.
|
||||
def load_rb(filename)
|
||||
eval(IO.read(filename), binding, filename)
|
||||
end
|
||||
|
||||
# Loads a YAML translations file. The data must have locales as
|
||||
# toplevel keys.
|
||||
def load_yml(filename)
|
||||
YAML::load(IO.read(filename))
|
||||
end
|
||||
|
||||
def warn_syntax_deprecation! #:nodoc:
|
||||
return if @skip_syntax_deprecation
|
||||
warn "The {{key}} interpolation syntax in I18n messages is deprecated. Please use %{key} instead.\n#{caller.join("\n")}"
|
||||
@skip_syntax_deprecation = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
# This module allows you to easily cache all responses from the backend - thus
|
||||
# speeding up the I18n aspects of your application quite a bit.
|
||||
#
|
||||
# To enable caching you can simply include the Cache module to the Simple
|
||||
# backend - or whatever other backend you are using:
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Cache)
|
||||
#
|
||||
# You will also need to set a cache store implementation that you want to use:
|
||||
#
|
||||
# I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
#
|
||||
# You can use any cache implementation you want that provides the same API as
|
||||
# ActiveSupport::Cache (only the methods #fetch and #write are being used).
|
||||
#
|
||||
# The cache_key implementation assumes that you only pass values to
|
||||
# I18n.translate that return a valid key from #hash (see
|
||||
# http://www.ruby-doc.org/core/classes/Object.html#M000337).
|
||||
module I18n
|
||||
class << self
|
||||
@@cache_store = nil
|
||||
@@cache_namespace = nil
|
||||
|
||||
def cache_store
|
||||
@@cache_store
|
||||
end
|
||||
|
||||
def cache_store=(store)
|
||||
@@cache_store = store
|
||||
end
|
||||
|
||||
def cache_namespace
|
||||
@@cache_namespace
|
||||
end
|
||||
|
||||
def cache_namespace=(namespace)
|
||||
@@cache_namespace = namespace
|
||||
end
|
||||
|
||||
def perform_caching?
|
||||
!cache_store.nil?
|
||||
end
|
||||
end
|
||||
|
||||
module Backend
|
||||
# TODO Should the cache be cleared if new translations are stored?
|
||||
module Cache
|
||||
def translate(*args)
|
||||
I18n.perform_caching? ? fetch(*args) { super } : super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def fetch(*args, &block)
|
||||
result = I18n.cache_store.fetch(cache_key(*args), &block)
|
||||
raise result if result.is_a?(Exception)
|
||||
result = result.dup if result.frozen? rescue result
|
||||
result
|
||||
rescue MissingTranslationData => exception
|
||||
I18n.cache_store.write(cache_key(*args), exception)
|
||||
raise exception
|
||||
end
|
||||
|
||||
def cache_key(*args)
|
||||
# This assumes that only simple, native Ruby values are passed to I18n.translate.
|
||||
# Also, in Ruby < 1.8.7 {}.hash != {}.hash
|
||||
# (see http://paulbarry.com/articles/2009/09/14/why-rails-3-will-require-ruby-1-8-7)
|
||||
# If args.inspect does not work for you for some reason, patches are very welcome :)
|
||||
hash = RUBY_VERSION >= "1.8.7" ? args.hash : args.inspect
|
||||
keys = ['i18n', I18n.cache_namespace, hash]
|
||||
keys.compact.join('-')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
# EXPERIMENTAL
|
||||
#
|
||||
# The Cascade module adds the ability to do cascading lookups to backends that
|
||||
# are compatible to the Simple backend.
|
||||
#
|
||||
# By cascading lookups we mean that for any key that can not be found the
|
||||
# Cascade module strips one segment off the scope part of the key and then
|
||||
# tries to look up the key in that scope.
|
||||
#
|
||||
# E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then
|
||||
# the segment :bar will be stripped off the scope part :"foo.bar" and the new
|
||||
# scope :foo will be used to look up the key :baz. If that does not succeed
|
||||
# then the remaining scope segment :foo will be omitted, too, and again the
|
||||
# key :baz will be looked up (now with no scope).
|
||||
#
|
||||
# To enable a cascading lookup one passes the :cascade option:
|
||||
#
|
||||
# I18n.t(:'foo.bar.baz', :cascade => true)
|
||||
#
|
||||
# This will return the first translation found for :"foo.bar.baz", :"foo.baz"
|
||||
# or :baz in this order.
|
||||
#
|
||||
# The cascading lookup takes precedence over resolving any given defaults.
|
||||
# I.e. defaults will kick in after the cascading lookups haven't succeeded.
|
||||
#
|
||||
# This behavior is useful for libraries like ActiveRecord validations where
|
||||
# the library wants to give users a bunch of more or less fine-grained options
|
||||
# of scopes for a particular key.
|
||||
#
|
||||
# Thanks to Clemens Kofler for the initial idea and implementation! See
|
||||
# http://github.com/clemens/i18n-cascading-backend
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Cascade
|
||||
def lookup(locale, key, scope = [], options = {})
|
||||
return super unless cascade = options[:cascade]
|
||||
|
||||
separator = options[:separator] || I18n.default_separator
|
||||
skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true
|
||||
step = cascade[:step]
|
||||
|
||||
keys = I18n.normalize_keys(nil, key, nil, separator)
|
||||
offset = options[:cascade][:offset] || keys.length
|
||||
scope = I18n.normalize_keys(nil, nil, scope, separator) + keys
|
||||
key = scope.slice!(-offset, offset).join(separator)
|
||||
|
||||
begin
|
||||
result = super
|
||||
return result unless result.nil?
|
||||
end while !scope.empty? && scope.slice!(-step, step) && (!scope.empty? || !skip_root)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# Backend that chains multiple other backends and checks each of them when
|
||||
# a translation needs to be looked up. This is useful when you want to use
|
||||
# standard translations with a Simple backend but store custom application
|
||||
# translations in a database or other backends.
|
||||
#
|
||||
# To use the Chain backend instantiate it and set it to the I18n module.
|
||||
# You can add chained backends through the initializer or backends
|
||||
# accessor:
|
||||
#
|
||||
# # preserves the existing Simple backend set to I18n.backend
|
||||
# I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend)
|
||||
#
|
||||
# The implementation assumes that all backends added to the Chain implement
|
||||
# a lookup method with the same API as Simple backend does.
|
||||
class Chain
|
||||
include Base
|
||||
|
||||
attr_accessor :backends
|
||||
|
||||
def initialize(*backends)
|
||||
self.backends = backends
|
||||
end
|
||||
|
||||
def reload!
|
||||
backends.each { |backend| backend.reload! }
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = {})
|
||||
backends.first.store_translations(locale, data, options = {})
|
||||
end
|
||||
|
||||
def available_locales
|
||||
backends.map { |backend| backend.available_locales }.flatten.uniq
|
||||
end
|
||||
|
||||
def translate(locale, key, options = {})
|
||||
return key.map { |k| translate(locale, k, options) } if key.is_a?(Array)
|
||||
|
||||
default = options.delete(:default)
|
||||
namespace = {}
|
||||
backends.each do |backend|
|
||||
begin
|
||||
options.update(:default => default) if default and backend == backends.last
|
||||
translation = backend.translate(locale, key, options)
|
||||
if namespace_lookup?(translation, options)
|
||||
namespace.update(translation)
|
||||
elsif !translation.nil?
|
||||
return translation
|
||||
end
|
||||
rescue MissingTranslationData
|
||||
end
|
||||
end
|
||||
return namespace unless namespace.empty?
|
||||
raise(I18n::MissingTranslationData.new(locale, key, options))
|
||||
end
|
||||
|
||||
def localize(locale, object, format = :default, options = {})
|
||||
backends.each do |backend|
|
||||
begin
|
||||
result = backend.localize(locale, object, format, options) and return result
|
||||
rescue MissingTranslationData
|
||||
end
|
||||
end
|
||||
raise(I18n::MissingTranslationData.new(locale, format, options))
|
||||
end
|
||||
|
||||
protected
|
||||
def namespace_lookup?(result, options)
|
||||
result.is_a?(Hash) and not options.has_key?(:count)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,100 +0,0 @@
|
||||
# encoding: utf-8
|
||||
require 'cldr'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
module Cldr
|
||||
include ::Cldr::Format
|
||||
|
||||
def localize(locale, object, format = :default, options = {})
|
||||
options[:as] ||= detect_type(object, options)
|
||||
send(:"format_#{options[:as]}", locale, object, format, options)
|
||||
end
|
||||
|
||||
def format_decimal(locale, object, format = :default, options = {})
|
||||
formatter(locale, :decimal, format).apply(object, options)
|
||||
end
|
||||
|
||||
def format_integer(locale, object, format = :default, options = {})
|
||||
format_object(number, options.merge(:precision => 0))
|
||||
end
|
||||
|
||||
def format_currency(locale, object, format = :default, options = {})
|
||||
options.merge!(:currency => lookup_currency(locale, options[:currency], object)) if options[:currency].is_a?(Symbol)
|
||||
formatter(locale, :currency, format).apply(object, options)
|
||||
end
|
||||
|
||||
def format_percent(locale, object, format = :default, options = {})
|
||||
formatter(locale, :percent, format).apply(object, options)
|
||||
end
|
||||
|
||||
def format_date(locale, object, format = :default, options = {})
|
||||
formatter(locale, :date, format).apply(object, options)
|
||||
end
|
||||
|
||||
def format_time(locale, object, format = :default, options = {})
|
||||
formatter(locale, :time, format).apply(object, options)
|
||||
end
|
||||
|
||||
def format_datetime(locale, object, format = :default, options = {})
|
||||
key = :"calendars.gregorian.formats.datetime.#{format}.pattern"
|
||||
date = I18n.l(object, :format => options[:date_format] || format, :locale => locale, :as => :date)
|
||||
time = I18n.l(object, :format => options[:time_format] || format, :locale => locale, :as => :time)
|
||||
I18n.t(key, :date => date, :time => time, :locale => locale, :raise => true)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def detect_type(object, options)
|
||||
options.has_key?(:currency) ? :currency : case object
|
||||
when ::Numeric
|
||||
:decimal
|
||||
when ::Date, ::DateTime, ::Time
|
||||
object.class.name.downcase.to_sym
|
||||
else
|
||||
raise_unspecified_format_type!
|
||||
end
|
||||
end
|
||||
|
||||
def formatter(locale, type, format)
|
||||
(@formatters ||= {})[:"#{locale}.#{type}.#{format}"] ||= begin
|
||||
format = lookup_format(locale, type, format)
|
||||
data = lookup_format_data(locale, type)
|
||||
::Cldr::Format.const_get(type.to_s.camelize).new(format, data)
|
||||
end
|
||||
end
|
||||
|
||||
def lookup_format(locale, type, format)
|
||||
key = case type
|
||||
when :date, :time, :datetime
|
||||
:"calendars.gregorian.formats.#{type}.#{format}.pattern"
|
||||
else
|
||||
:"numbers.formats.#{type}.patterns.#{format || :default}"
|
||||
end
|
||||
I18n.t(key, :locale => locale, :raise => true)
|
||||
end
|
||||
|
||||
def lookup_format_data(locale, type)
|
||||
key = case type
|
||||
when :date, :time, :datetime
|
||||
:'calendars.gregorian'
|
||||
else
|
||||
:'numbers.symbols'
|
||||
end
|
||||
I18n.t(key, :locale => locale, :raise => true)
|
||||
end
|
||||
|
||||
def lookup_currency(locale, currency, count)
|
||||
I18n.t(:"currencies.#{currency}", :locale => locale, :count => count)
|
||||
end
|
||||
|
||||
def raise_unspecified_format_type!
|
||||
raise ArgumentError.new("You have to specify a format type, e.g. :as => :number.")
|
||||
end
|
||||
|
||||
def raise_unspecified_currency!
|
||||
raise ArgumentError.new("You have to specify a currency, e.g. :currency => 'EUR'.")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
# I18n locale fallbacks are useful when you want your application to use
|
||||
# translations from other locales when translations for the current locale are
|
||||
# missing. E.g. you might want to use :en translations when translations in
|
||||
# your applications main locale :de are missing.
|
||||
#
|
||||
# To enable locale fallbacks you can simply include the Fallbacks module to
|
||||
# the Simple backend - or whatever other backend you are using:
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
|
||||
module I18n
|
||||
@@fallbacks = nil
|
||||
|
||||
class << self
|
||||
# Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+.
|
||||
def fallbacks
|
||||
@@fallbacks ||= I18n::Locale::Fallbacks.new
|
||||
end
|
||||
|
||||
# Sets the current fallbacks implementation. Use this to set a different fallbacks implementation.
|
||||
def fallbacks=(fallbacks)
|
||||
@@fallbacks = fallbacks
|
||||
end
|
||||
end
|
||||
|
||||
module Backend
|
||||
module Fallbacks
|
||||
# Overwrites the Base backend translate method so that it will try each
|
||||
# locale given by I18n.fallbacks for the given locale. E.g. for the
|
||||
# locale :"de-DE" it might try the locales :"de-DE", :de and :en
|
||||
# (depends on the fallbacks implementation) until it finds a result with
|
||||
# the given options. If it does not find any result for any of the
|
||||
# locales it will then raise a MissingTranslationData exception as
|
||||
# usual.
|
||||
#
|
||||
# The default option takes precedence over fallback locales
|
||||
# only when it's not a String. When default contains String it
|
||||
# is evaluated after fallback locales.
|
||||
def translate(locale, key, options = {})
|
||||
default = extract_string_default!(options) if options[:default]
|
||||
|
||||
I18n.fallbacks[locale].each do |fallback|
|
||||
begin
|
||||
result = super(fallback, key, options)
|
||||
return result unless result.nil?
|
||||
rescue I18n::MissingTranslationData
|
||||
end
|
||||
end
|
||||
|
||||
return super(locale, nil, options.merge(:default => default)) if default
|
||||
raise(I18n::MissingTranslationData.new(locale, key, options))
|
||||
end
|
||||
|
||||
def extract_string_default!(options)
|
||||
defaults = Array(options[:default])
|
||||
if index = find_first_string_default(defaults)
|
||||
options[:default] = defaults[0, index]
|
||||
defaults[index]
|
||||
end
|
||||
end
|
||||
|
||||
def find_first_string_default(defaults)
|
||||
defaults.each_index { |ix| return ix if String === defaults[ix] }
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,113 +0,0 @@
|
||||
module I18n
|
||||
module Backend
|
||||
# This module contains several helpers to assist flattening translations.
|
||||
# You may want to flatten translations for:
|
||||
#
|
||||
# 1) speed up lookups, as in the Memoize backend;
|
||||
# 2) In case you want to store translations in a data store, as in ActiveRecord backend;
|
||||
#
|
||||
# You can check both backends above for some examples.
|
||||
# This module also keeps all links in a hash so they can be properly resolved when flattened.
|
||||
module Flatten
|
||||
SEPARATOR_ESCAPE_CHAR = "\001"
|
||||
FLATTEN_SEPARATOR = "."
|
||||
|
||||
# normalize_keys the flatten way. This method is significantly faster
|
||||
# and creates way less objects than the one at I18n.normalize_keys.
|
||||
# It also handles escaping the translation keys.
|
||||
def self.normalize_flat_keys(locale, key, scope, separator)
|
||||
keys = [scope, key].flatten.compact
|
||||
separator ||= I18n.default_separator
|
||||
|
||||
if separator != FLATTEN_SEPARATOR
|
||||
keys.map! do |k|
|
||||
k.to_s.tr("#{FLATTEN_SEPARATOR}#{separator}",
|
||||
"#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}")
|
||||
end
|
||||
end
|
||||
|
||||
keys.join(".")
|
||||
end
|
||||
|
||||
# Receives a string and escape the default separator.
|
||||
def self.escape_default_separator(key) #:nodoc:
|
||||
key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR)
|
||||
end
|
||||
|
||||
# Shortcut to I18n::Backend::Flatten.normalize_flat_keys
|
||||
# and then resolve_links.
|
||||
def normalize_flat_keys(locale, key, scope, separator)
|
||||
key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator)
|
||||
resolve_link(locale, key)
|
||||
end
|
||||
|
||||
# Store flattened links.
|
||||
def links
|
||||
@links ||= Hash.new { |h,k| h[k] = {} }
|
||||
end
|
||||
|
||||
# Flatten keys for nested Hashes by chaining up keys:
|
||||
#
|
||||
# >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind
|
||||
# => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" }
|
||||
#
|
||||
def flatten_keys(hash, escape, prev_key=nil, &block)
|
||||
hash.each_pair do |key, value|
|
||||
key = escape_default_separator(key) if escape
|
||||
curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym
|
||||
yield curr_key, value
|
||||
flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
# Receives a hash of translations (where the key is a locale and
|
||||
# the value is another hash) and return a hash with all
|
||||
# translations flattened.
|
||||
#
|
||||
# Nested hashes are included in the flattened hash just if subtree
|
||||
# is true and Symbols are automatically stored as links.
|
||||
def flatten_translations(locale, data, escape, subtree)
|
||||
hash = {}
|
||||
flatten_keys(data, escape) do |key, value|
|
||||
if value.is_a?(Hash)
|
||||
hash[key] = value if subtree
|
||||
else
|
||||
store_link(locale, key, value) if value.is_a?(Symbol)
|
||||
hash[key] = value
|
||||
end
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def store_link(locale, key, link)
|
||||
links[locale.to_sym][key.to_s] = link.to_s
|
||||
end
|
||||
|
||||
def resolve_link(locale, key)
|
||||
key, locale = key.to_s, locale.to_sym
|
||||
links = self.links[locale]
|
||||
|
||||
if links.key?(key)
|
||||
links[key]
|
||||
elsif link = find_link(locale, key)
|
||||
store_link(locale, key, key.gsub(*link))
|
||||
else
|
||||
key
|
||||
end
|
||||
end
|
||||
|
||||
def find_link(locale, key) #:nodoc:
|
||||
links[locale].each do |from, to|
|
||||
return [from, to] if key[0, from.length] == from
|
||||
end && nil
|
||||
end
|
||||
|
||||
def escape_default_separator(key) #:nodoc:
|
||||
I18n::Backend::Flatten.escape_default_separator(key)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,75 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
require 'i18n/gettext'
|
||||
require 'i18n/gettext/po_parser'
|
||||
|
||||
# Experimental support for using Gettext po files to store translations.
|
||||
#
|
||||
# To use this you can simply include the module to the Simple backend - or
|
||||
# whatever other backend you are using.
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Gettext)
|
||||
#
|
||||
# Now you should be able to include your Gettext translation (*.po) files to
|
||||
# the I18n.load_path so they're loaded to the backend and you can use them as
|
||||
# usual:
|
||||
#
|
||||
# I18n.load_path += Dir["path/to/locales/*.po"]
|
||||
#
|
||||
# Following the Gettext convention this implementation expects that your
|
||||
# translation files are named by their locales. E.g. the file en.po would
|
||||
# contain the translations for the English locale.
|
||||
module I18n
|
||||
module Backend
|
||||
module Gettext
|
||||
class PoData < Hash
|
||||
def set_comment(msgid_or_sym, comment)
|
||||
# ignore
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def load_po(filename)
|
||||
locale = ::File.basename(filename, '.po').to_sym
|
||||
data = normalize(locale, parse(filename))
|
||||
{ locale => data }
|
||||
end
|
||||
|
||||
def parse(filename)
|
||||
GetText::PoParser.new.parse(::File.read(filename), PoData.new)
|
||||
end
|
||||
|
||||
def normalize(locale, data)
|
||||
data.inject({}) do |result, (key, value)|
|
||||
unless key.nil? || key.empty?
|
||||
key, value = normalize_pluralization(locale, key, value) if key.index("\000")
|
||||
|
||||
parts = key.split('|').reverse
|
||||
normalized = parts.inject({}) do |normalized, part|
|
||||
normalized = { part => normalized.empty? ? value : normalized }
|
||||
end
|
||||
|
||||
# deep_merge by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809
|
||||
merger = proc { |key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2, &merger) : v2 }
|
||||
result.merge!(normalized, &merger)
|
||||
end
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_pluralization(locale, key, value)
|
||||
# FIXME po_parser includes \000 chars that can not be turned into Symbols
|
||||
key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first
|
||||
|
||||
keys = I18n::Gettext.plural_keys(locale)
|
||||
values = value.split("\000")
|
||||
raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect}" if values.size != keys.size
|
||||
|
||||
result = {}
|
||||
values.each_with_index { |value, ix| result[keys[ix]] = value }
|
||||
[key, result]
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,123 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
# The InterpolationCompiler module contains optimizations that can tremendously
|
||||
# speed up the interpolation process on the Simple backend.
|
||||
#
|
||||
# It works by defining a pre-compiled method on stored translation Strings that
|
||||
# already bring all the knowledge about contained interpolation variables etc.
|
||||
# so that the actual recurring interpolation will be very fast.
|
||||
#
|
||||
# To enable pre-compiled interpolations you can simply include the
|
||||
# InterpolationCompiler module to the Simple backend:
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::InterpolationCompiler)
|
||||
#
|
||||
# Note that InterpolationCompiler does not yield meaningful results and consequently
|
||||
# should not be used with Ruby 1.9 (YARV) but improves performance everywhere else
|
||||
# (jRuby, Rubinius and 1.8.7).
|
||||
module I18n
|
||||
module Backend
|
||||
module InterpolationCompiler
|
||||
module Compiler
|
||||
extend self
|
||||
|
||||
TOKENIZER = /(%%\{[^\}]+\}|%\{[^\}]+\})/
|
||||
INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/
|
||||
|
||||
def compile_if_an_interpolation(string)
|
||||
if interpolated_str?(string)
|
||||
string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__
|
||||
def i18n_interpolate(v = {})
|
||||
"#{compiled_interpolation_body(string)}"
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
|
||||
string
|
||||
end
|
||||
|
||||
def interpolated_str?(str)
|
||||
str.kind_of?(::String) && str =~ INTERPOLATION_SYNTAX_PATTERN
|
||||
end
|
||||
|
||||
protected
|
||||
# tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"]
|
||||
def tokenize(str)
|
||||
str.split(TOKENIZER)
|
||||
end
|
||||
|
||||
def compiled_interpolation_body(str)
|
||||
tokenize(str).map do |token|
|
||||
(matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token)
|
||||
end.join
|
||||
end
|
||||
|
||||
def handle_interpolation_token(interpolation, matchdata)
|
||||
escaped, pattern, key = matchdata.values_at(1, 2, 3)
|
||||
escaped ? pattern : compile_interpolation_token(key.to_sym)
|
||||
end
|
||||
|
||||
def compile_interpolation_token(key)
|
||||
"\#{#{interpolate_or_raise_missing(key)}}"
|
||||
end
|
||||
|
||||
def interpolate_or_raise_missing(key)
|
||||
escaped_key = escape_key_sym(key)
|
||||
Base::RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key)
|
||||
end
|
||||
|
||||
def interpolate_key(key)
|
||||
[direct_key(key), nil_key(key), missing_key(key)].join('||')
|
||||
end
|
||||
|
||||
def direct_key(key)
|
||||
"((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)"
|
||||
end
|
||||
|
||||
def nil_key(key)
|
||||
"(v.has_key?(#{key}) && '')"
|
||||
end
|
||||
|
||||
def missing_key(key)
|
||||
"raise(MissingInterpolationArgument.new(#{key}, self))"
|
||||
end
|
||||
|
||||
def reserved_key(key)
|
||||
"raise(ReservedInterpolationKey.new(#{key}, self))"
|
||||
end
|
||||
|
||||
def escape_plain_str(str)
|
||||
str.gsub(/"|\\|#/) {|x| "\\#{x}"}
|
||||
end
|
||||
|
||||
def escape_key_sym(key)
|
||||
# rely on Ruby to do all the hard work :)
|
||||
key.to_sym.inspect
|
||||
end
|
||||
end
|
||||
|
||||
def interpolate(locale, string, values)
|
||||
if string.respond_to?(:i18n_interpolate)
|
||||
string.i18n_interpolate(values)
|
||||
elsif values
|
||||
super
|
||||
else
|
||||
string
|
||||
end
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = {})
|
||||
compile_all_strings_in(data)
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
def compile_all_strings_in(data)
|
||||
data.each_value do |value|
|
||||
Compiler.compile_if_an_interpolation(value)
|
||||
compile_all_strings_in(value) if value.kind_of?(Hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,102 +0,0 @@
|
||||
# encoding: utf-8
|
||||
|
||||
require 'i18n/backend/base'
|
||||
require 'active_support/json'
|
||||
|
||||
module I18n
|
||||
module Backend
|
||||
# This is a basic backend for key value stores. It receives on
|
||||
# initialization the store, which should respond to three methods:
|
||||
#
|
||||
# * store#[](key) - Used to get a value
|
||||
# * store#[]=(key, value) - Used to set a value
|
||||
# * store#keys - Used to get all keys
|
||||
#
|
||||
# Since these stores only supports string, all values are converted
|
||||
# to JSON before being stored, allowing it to also store booleans,
|
||||
# hashes and arrays. However, this store does not support Procs.
|
||||
#
|
||||
# As the ActiveRecord backend, Symbols are just supported when loading
|
||||
# translations from the filesystem or through explicit store translations.
|
||||
#
|
||||
# Also, avoid calling I18n.available_locales since it's a somehow
|
||||
# expensive operation in most stores.
|
||||
#
|
||||
# == Example
|
||||
#
|
||||
# To setup I18n to use TokyoCabinet in memory is quite straightforward:
|
||||
#
|
||||
# require 'rufus/tokyo/cabinet' # gem install rufus-tokyo
|
||||
# I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*'))
|
||||
#
|
||||
# == Performance
|
||||
#
|
||||
# You may make this backend even faster by including the Memoize module.
|
||||
# However, notice that you should properly clear the cache if you change
|
||||
# values directly in the key-store.
|
||||
#
|
||||
# == Subtrees
|
||||
#
|
||||
# In most backends, you are allowed to retrieve part of a translation tree:
|
||||
#
|
||||
# I18n.backend.store_translations :en, :foo => { :bar => :baz }
|
||||
# I18n.t "foo" #=> { :bar => :baz }
|
||||
#
|
||||
# This backend supports this feature by default, but it slows down the storage
|
||||
# of new data considerably and makes hard to delete entries. That said, you are
|
||||
# allowed to disable the storage of subtrees on initialization:
|
||||
#
|
||||
# I18n::Backend::KeyValue.new(@store, false)
|
||||
#
|
||||
# This is useful if you are using a KeyValue backend chained to a Simple backend.
|
||||
class KeyValue
|
||||
module Implementation
|
||||
attr_accessor :store
|
||||
|
||||
include Base, Flatten
|
||||
|
||||
def initialize(store, subtrees=true)
|
||||
@store, @subtrees = store, subtrees
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = {})
|
||||
escape = options.fetch(:escape, true)
|
||||
flatten_translations(locale, data, escape, @subtrees).each do |key, value|
|
||||
key = "#{locale}.#{key}"
|
||||
|
||||
case value
|
||||
when Hash
|
||||
if @subtrees && (old_value = @store[key])
|
||||
old_value = ActiveSupport::JSON.decode(old_value)
|
||||
value = old_value.deep_symbolize_keys.deep_merge!(value) if old_value.is_a?(Hash)
|
||||
end
|
||||
when Proc
|
||||
raise "Key-value stores cannot handle procs"
|
||||
end
|
||||
|
||||
@store[key] = ActiveSupport::JSON.encode(value) unless value.is_a?(Symbol)
|
||||
end
|
||||
end
|
||||
|
||||
def available_locales
|
||||
locales = @store.keys.map { |k| k =~ /\./; $` }
|
||||
locales.uniq!
|
||||
locales.compact!
|
||||
locales.map! { |k| k.to_sym }
|
||||
locales
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def lookup(locale, key, scope = [], options = {})
|
||||
key = normalize_flat_keys(locale, key, scope, options[:separator])
|
||||
value = @store["#{locale}.#{key}"]
|
||||
value = ActiveSupport::JSON.decode(value) if value
|
||||
value.is_a?(Hash) ? value.deep_symbolize_keys : value
|
||||
end
|
||||
end
|
||||
|
||||
include Implementation
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Memoize module simply memoizes the values returned by lookup using
|
||||
# a flat hash and can tremendously speed up the lookup process in a backend.
|
||||
#
|
||||
# To enable it you can simply include the Memoize module to your backend:
|
||||
#
|
||||
# I18n::Backend::Simple.send(:include, I18n::Backend::Memoize)
|
||||
#
|
||||
# Notice that it's the responsibility of the backend to define whenever the
|
||||
# cache should be cleaned.
|
||||
module I18n
|
||||
module Backend
|
||||
module Memoize
|
||||
def available_locales
|
||||
@memoized_locales ||= super
|
||||
end
|
||||
|
||||
def store_translations(locale, data, options = {})
|
||||
reset_memoizations!(locale)
|
||||
super
|
||||
end
|
||||
|
||||
def reload!
|
||||
reset_memoizations!
|
||||
super
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def lookup(locale, key, scope = nil, options = {})
|
||||
flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale,
|
||||
key, scope, options[:separator]).to_sym
|
||||
flat_hash = memoized_lookup[locale.to_sym]
|
||||
flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super)
|
||||
end
|
||||
|
||||
def memoized_lookup
|
||||
@memoized_lookup ||= Hash.new { |h, k| h[k] = {} }
|
||||
end
|
||||
|
||||
def reset_memoizations!(locale=nil)
|
||||
@memoized_locales = nil
|
||||
(locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear
|
||||
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