mirror of
https://github.com/github/rails.git
synced 2026-01-12 08:08:31 -05:00
Compare commits
15 Commits
github45
...
activesupp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
775febba74 | ||
|
|
d33a754a92 | ||
|
|
c12cd651c2 | ||
|
|
f709b5d1c8 | ||
|
|
c6f9ec2d8d | ||
|
|
d7440a463c | ||
|
|
7655b80261 | ||
|
|
c5be730a2e | ||
|
|
052556e5cf | ||
|
|
173bc3c9e5 | ||
|
|
01280149f2 | ||
|
|
79c1106fb2 | ||
|
|
8f6982c04b | ||
|
|
a471098ab8 | ||
|
|
87ef1f0e73 |
101
Gemfile
Normal file
101
Gemfile
Normal file
@@ -0,0 +1,101 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gemspec
|
||||
|
||||
if ENV['AREL']
|
||||
gem 'arel', :path => ENV['AREL']
|
||||
else
|
||||
gem 'arel'
|
||||
end
|
||||
|
||||
gem 'bcrypt-ruby', '~> 3.0.0'
|
||||
#gem 'jquery-rails'
|
||||
|
||||
if ENV['JOURNEY']
|
||||
gem 'journey', :path => ENV['JOURNEY']
|
||||
else
|
||||
gem 'journey', :git => 'git://github.com/rails/journey.git', :branch => '1-0-stable'
|
||||
end
|
||||
|
||||
# This needs to be with require false to avoid
|
||||
# it being automatically loaded by sprockets
|
||||
#gem 'uglifier', '>= 1.0.3', :require => false
|
||||
|
||||
gem 'rake', '>= 0.8.7'
|
||||
gem 'mocha', '>= 0.13.0', :require => false
|
||||
|
||||
group :doc do
|
||||
# The current sdoc cannot generate GitHub links due
|
||||
# to a bug, but the PR that fixes it has been there
|
||||
# for some weeks unapplied. As a temporary solution
|
||||
# this is our own fork with the fix.
|
||||
gem 'sdoc', :git => 'git://github.com/fxn/sdoc.git'
|
||||
gem 'RedCloth', '~> 4.2'
|
||||
gem 'w3c_validators'
|
||||
end
|
||||
|
||||
# AS
|
||||
gem 'memcache-client', '>= 1.8.5'
|
||||
|
||||
platforms :mri_18 do
|
||||
gem 'system_timer'
|
||||
gem 'json'
|
||||
end
|
||||
|
||||
# Add your own local bundler stuff
|
||||
instance_eval File.read '.Gemfile' if File.exists? '.Gemfile'
|
||||
|
||||
platforms :mri do
|
||||
group :test do
|
||||
gem 'ruby-prof', '~> 0.11.2' if RUBY_VERSION < '2.0'
|
||||
end
|
||||
end
|
||||
|
||||
platforms :ruby do
|
||||
gem 'yajl-ruby'
|
||||
gem 'nokogiri', '>= 1.4.5'
|
||||
|
||||
# AR
|
||||
gem 'sqlite3', '~> 1.3.5'
|
||||
|
||||
group :db do
|
||||
gem 'pg', '>= 0.11.0'
|
||||
gem 'mysql', '>= 2.8.1'
|
||||
gem 'mysql2', '>= 0.3.10'
|
||||
end
|
||||
end
|
||||
|
||||
platforms :jruby do
|
||||
gem 'json'
|
||||
gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.0'
|
||||
|
||||
# This is needed by now to let tests work on JRuby
|
||||
# TODO: When the JRuby guys merge jruby-openssl in
|
||||
# jruby this will be removed
|
||||
gem 'jruby-openssl'
|
||||
|
||||
group :db do
|
||||
gem 'activerecord-jdbcmysql-adapter', '>= 1.2.0'
|
||||
gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.0'
|
||||
end
|
||||
end
|
||||
|
||||
# gems that are necessary for ActiveRecord tests with Oracle database
|
||||
if ENV['ORACLE_ENHANCED_PATH'] || ENV['ORACLE_ENHANCED']
|
||||
platforms :ruby do
|
||||
gem 'ruby-oci8', '>= 2.0.4'
|
||||
end
|
||||
if ENV['ORACLE_ENHANCED_PATH']
|
||||
gem 'activerecord-oracle_enhanced-adapter', :path => ENV['ORACLE_ENHANCED_PATH']
|
||||
else
|
||||
gem 'activerecord-oracle_enhanced-adapter', :git => 'git://github.com/rsim/oracle-enhanced.git'
|
||||
end
|
||||
end
|
||||
|
||||
# A gem necessary for ActiveRecord tests with IBM DB
|
||||
gem 'ibm_db' if ENV['IBM_DB']
|
||||
|
||||
gem 'benchmark-ips'
|
||||
|
||||
gem "tzinfo"
|
||||
gem "builder"
|
||||
18
Gemfile.sh
18
Gemfile.sh
@@ -1,9 +1,9 @@
|
||||
gem install mocha -v=0.13.1
|
||||
gem install rake -v=10.1.0
|
||||
gem install rdoc -v=4.0.1
|
||||
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
|
||||
gem install builder -v=3.2.2
|
||||
gem install mocha -v=0.13.1
|
||||
gem install rake -v=10.1.0
|
||||
gem install rdoc -v=4.0.1
|
||||
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 multi_json -v=1.8.2
|
||||
gem install i18n -v=0.6.5
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.3.14.github45
|
||||
2.3.14.github30
|
||||
|
||||
@@ -14,4 +14,5 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.add_dependency 'activesupport', "= #{version}"
|
||||
s.add_dependency 'rack', '~> 1.4'
|
||||
s.add_dependency 'erubis', '~> 2.7.0'
|
||||
end
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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, UrlWriter]
|
||||
[Base, CGIHandler, CgiRequest, Request, Response, Http::Headers, UrlRewriter, UrlWriter]
|
||||
end
|
||||
|
||||
autoload :Base, 'action_controller/base'
|
||||
@@ -99,6 +99,10 @@ module ActionController
|
||||
autoload :CookieStore, 'action_controller/session/cookie_store'
|
||||
autoload :MemCacheStore, 'action_controller/session/mem_cache_store'
|
||||
end
|
||||
|
||||
# DEPRECATE: Remove CGI support
|
||||
autoload :CgiRequest, 'action_controller/cgi_process'
|
||||
autoload :CGIHandler, 'action_controller/cgi_process'
|
||||
end
|
||||
|
||||
autoload :Mime, 'action_controller/mime_type'
|
||||
|
||||
@@ -1320,14 +1320,7 @@ module ActionController #:nodoc:
|
||||
render
|
||||
end
|
||||
|
||||
CVE_2014_0310 = Class.new(StandardError)
|
||||
|
||||
def perform_action
|
||||
# CVE-2014-0130 protection
|
||||
if action_name.include? "/"
|
||||
raise CVE_2014_0310
|
||||
end
|
||||
|
||||
if action_methods.include?(action_name)
|
||||
send(action_name)
|
||||
default_render unless performed?
|
||||
|
||||
@@ -87,6 +87,7 @@ module ActionController #:nodoc:
|
||||
log_message << " [#{complete_request_uri rescue "unknown"}]"
|
||||
|
||||
logger.info(log_message)
|
||||
response.headers["X-Runtime"] = "%.0f" % ms
|
||||
else
|
||||
perform_action_without_benchmark
|
||||
end
|
||||
|
||||
@@ -39,9 +39,9 @@ module ActionController #:nodoc:
|
||||
if cache = read_fragment(name, options)
|
||||
buffer.safe_concat(cache.html_safe)
|
||||
else
|
||||
pos = buffer.bytesize
|
||||
pos = buffer.length
|
||||
block.call
|
||||
write_fragment(name, buffer.byteslice(pos..-1), options)
|
||||
write_fragment(name, buffer[pos..-1], options)
|
||||
end
|
||||
else
|
||||
block.call
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
require 'action_controller/cgi_ext/stdinput'
|
||||
require 'action_controller/cgi_ext/query_extension'
|
||||
require 'action_controller/cgi_ext/cookie'
|
||||
|
||||
class CGI #:nodoc:
|
||||
include ActionController::CgiExt::Stdinput
|
||||
|
||||
class << self
|
||||
alias :escapeHTML_fail_on_nil :escapeHTML
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
require 'delegate'
|
||||
require 'cgi'
|
||||
require 'cgi/cookie'
|
||||
|
||||
CGI.module_eval { remove_const "Cookie" }
|
||||
|
||||
@@ -26,7 +24,7 @@ class CGI #:nodoc:
|
||||
# * <tt>:secure</tt> - Whether this cookie is a secure cookie or not (defaults to
|
||||
# +false+). Secure cookies are only transmitted to HTTPS servers.
|
||||
# * <tt>:http_only</tt> - Whether this cookie can be accessed by client side scripts (e.g. document.cookie) or only over HTTP.
|
||||
# More details in http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx. Defaults to +false+.
|
||||
# More details in http://msdn2.microsoft.com/en-us/library/system.web.httpcookie.httponly.aspx. Defaults to +false+.
|
||||
#
|
||||
# These keywords correspond to attributes of the cookie object.
|
||||
def initialize(name = '', *value)
|
||||
|
||||
22
actionpack/lib/action_controller/cgi_ext/query_extension.rb
Normal file
22
actionpack/lib/action_controller/cgi_ext/query_extension.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
require 'cgi'
|
||||
|
||||
class CGI #:nodoc:
|
||||
module QueryExtension
|
||||
# Remove the old initialize_query method before redefining it.
|
||||
remove_method :initialize_query
|
||||
|
||||
# Neuter CGI parameter parsing.
|
||||
def initialize_query
|
||||
# Fix some strange request environments.
|
||||
env_table['REQUEST_METHOD'] ||= 'GET'
|
||||
|
||||
# POST assumes missing Content-Type is application/x-www-form-urlencoded.
|
||||
if env_table['CONTENT_TYPE'].blank? && env_table['REQUEST_METHOD'] == 'POST'
|
||||
env_table['CONTENT_TYPE'] = 'application/x-www-form-urlencoded'
|
||||
end
|
||||
|
||||
@cookies = CGI::Cookie::parse(env_table['HTTP_COOKIE'] || env_table['COOKIE'])
|
||||
@params = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
24
actionpack/lib/action_controller/cgi_ext/stdinput.rb
Normal file
24
actionpack/lib/action_controller/cgi_ext/stdinput.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
require 'cgi'
|
||||
|
||||
module ActionController
|
||||
module CgiExt
|
||||
# Publicize the CGI's internal input stream so we can lazy-read
|
||||
# request.body. Make it writable so we don't have to play $stdin games.
|
||||
module Stdinput
|
||||
def self.included(base)
|
||||
base.class_eval do
|
||||
remove_method :stdinput
|
||||
attr_accessor :stdinput
|
||||
end
|
||||
|
||||
base.alias_method_chain :initialize, :stdinput
|
||||
end
|
||||
|
||||
def initialize_with_stdinput(type = nil, stdinput = $stdin)
|
||||
@stdinput = stdinput
|
||||
@stdinput.set_encoding(Encoding::BINARY) if @stdinput.respond_to?(:set_encoding)
|
||||
initialize_without_stdinput(type || 'query')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
77
actionpack/lib/action_controller/cgi_process.rb
Normal file
77
actionpack/lib/action_controller/cgi_process.rb
Normal file
@@ -0,0 +1,77 @@
|
||||
require 'action_controller/cgi_ext'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class CGIHandler
|
||||
module ProperStream
|
||||
def each
|
||||
while line = gets
|
||||
yield line
|
||||
end
|
||||
end
|
||||
|
||||
def read(*args)
|
||||
if args.empty?
|
||||
super || ""
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.dispatch_cgi(app, cgi, out = $stdout)
|
||||
env = cgi.__send__(:env_table)
|
||||
env.delete "HTTP_CONTENT_LENGTH"
|
||||
|
||||
cgi.stdinput.extend ProperStream
|
||||
|
||||
env["SCRIPT_NAME"] = "" if env["SCRIPT_NAME"] == "/"
|
||||
|
||||
env.update({
|
||||
"rack.version" => [0,1],
|
||||
"rack.input" => cgi.stdinput,
|
||||
"rack.errors" => $stderr,
|
||||
"rack.multithread" => false,
|
||||
"rack.multiprocess" => true,
|
||||
"rack.run_once" => false,
|
||||
"rack.url_scheme" => ["yes", "on", "1"].include?(env["HTTPS"]) ? "https" : "http"
|
||||
})
|
||||
|
||||
env["QUERY_STRING"] ||= ""
|
||||
env["HTTP_VERSION"] ||= env["SERVER_PROTOCOL"]
|
||||
env["REQUEST_PATH"] ||= "/"
|
||||
env.delete "PATH_INFO" if env["PATH_INFO"] == ""
|
||||
|
||||
status, headers, body = app.call(env)
|
||||
begin
|
||||
out.binmode if out.respond_to?(:binmode)
|
||||
out.sync = false if out.respond_to?(:sync=)
|
||||
|
||||
headers['Status'] = status.to_s
|
||||
|
||||
if headers.include?('Set-Cookie')
|
||||
headers['cookie'] = headers.delete('Set-Cookie').split("\n")
|
||||
end
|
||||
|
||||
out.write(cgi.header(headers))
|
||||
|
||||
body.each { |part|
|
||||
out.write part
|
||||
out.flush if out.respond_to?(:flush)
|
||||
}
|
||||
ensure
|
||||
body.close if body.respond_to?(:close)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class CgiRequest #:nodoc:
|
||||
DEFAULT_SESSION_OPTIONS = {
|
||||
:database_manager => nil,
|
||||
:prefix => "ruby_sess.",
|
||||
:session_path => "/",
|
||||
:session_key => "_session_id",
|
||||
:cookie_only => true,
|
||||
:session_http_only => true
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -22,6 +22,11 @@ module ActionController
|
||||
end
|
||||
end
|
||||
|
||||
# DEPRECATE: Remove CGI support
|
||||
def dispatch(cgi = nil, session_options = CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
|
||||
new(output).dispatch_cgi(cgi, session_options)
|
||||
end
|
||||
|
||||
# Add a preparation callback. Preparation callbacks are run before every
|
||||
# request in development mode, and before the first request in production
|
||||
# mode.
|
||||
@@ -37,7 +42,13 @@ module ActionController
|
||||
end
|
||||
|
||||
def run_prepare_callbacks
|
||||
new.send :run_callbacks, :prepare_dispatch
|
||||
if defined?(Rails) && Rails.logger
|
||||
logger = Rails.logger
|
||||
else
|
||||
logger = Logger.new($stderr)
|
||||
end
|
||||
|
||||
new(logger).send :run_callbacks, :prepare_dispatch
|
||||
end
|
||||
|
||||
def reload_application
|
||||
@@ -64,8 +75,10 @@ module ActionController
|
||||
include ActiveSupport::Callbacks
|
||||
define_callbacks :prepare_dispatch, :before_dispatch, :after_dispatch
|
||||
|
||||
def initialize
|
||||
build_middleware_stack
|
||||
# DEPRECATE: Remove arguments, since they are only used by CGI
|
||||
def initialize(output = $stdout, request = nil, response = nil)
|
||||
@output = output
|
||||
build_middleware_stack if @@cache_classes
|
||||
end
|
||||
|
||||
def dispatch
|
||||
@@ -83,11 +96,21 @@ module ActionController
|
||||
end
|
||||
end
|
||||
|
||||
# DEPRECATE: Remove CGI support
|
||||
def dispatch_cgi(cgi, session_options)
|
||||
CGIHandler.dispatch_cgi(self, cgi, @output)
|
||||
end
|
||||
|
||||
def call(env)
|
||||
if @@cache_classes
|
||||
@app.call(env)
|
||||
else
|
||||
Reloader.run do
|
||||
# When class reloading is turned on, we will want to rebuild the
|
||||
# middleware stack every time we process a request. If we don't
|
||||
# rebuild the middleware stack, then the stack may contain references
|
||||
# to old classes metal classes, which will b0rk class reloading.
|
||||
build_middleware_stack
|
||||
@app.call(env)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -45,74 +45,37 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def []=(k, v)
|
||||
k = k.to_s
|
||||
@flash[k] = v
|
||||
@flash.discard(k)
|
||||
v
|
||||
end
|
||||
|
||||
def [](k)
|
||||
@flash[k.to_s]
|
||||
@flash[k]
|
||||
end
|
||||
end
|
||||
|
||||
class FlashHash < Hash
|
||||
def self.from_session_value(value)
|
||||
flash = case value
|
||||
when FlashHash # Rails 2.3
|
||||
value
|
||||
when Hash # Rails 4.0
|
||||
flashes = value['flashes'] || {}
|
||||
flashes.stringify_keys!
|
||||
discard = value['discard'] || []
|
||||
discard = discard.map do |item|
|
||||
item.kind_of?(Symbol) ? item.to_s : item
|
||||
end
|
||||
used = Hash[flashes.keys.map{|k| [k, discard.include?(k)] }]
|
||||
|
||||
new_from_values(flashes, used)
|
||||
else
|
||||
new
|
||||
end
|
||||
flash
|
||||
end
|
||||
|
||||
def initialize #:nodoc:
|
||||
super
|
||||
@used = {}
|
||||
end
|
||||
|
||||
def to_session_value
|
||||
return nil if empty?
|
||||
rails_3_discard_list = @used.map{|k,v| k if v}.compact
|
||||
{'discard' => rails_3_discard_list, 'flashes' => Hash[to_a]}
|
||||
end
|
||||
|
||||
def []=(k, v) #:nodoc:
|
||||
k = k.to_s
|
||||
keep(k)
|
||||
super(k, v)
|
||||
end
|
||||
|
||||
def [](k)
|
||||
super(k.to_s)
|
||||
end
|
||||
|
||||
def delete(k)
|
||||
super(k.to_s)
|
||||
super
|
||||
end
|
||||
|
||||
def update(h) #:nodoc:
|
||||
h.stringify_keys!
|
||||
h.keys.each { |k| keep(k) }
|
||||
super(h)
|
||||
super
|
||||
end
|
||||
|
||||
alias :merge! :update
|
||||
|
||||
def replace(h) #:nodoc:
|
||||
@used = {}
|
||||
super(h.stringify_keys)
|
||||
super
|
||||
end
|
||||
|
||||
# Sets a flash that will not be available to the next action, only to the current.
|
||||
@@ -163,7 +126,8 @@ module ActionController #:nodoc:
|
||||
end
|
||||
|
||||
def store(session, key = "flash")
|
||||
session[key] = to_session_value
|
||||
return if self.empty?
|
||||
session[key] = self
|
||||
end
|
||||
|
||||
private
|
||||
@@ -174,20 +138,11 @@ module ActionController #:nodoc:
|
||||
# use('msg', false) # marks the "msg" entry as unused (keeps it around for one more action)
|
||||
def use(k=nil, v=true)
|
||||
unless k.nil?
|
||||
@used[k.to_s] = v
|
||||
@used[k] = v
|
||||
else
|
||||
keys.each{ |key| use(key, v) }
|
||||
end
|
||||
end
|
||||
|
||||
def self.new_from_values(flashes, used)
|
||||
new.tap do |flash_hash|
|
||||
flashes.each do |k, v|
|
||||
flash_hash[k] = v
|
||||
end
|
||||
flash_hash.instance_variable_set("@used", used)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods #:nodoc:
|
||||
@@ -213,11 +168,11 @@ module ActionController #:nodoc:
|
||||
if notice = response_status_and_flash.delete(:notice)
|
||||
flash[:notice] = notice
|
||||
end
|
||||
|
||||
|
||||
if other_flashes = response_status_and_flash.delete(:flash)
|
||||
flash.update(other_flashes)
|
||||
end
|
||||
|
||||
|
||||
redirect_to_without_flash(options, response_status_and_flash)
|
||||
end
|
||||
|
||||
@@ -226,19 +181,19 @@ module ActionController #:nodoc:
|
||||
# to put a new one.
|
||||
def flash #:doc:
|
||||
if !defined?(@_flash)
|
||||
@_flash = Flash::FlashHash.from_session_value(session["flash"])
|
||||
@_flash = session["flash"] || FlashHash.new
|
||||
@_flash.sweep
|
||||
end
|
||||
|
||||
@_flash
|
||||
end
|
||||
|
||||
|
||||
|
||||
# Convenience accessor for flash[:alert]
|
||||
def alert
|
||||
flash[:alert]
|
||||
end
|
||||
|
||||
|
||||
# Convenience accessor for flash[:alert]=
|
||||
def alert=(message)
|
||||
flash[:alert] = message
|
||||
@@ -248,7 +203,7 @@ module ActionController #:nodoc:
|
||||
def notice
|
||||
flash[:notice]
|
||||
end
|
||||
|
||||
|
||||
# Convenience accessor for flash[:notice]=
|
||||
def notice=(message)
|
||||
flash[:notice] = message
|
||||
|
||||
@@ -423,13 +423,13 @@ EOM
|
||||
|
||||
# Override Rack's GET method to support indifferent access
|
||||
def GET
|
||||
@env["action_controller.request.query_parameters"] ||= deep_munge(normalize_parameters(super) || {})
|
||||
@env["action_controller.request.query_parameters"] ||= normalize_parameters(super)
|
||||
end
|
||||
alias_method :query_parameters, :GET
|
||||
|
||||
# Override Rack's POST method to support indifferent access
|
||||
def POST
|
||||
@env["action_controller.request.request_parameters"] ||= deep_munge(normalize_parameters(super) || {})
|
||||
@env["action_controller.request.request_parameters"] ||= normalize_parameters(super)
|
||||
end
|
||||
alias_method :request_parameters, :POST
|
||||
|
||||
@@ -469,22 +469,6 @@ EOM
|
||||
!(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
|
||||
end
|
||||
|
||||
# Remove nils from the params hash
|
||||
def deep_munge(hash)
|
||||
hash.each do |k, v|
|
||||
case v
|
||||
when Array
|
||||
v.grep(Hash) { |x| deep_munge(x) }
|
||||
v.compact!
|
||||
hash[k] = nil if v.empty?
|
||||
when Hash
|
||||
deep_munge(v)
|
||||
end
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
# Convert nested Hashs to HashWithIndifferentAccess and replace
|
||||
# file upload hashs with UploadedFile objects
|
||||
def normalize_parameters(value)
|
||||
|
||||
@@ -106,7 +106,7 @@ module ActionController #:nodoc:
|
||||
# Sets the token value for the current session. Pass a <tt>:secret</tt> option
|
||||
# in +protect_from_forgery+ to add a custom salt to the hash.
|
||||
def form_authenticity_token
|
||||
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
|
||||
session[:_csrf_token] ||= SecureRandom.base64(32)
|
||||
end
|
||||
|
||||
def protect_against_forgery?
|
||||
|
||||
@@ -2,7 +2,7 @@ require 'rack/utils'
|
||||
|
||||
module ActionController
|
||||
module Session
|
||||
class AbstractStore
|
||||
class AbstractStore
|
||||
ENV_SESSION_KEY = 'rack.session'.freeze
|
||||
ENV_SESSION_OPTIONS_KEY = 'rack.session.options'.freeze
|
||||
|
||||
@@ -55,17 +55,17 @@ module ActionController
|
||||
|
||||
def [](key)
|
||||
load_for_read!
|
||||
super(key.to_s) || super(key)
|
||||
super
|
||||
end
|
||||
|
||||
def has_key?(key)
|
||||
load_for_read!
|
||||
super(key.to_s) || super(key)
|
||||
super
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
load_for_write!
|
||||
super(key.to_s, value)
|
||||
super
|
||||
end
|
||||
|
||||
def clear
|
||||
@@ -87,9 +87,7 @@ module ActionController
|
||||
|
||||
def delete(key)
|
||||
load_for_write!
|
||||
value = super(key)
|
||||
string_value = super(key.to_s)
|
||||
string_value || value
|
||||
super
|
||||
end
|
||||
|
||||
def data
|
||||
@@ -121,7 +119,7 @@ module ActionController
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
def load_for_read!
|
||||
load! if !loaded? && exists?
|
||||
end
|
||||
@@ -185,7 +183,7 @@ module ActionController
|
||||
request = ActionController::Request.new(env)
|
||||
|
||||
return response if (options[:secure] && !request.ssl?)
|
||||
|
||||
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
|
||||
|
||||
sid = options[:id] || generate_sid
|
||||
@@ -207,14 +205,14 @@ module ActionController
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
def prepare!(env)
|
||||
env[ENV_SESSION_KEY] = SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = OptionsHash.new(self, env, @default_options)
|
||||
end
|
||||
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
@@ -224,7 +222,7 @@ module ActionController
|
||||
[sid, session]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def extract_session_id(env)
|
||||
stale_session_check! do
|
||||
request = Rack::Request.new(env)
|
||||
@@ -237,7 +235,7 @@ module ActionController
|
||||
def current_session_id(env)
|
||||
env[ENV_SESSION_OPTIONS_KEY][:id]
|
||||
end
|
||||
|
||||
|
||||
def exists?(env)
|
||||
current_session_id(env).present?
|
||||
end
|
||||
@@ -249,11 +247,11 @@ module ActionController
|
||||
def set_session(env, sid, session_data)
|
||||
raise '#set_session needs to be implemented.'
|
||||
end
|
||||
|
||||
|
||||
def destroy(env)
|
||||
raise '#destroy needs to be implemented.'
|
||||
end
|
||||
|
||||
|
||||
module SessionUtils
|
||||
private
|
||||
def stale_session_check!
|
||||
|
||||
@@ -37,7 +37,7 @@ module ActionController
|
||||
# Note that changing digest or secret invalidates all existing sessions!
|
||||
class CookieStore
|
||||
include AbstractStore::SessionUtils
|
||||
|
||||
|
||||
# Cookies can typically store 4096 bytes.
|
||||
MAX = 4096
|
||||
SECRET_MIN_LENGTH = 30 # characters
|
||||
@@ -86,8 +86,7 @@ module ActionController
|
||||
@secret = options.delete(:secret).freeze
|
||||
|
||||
@digest = options.delete(:digest) || 'SHA1'
|
||||
@serializer = options.delete(:serializer) || Marshal
|
||||
@verifier = verifier_for(@secret, @digest, @serializer)
|
||||
@verifier = verifier_for(@secret, @digest)
|
||||
|
||||
@default_options = DEFAULT_OPTIONS.merge(options).freeze
|
||||
|
||||
@@ -96,21 +95,14 @@ module ActionController
|
||||
|
||||
def call(env)
|
||||
prepare!(env)
|
||||
|
||||
|
||||
status, headers, body = @app.call(env)
|
||||
|
||||
session_data = env[ENV_SESSION_KEY]
|
||||
options = env[ENV_SESSION_OPTIONS_KEY]
|
||||
request = ActionController::Request.new(env)
|
||||
|
||||
|
||||
if !(options[:secure] && !request.ssl?) && (!session_data.is_a?(AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after])
|
||||
|
||||
# Backport standard Rack::Session::Cookie behavior
|
||||
# Skip writing session if env['rack.session.options'][:skip] is set
|
||||
if options[:skip]
|
||||
return [status, headers, body]
|
||||
end
|
||||
|
||||
session_data.send(:load!) if session_data.is_a?(AbstractStore::SessionHash) && !session_data.loaded?
|
||||
|
||||
persistent_session_id!(session_data)
|
||||
@@ -130,7 +122,7 @@ module ActionController
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
|
||||
def prepare!(env)
|
||||
env[ENV_SESSION_KEY] = AbstractStore::SessionHash.new(self, env)
|
||||
env[ENV_SESSION_OPTIONS_KEY] = AbstractStore::OptionsHash.new(self, env, @default_options)
|
||||
@@ -139,13 +131,13 @@ module ActionController
|
||||
def load_session(env)
|
||||
data = unpacked_cookie_data(env)
|
||||
data = persistent_session_id!(data)
|
||||
[data["session_id"] || data[:session_id], data]
|
||||
[data[:session_id], data]
|
||||
end
|
||||
|
||||
|
||||
def extract_session_id(env)
|
||||
if data = unpacked_cookie_data(env)
|
||||
persistent_session_id!(data) unless data.empty?
|
||||
data["session_id"] || data[:session_id]
|
||||
data[:session_id]
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -209,19 +201,19 @@ module ActionController
|
||||
|
||||
if secret.length < SECRET_MIN_LENGTH
|
||||
raise ArgumentError, "Secret should be something secure, " +
|
||||
"like \"#{ActiveSupport::SecureRandom.hex(16)}\". The value you " +
|
||||
"like \"#{SecureRandom.hex(16)}\". The value you " +
|
||||
"provided, \"#{secret}\", is shorter than the minimum length " +
|
||||
"of #{SECRET_MIN_LENGTH} characters"
|
||||
end
|
||||
end
|
||||
|
||||
def verifier_for(secret, digest, serializer)
|
||||
def verifier_for(secret, digest)
|
||||
key = secret.respond_to?(:call) ? secret.call : secret
|
||||
ActiveSupport::MessageVerifier.new(key, digest: digest, serializer: serializer)
|
||||
ActiveSupport::MessageVerifier.new(key, digest: digest)
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
@@ -233,12 +225,12 @@ module ActionController
|
||||
end
|
||||
|
||||
def inject_persistent_session_id(data)
|
||||
requires_session_id?(data) ? { "session_id" => generate_sid } : {}
|
||||
requires_session_id?(data) ? { :session_id => generate_sid } : {}
|
||||
end
|
||||
|
||||
def requires_session_id?(data)
|
||||
if data
|
||||
data.respond_to?(:key?) && !(data.key?("session_id") || data.key?(:session_id))
|
||||
data.respond_to?(:key?) && !data.key?(:session_id)
|
||||
else
|
||||
true
|
||||
end
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
require 'active_support/core_ext/string/bytesize'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
# Methods for sending arbitrary data and for streaming files to the browser,
|
||||
# instead of rendering.
|
||||
|
||||
@@ -219,7 +219,7 @@ module ActionController #:nodoc:
|
||||
|
||||
# A shortcut to the flash. Returns an empty hash if no session flash exists.
|
||||
def flash
|
||||
ActionController::Flash::FlashHash.from_session_value(session["flash"]) || {}
|
||||
session['flash'] || {}
|
||||
end
|
||||
|
||||
# Do we have a flash?
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -768,11 +768,7 @@ module ActionView
|
||||
options = options.stringify_keys
|
||||
tag_value = options.delete("value")
|
||||
name_and_id = options.dup
|
||||
if name_and_id.has_key?("for")
|
||||
name_and_id["id"] = name_and_id["for"]
|
||||
else
|
||||
name_and_id.delete("id")
|
||||
end
|
||||
name_and_id["id"] = name_and_id["for"]
|
||||
add_default_name_and_id_for_value(tag_value, name_and_id)
|
||||
options.delete("index")
|
||||
options["for"] ||= name_and_id["id"]
|
||||
@@ -932,15 +928,15 @@ module ActionView
|
||||
|
||||
def add_default_name_and_id(options)
|
||||
if options.has_key?("index")
|
||||
options["name"] = tag_name_with_index(options["index"]) unless options.has_key?("name")
|
||||
options["id"] = tag_id_with_index(options["index"]) unless options.has_key?("id")
|
||||
options["name"] ||= tag_name_with_index(options["index"])
|
||||
options["id"] ||= tag_id_with_index(options["index"])
|
||||
options.delete("index")
|
||||
elsif defined?(@auto_index)
|
||||
options["name"] = tag_name_with_index(@auto_index) unless options.has_key?("name")
|
||||
options["id"] = tag_id_with_index(@auto_index) unless options.has_key?("id")
|
||||
options["name"] ||= tag_name_with_index(@auto_index)
|
||||
options["id"] ||= tag_id_with_index(@auto_index)
|
||||
else
|
||||
options["name"] = tag_name + (options.has_key?('multiple') ? '[]' : '') unless options.has_key?("name")
|
||||
options["id"] = tag_id unless options.has_key?("id")
|
||||
options["name"] ||= tag_name + (options.has_key?('multiple') ? '[]' : '')
|
||||
options["id"] ||= tag_id
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -73,8 +73,6 @@ module ActionView
|
||||
def number_to_currency(number, options = {})
|
||||
options.symbolize_keys!
|
||||
|
||||
options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
|
||||
|
||||
defaults = I18n.translate(:'number.format', :locale => options[:locale], :raise => true) rescue {}
|
||||
currency = I18n.translate(:'number.currency.format', :locale => options[:locale], :raise => true) rescue {}
|
||||
defaults = defaults.merge(currency)
|
||||
@@ -87,13 +85,11 @@ module ActionView
|
||||
separator = '' if precision == 0
|
||||
|
||||
begin
|
||||
value = number_with_precision(number,
|
||||
format.gsub(/%n/, number_with_precision(number,
|
||||
:precision => precision,
|
||||
:delimiter => delimiter,
|
||||
:separator => separator)
|
||||
value = ERB::Util.html_escape(value) if value
|
||||
unit = ERB::Util.html_escape(unit)
|
||||
format.gsub(/%n/, value).gsub(/%u/, unit).html_safe
|
||||
).gsub(/%u/, unit).html_safe
|
||||
rescue
|
||||
number
|
||||
end
|
||||
|
||||
@@ -17,7 +17,7 @@ module ActionView
|
||||
src << "@output_buffer.safe_append='"
|
||||
src << "\n" * @newline_pending if @newline_pending > 0
|
||||
src << escape_text(text)
|
||||
src << "'.freeze;"
|
||||
src << "';"
|
||||
|
||||
@newline_pending = 0
|
||||
end
|
||||
@@ -63,7 +63,7 @@ module ActionView
|
||||
|
||||
def flush_newline_if_pending(src)
|
||||
if @newline_pending > 0
|
||||
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;"
|
||||
src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';"
|
||||
@newline_pending = 0
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
$:.unshift File.expand_path('../../lib', __FILE__)
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
$:.unshift File.expand_path('../fixtures/helpers', __FILE__)
|
||||
$:.unshift File.expand_path('../fixtures/alternate_helpers', __FILE__)
|
||||
begin
|
||||
old, $VERBOSE = $VERBOSE, nil
|
||||
require File.expand_path('../../../load_paths', __FILE__)
|
||||
ensure
|
||||
$VERBOSE = old
|
||||
end
|
||||
|
||||
require 'rubygems'
|
||||
require 'yaml'
|
||||
|
||||
@@ -622,19 +622,6 @@ class FragmentCachingTest < ActionController::TestCase
|
||||
assert_equal 'generated till now -> fragment content', buffer
|
||||
end
|
||||
|
||||
def test_fragment_for_bytesize
|
||||
buffer = "\xC4\x8D"
|
||||
buffer.force_encoding('ASCII-8BIT')
|
||||
|
||||
@controller.fragment_for(buffer, 'bytesize') do
|
||||
buffer.force_encoding('UTF-8')
|
||||
buffer << "abc"
|
||||
end
|
||||
|
||||
assert_equal Encoding::UTF_8, buffer.encoding
|
||||
assert_equal "abc", @store.read('views/bytesize')
|
||||
end
|
||||
|
||||
def test_html_safety
|
||||
assert_nil @store.read('views/name')
|
||||
content = 'value'.html_safe
|
||||
|
||||
@@ -45,7 +45,7 @@ class DispatcherTest < Test::Unit::TestCase
|
||||
def test_rebuilds_middleware_stack_on_every_request_if_in_loading_mode
|
||||
dispatcher = create_dispatcher(false)
|
||||
dispatcher.instance_variable_set(:"@app", lambda { |env| })
|
||||
dispatcher.expects(:build_middleware_stack).never
|
||||
dispatcher.expects(:build_middleware_stack).twice
|
||||
dispatcher.call(nil)
|
||||
Reloader.default_lock.unlock
|
||||
dispatcher.call(nil)
|
||||
|
||||
@@ -5,6 +5,7 @@ class BaseRackTest < ActiveSupport::TestCase
|
||||
@env = {
|
||||
"HTTP_MAX_FORWARDS" => "10",
|
||||
"SERVER_NAME" => "glu.ttono.us",
|
||||
"FCGI_ROLE" => "RESPONDER",
|
||||
"AUTH_TYPE" => "Basic",
|
||||
"HTTP_X_FORWARDED_HOST" => "glu.ttono.us",
|
||||
"HTTP_ACCEPT_CHARSET" => "UTF-8",
|
||||
|
||||
@@ -79,7 +79,7 @@ module RequestForgeryProtectionTests
|
||||
def setup
|
||||
@token = "cf50faa3fe97702ca1ae"
|
||||
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
|
||||
SecureRandom.stubs(:base64).returns(@token)
|
||||
ActionController::Base.request_forgery_protection_token = :authenticity_token
|
||||
end
|
||||
|
||||
@@ -186,7 +186,7 @@ class RequestForgeryProtectionControllerTest < ActionController::TestCase
|
||||
include RequestForgeryProtectionTests
|
||||
|
||||
test 'should emit a csrf-token meta tag' do
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||
SecureRandom.stubs(:base64).returns(@token + '<=?')
|
||||
get :meta
|
||||
assert_equal %(<meta name="csrf-param" content="authenticity_token"/>\n<meta name="csrf-token" content="cf50faa3fe97702ca1ae<=?"/>), @response.body
|
||||
end
|
||||
@@ -208,7 +208,7 @@ class FreeCookieControllerTest < ActionController::TestCase
|
||||
@response = ActionController::TestResponse.new
|
||||
@token = "cf50faa3fe97702ca1ae"
|
||||
|
||||
ActiveSupport::SecureRandom.stubs(:base64).returns(@token)
|
||||
SecureRandom.stubs(:base64).returns(@token)
|
||||
end
|
||||
|
||||
def test_should_not_render_form_with_token_tag
|
||||
|
||||
@@ -175,10 +175,6 @@ class FormHelperTest < ActionView::TestCase
|
||||
I18n.locale = old_locale
|
||||
end
|
||||
|
||||
def test_label_with_for_attribute_as_nil
|
||||
assert_dom_equal('<label>Title</label>', label(:post, :title, nil, :for => nil))
|
||||
end
|
||||
|
||||
def test_label_with_for_attribute_as_symbol
|
||||
assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for"))
|
||||
end
|
||||
@@ -278,11 +274,6 @@ class FormHelperTest < ActionView::TestCase
|
||||
hidden_field("post", "title", :value => "Something Else")
|
||||
end
|
||||
|
||||
def test_text_field_with_id_as_nil
|
||||
assert_dom_equal '<input name="post[title]" type="hidden" value="Hello World" />',
|
||||
hidden_field("post", "title", :id => nil)
|
||||
end
|
||||
|
||||
def test_check_box
|
||||
assert_dom_equal(
|
||||
'<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
|
||||
|
||||
@@ -3,12 +3,6 @@ require 'abstract_unit'
|
||||
class NumberHelperTest < ActionView::TestCase
|
||||
tests ActionView::Helpers::NumberHelper
|
||||
|
||||
def test_number_helpers_escape_delimiter_and_separator
|
||||
assert_equal "$1<script></script>01", number_to_currency(1.01, :separator => "<script></script>")
|
||||
assert_equal "$1<script></script>000.00", number_to_currency(1000, :delimiter => "<script></script>")
|
||||
assert_equal "<script>1,000.00$</script>", number_to_currency(1000, :format => "<script>%n%u</script>")
|
||||
end
|
||||
|
||||
def test_number_to_phone
|
||||
assert_equal("555-1234", number_to_phone(5551234))
|
||||
assert_equal("800-555-1212", number_to_phone(8005551212))
|
||||
@@ -30,10 +24,9 @@ class NumberHelperTest < ActionView::TestCase
|
||||
assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506))
|
||||
assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0}))
|
||||
assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1}))
|
||||
assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => raw("£"), :separator => ",", :delimiter => ""}))
|
||||
assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}))
|
||||
assert_equal("£1234567890,50", number_to_currency(1234567890.50, {:unit => "£", :separator => ",", :delimiter => ""}))
|
||||
assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))
|
||||
assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => raw("Kč"), :format => "%n %u"}))
|
||||
assert_equal("1,234,567,890.50 Kč", number_to_currency("1234567890.50", {:unit => "Kč", :format => "%n %u"}))
|
||||
#assert_equal("$x.", number_to_currency("x")) # fails due to API consolidation
|
||||
assert_equal("$x", number_to_currency("x"))
|
||||
assert_nil number_to_currency(nil)
|
||||
|
||||
@@ -9,8 +9,8 @@ module RenderTestCases
|
||||
|
||||
# Reload and register danish language for testing
|
||||
I18n.reload!
|
||||
I18n.backend.store_translations 'da', 'da' => {}
|
||||
I18n.backend.store_translations 'pt-BR', 'pt-BR' => {}
|
||||
I18n.backend.store_translations 'da', {}
|
||||
I18n.backend.store_translations '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
|
||||
|
||||
@@ -8,7 +8,7 @@ require 'active_model'
|
||||
require 'active_model/state_machine'
|
||||
|
||||
$:.unshift File.expand_path('../../../activesupport/lib', __FILE__)
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
require 'active_support/test_case'
|
||||
|
||||
class ActiveModel::TestCase < ActiveSupport::TestCase
|
||||
|
||||
@@ -22,12 +22,12 @@
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue LoadError
|
||||
activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
|
||||
if File.directory?(activesupport_path)
|
||||
$:.unshift activesupport_path
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -56,6 +56,7 @@ module ActiveRecord
|
||||
autoload :DynamicScopeMatch, 'active_record/dynamic_scope_match'
|
||||
autoload :Migration, 'active_record/migration'
|
||||
autoload :Migrator, 'active_record/migration'
|
||||
autoload :ModelName, 'active_record/model_name'
|
||||
autoload :NamedScope, 'active_record/named_scope'
|
||||
autoload :NestedAttributes, 'active_record/nested_attributes'
|
||||
autoload :Observing, 'active_record/observer'
|
||||
|
||||
@@ -2487,6 +2487,12 @@ module ActiveRecord #:nodoc:
|
||||
result
|
||||
end
|
||||
|
||||
# Returns an ActiveRecord::ModelName object for module. It can be
|
||||
# used to retrieve all kinds of naming-related information.
|
||||
def model_name
|
||||
@model_name ||= ::ActiveRecord::ModelName.new(name)
|
||||
end
|
||||
|
||||
# A model instance's primary key is always available as model.id
|
||||
# whether you name it the default 'id' or set it to something else.
|
||||
def id
|
||||
|
||||
@@ -195,9 +195,7 @@ module ActiveRecord
|
||||
def log_info(sql, name, ms)
|
||||
if @logger && @logger.debug?
|
||||
name = '%s (%.1fms)' % [name || 'SQL', ms]
|
||||
if sql.respond_to?(:force_encoding)
|
||||
sql = sql.dup.force_encoding 'binary'
|
||||
end
|
||||
sql.force_encoding 'binary' if sql.respond_to?(:force_encoding)
|
||||
@logger.debug(format_log_entry(name, sql.squeeze(' ')))
|
||||
end
|
||||
end
|
||||
@@ -214,7 +212,13 @@ module ActiveRecord
|
||||
log_info(sql, name, 0)
|
||||
nil
|
||||
end
|
||||
rescue => e
|
||||
rescue SystemExit, SignalException, NoMemoryError => e
|
||||
# Don't re-wrap these exceptions. They are probably not being caused by invalid
|
||||
# sql, but rather some external stimulus beyond the responsibilty of this code.
|
||||
# Additionaly, wrapping these exceptions with StatementInvalid would lead to
|
||||
# meaningful loss of data, such as losing SystemExit#status.
|
||||
raise e
|
||||
rescue Exception => e
|
||||
# Log message and raise exception.
|
||||
# Set last_verification to 0, so that connection gets verified
|
||||
# upon reentering the request loop
|
||||
|
||||
25
activerecord/lib/active_record/model_name.rb
Normal file
25
activerecord/lib/active_record/model_name.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
module ActiveRecord
|
||||
class ModelName < String
|
||||
alias_method :cache_key, :collection
|
||||
|
||||
def singular
|
||||
@singular ||= ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
|
||||
end
|
||||
|
||||
def plural
|
||||
@plural ||= ActiveSupport::Inflector.pluralize(singular).freeze
|
||||
end
|
||||
|
||||
def element
|
||||
@element ||= ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze
|
||||
end
|
||||
|
||||
def collection
|
||||
@collection ||= ActiveSupport::Inflector.tableize(self).freeze
|
||||
end
|
||||
|
||||
def partial_path
|
||||
@partial_path ||= "#{collection}/#{element}".freeze
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,4 @@
|
||||
require 'active_support/json'
|
||||
require 'active_support/core_ext/module/model_naming'
|
||||
|
||||
module ActiveRecord #:nodoc:
|
||||
module Serialization
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../lib')
|
||||
$:.unshift(File.dirname(__FILE__) + '/../../../activesupport/lib')
|
||||
begin
|
||||
old, $VERBOSE = $VERBOSE, nil
|
||||
require File.expand_path('../../../load_paths', __FILE__)
|
||||
ensure
|
||||
$VERBOSE = old
|
||||
end
|
||||
|
||||
require 'config'
|
||||
|
||||
|
||||
@@ -11,4 +11,7 @@ Gem::Specification.new do |s|
|
||||
s.homepage = 'http://www.rubyonrails.org'
|
||||
|
||||
s.require_path = 'lib'
|
||||
|
||||
s.add_dependency('i18n', '~> 0.6', '>= 0.6.4')
|
||||
s.add_dependency('multi_json', '~> 1.0')
|
||||
end
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
begin
|
||||
$:.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
|
||||
require 'active_support'
|
||||
require 'active_support/all'
|
||||
rescue IOError
|
||||
end
|
||||
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
Dir.chdir(File.expand_path("..", __FILE__))
|
||||
$: << File.expand_path("../lib", __FILE__)
|
||||
require "active_support"
|
||||
|
||||
ActiveSupport::TimeZone.all
|
||||
|
||||
def flatten_constants(mod, ary = [])
|
||||
ary << mod
|
||||
mod.constants.each do |const|
|
||||
flatten_constants(mod.const_get(const), ary)
|
||||
end
|
||||
ary
|
||||
end
|
||||
|
||||
defns = flatten_constants(TZInfo::Definitions).select { |mod|
|
||||
defined?(mod.get)
|
||||
}.map { |tz|
|
||||
tz.get
|
||||
}
|
||||
|
||||
file = "lib/active_support/vendor/tzinfo-0.3.12/tzinfo/definitions.dump"
|
||||
data = Marshal.dump(defns)
|
||||
Marshal.load(data)
|
||||
File.open(file, "wb") do |f|
|
||||
require "pry"
|
||||
pry binding
|
||||
f.write(data)
|
||||
end
|
||||
puts "Wrote #{data.size} bytes to #{file}"
|
||||
@@ -1,5 +1,5 @@
|
||||
#--
|
||||
# Copyright (c) 2005 David Heinemeier Hansson
|
||||
# Copyright (c) 2005-2011 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
@@ -21,40 +21,62 @@
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
module ActiveSupport
|
||||
def self.load_all!
|
||||
[Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte, SecureRandom, TimeWithZone]
|
||||
end
|
||||
require 'securerandom'
|
||||
|
||||
autoload :BacktraceCleaner, 'active_support/backtrace_cleaner'
|
||||
autoload :Base64, 'active_support/base64'
|
||||
autoload :BasicObject, 'active_support/basic_object'
|
||||
autoload :BufferedLogger, 'active_support/buffered_logger'
|
||||
autoload :Cache, 'active_support/cache'
|
||||
autoload :Callbacks, 'active_support/callbacks'
|
||||
autoload :Deprecation, 'active_support/deprecation'
|
||||
autoload :Duration, 'active_support/duration'
|
||||
autoload :Gzip, 'active_support/gzip'
|
||||
autoload :Inflector, 'active_support/inflector'
|
||||
autoload :Memoizable, 'active_support/memoizable'
|
||||
autoload :MessageEncryptor, 'active_support/message_encryptor'
|
||||
autoload :MessageVerifier, 'active_support/message_verifier'
|
||||
autoload :Multibyte, 'active_support/multibyte'
|
||||
autoload :OptionMerger, 'active_support/option_merger'
|
||||
autoload :OrderedHash, 'active_support/ordered_hash'
|
||||
autoload :OrderedOptions, 'active_support/ordered_options'
|
||||
autoload :Rescuable, 'active_support/rescuable'
|
||||
autoload :SafeBuffer, 'active_support/core_ext/string/output_safety'
|
||||
autoload :SecureRandom, 'active_support/secure_random'
|
||||
autoload :StringInquirer, 'active_support/string_inquirer'
|
||||
autoload :TimeWithZone, 'active_support/time_with_zone'
|
||||
autoload :TimeZone, 'active_support/values/time_zone'
|
||||
autoload :XmlMini, 'active_support/xml_mini'
|
||||
module ActiveSupport
|
||||
class << self
|
||||
attr_accessor :load_all_hooks
|
||||
def on_load_all(&hook) load_all_hooks << hook end
|
||||
def load_all!; load_all_hooks.each { |hook| hook.call } end
|
||||
end
|
||||
self.load_all_hooks = []
|
||||
|
||||
on_load_all do
|
||||
[Dependencies, Deprecation, Gzip, MessageVerifier, Multibyte]
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_support/vendor'
|
||||
require 'active_support/core_ext'
|
||||
require 'active_support/dependencies'
|
||||
require 'active_support/json'
|
||||
require "active_support/dependencies/autoload"
|
||||
require "active_support/version"
|
||||
|
||||
I18n.load_path << "#{File.dirname(__FILE__)}/active_support/locale/en.yml"
|
||||
module ActiveSupport
|
||||
extend ActiveSupport::Autoload
|
||||
|
||||
autoload :DescendantsTracker
|
||||
autoload :FileUpdateChecker
|
||||
autoload :LogSubscriber
|
||||
autoload :Notifications
|
||||
|
||||
# TODO: Narrow this list down
|
||||
eager_autoload do
|
||||
autoload :BacktraceCleaner
|
||||
autoload :Base64
|
||||
autoload :BasicObject
|
||||
autoload :Benchmarkable
|
||||
autoload :BufferedLogger
|
||||
autoload :Cache
|
||||
autoload :Callbacks
|
||||
autoload :Concern
|
||||
autoload :Configurable
|
||||
autoload :Deprecation
|
||||
autoload :Gzip
|
||||
autoload :Inflector
|
||||
autoload :JSON
|
||||
autoload :Memoizable
|
||||
autoload :MessageEncryptor
|
||||
autoload :MessageVerifier
|
||||
autoload :Multibyte
|
||||
autoload :OptionMerger
|
||||
autoload :OrderedHash
|
||||
autoload :OrderedOptions
|
||||
autoload :Rescuable
|
||||
autoload :StringInquirer
|
||||
autoload :TaggedLogging
|
||||
autoload :XmlMini
|
||||
end
|
||||
|
||||
autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
|
||||
autoload :TestCase
|
||||
end
|
||||
|
||||
autoload :I18n, "active_support/i18n"
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
# For forward compatibility with Rails 3.
|
||||
#
|
||||
# require 'active_support' loads a very bare minumum in Rails 3.
|
||||
# require 'active_support/all' loads the whole suite like Rails 2 did.
|
||||
#
|
||||
# To prepare for Rails 3, switch to require 'active_support/all' now.
|
||||
|
||||
require 'active_support'
|
||||
require 'active_support/time'
|
||||
require 'active_support/core_ext'
|
||||
|
||||
@@ -1,27 +1,43 @@
|
||||
module ActiveSupport
|
||||
# Many backtraces include too much information that's not relevant for the context. This makes it hard to find the signal
|
||||
# in the backtrace and adds debugging time. With a BacktraceCleaner, you can setup filters and silencers for your particular
|
||||
# context, so only the relevant lines are included.
|
||||
# Backtraces often include many lines that are not relevant for the context under review. This makes it hard to find the
|
||||
# signal amongst the backtrace noise, and adds debugging time. With a BacktraceCleaner, filters and silencers are used to
|
||||
# remove the noisy lines, so that only the most relevant lines remain.
|
||||
#
|
||||
# If you need to reconfigure an existing BacktraceCleaner, like the one in Rails, to show as much as possible, you can always
|
||||
# call BacktraceCleaner#remove_silencers!
|
||||
# Filters are used to modify lines of data, while silencers are used to remove lines entirely. The typical filter use case
|
||||
# is to remove lengthy path information from the start of each line, and view file paths relevant to the app directory
|
||||
# instead of the file system root. The typical silencer use case is to exclude the output of a noisy library from the
|
||||
# backtrace, so that you can focus on the rest.
|
||||
#
|
||||
# Example:
|
||||
# ==== Example:
|
||||
#
|
||||
# bc = BacktraceCleaner.new
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
|
||||
# bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
|
||||
#
|
||||
# To reconfigure an existing BacktraceCleaner (like the default one in Rails) and show as much data as possible, you can
|
||||
# always call <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the backtrace to a pristine state. If you
|
||||
# need to reconfigure an existing BacktraceCleaner so that it does not filter or modify the paths of any lines of the
|
||||
# backtrace, you can call BacktraceCleaner#remove_filters! These two methods will give you a completely untouched backtrace.
|
||||
#
|
||||
# Inspired by the Quiet Backtrace gem by Thoughtbot.
|
||||
class BacktraceCleaner
|
||||
def initialize
|
||||
@filters, @silencers = [], []
|
||||
end
|
||||
|
||||
# Returns the backtrace after all filters and silencers has been run against it. Filters run first, then silencers.
|
||||
def clean(backtrace)
|
||||
silence(filter(backtrace))
|
||||
|
||||
# Returns the backtrace after all filters and silencers have been run against it. Filters run first, then silencers.
|
||||
def clean(backtrace, kind = :silent)
|
||||
filtered = filter(backtrace)
|
||||
|
||||
case kind
|
||||
when :silent
|
||||
silence(filtered)
|
||||
when :noise
|
||||
noise(filtered)
|
||||
else
|
||||
filtered
|
||||
end
|
||||
end
|
||||
|
||||
# Adds a filter from the block provided. Each line in the backtrace will be mapped against this filter.
|
||||
@@ -34,8 +50,8 @@ module ActiveSupport
|
||||
@filters << block
|
||||
end
|
||||
|
||||
# Adds a silencer from the block provided. If the silencer returns true for a given line, it'll be excluded from the
|
||||
# clean backtrace.
|
||||
# Adds a silencer from the block provided. If the silencer returns true for a given line, it will be excluded from
|
||||
# the clean backtrace.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
@@ -46,26 +62,37 @@ module ActiveSupport
|
||||
end
|
||||
|
||||
# Will remove all silencers, but leave in the filters. This is useful if your context of debugging suddenly expands as
|
||||
# you suspect a bug in the libraries you use.
|
||||
# you suspect a bug in one of the libraries you use.
|
||||
def remove_silencers!
|
||||
@silencers = []
|
||||
end
|
||||
|
||||
|
||||
def remove_filters!
|
||||
@filters = []
|
||||
end
|
||||
|
||||
private
|
||||
def filter(backtrace)
|
||||
@filters.each do |f|
|
||||
backtrace = backtrace.map { |line| f.call(line) }
|
||||
end
|
||||
|
||||
|
||||
backtrace
|
||||
end
|
||||
|
||||
|
||||
def silence(backtrace)
|
||||
@silencers.each do |s|
|
||||
backtrace = backtrace.reject { |line| s.call(line) }
|
||||
end
|
||||
|
||||
|
||||
backtrace
|
||||
end
|
||||
|
||||
def noise(backtrace)
|
||||
@silencers.each do |s|
|
||||
backtrace = backtrace.select { |line| s.call(line) }
|
||||
end
|
||||
|
||||
backtrace
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,33 +1,54 @@
|
||||
require 'active_support/deprecation'
|
||||
|
||||
begin
|
||||
require 'base64'
|
||||
rescue LoadError
|
||||
end
|
||||
# The Base64 module isn't available in earlier versions of Ruby 1.9.
|
||||
module Base64
|
||||
# Encodes a string to its base 64 representation. Each 60 characters of
|
||||
# output is separated by a newline character.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==\n"
|
||||
def self.encode64(data)
|
||||
[data].pack("m")
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
if defined? ::Base64
|
||||
Base64 = ::Base64
|
||||
else
|
||||
# Base64 provides utility methods for encoding and de-coding binary data
|
||||
# using a base 64 representation. A base 64 representation of binary data
|
||||
# consists entirely of printable US-ASCII characters. The Base64 module
|
||||
# is included in Ruby 1.8, but has been removed in Ruby 1.9.
|
||||
module Base64
|
||||
# Encodes a string to its base 64 representation. Each 60 characters of
|
||||
# output is separated by a newline character.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==\n"
|
||||
def self.encode64(data)
|
||||
[data].pack("m")
|
||||
end
|
||||
|
||||
# Decodes a base 64 encoded string to its original representation.
|
||||
#
|
||||
# ActiveSupport::Base64.decode64("T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==")
|
||||
# # => "Original unencoded string"
|
||||
def self.decode64(data)
|
||||
data.unpack("m").first
|
||||
end
|
||||
# Decodes a base 64 encoded string to its original representation.
|
||||
#
|
||||
# ActiveSupport::Base64.decode64("T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw==")
|
||||
# # => "Original unencoded string"
|
||||
def self.decode64(data)
|
||||
data.unpack("m").first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unless Base64.respond_to?(:strict_encode64)
|
||||
# Included in Ruby 1.9
|
||||
def Base64.strict_encode64(value)
|
||||
encode64(value).gsub(/\n/, '')
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveSupport
|
||||
module Base64
|
||||
def self.encode64(value)
|
||||
ActiveSupport::Deprecation.warn "ActiveSupport::Base64.encode64 " \
|
||||
"is deprecated. Use Base64.encode64 instead", caller
|
||||
::Base64.encode64(value)
|
||||
end
|
||||
|
||||
def self.decode64(value)
|
||||
ActiveSupport::Deprecation.warn "ActiveSupport::Base64.decode64 " \
|
||||
"is deprecated. Use Base64.decode64 instead", caller
|
||||
::Base64.decode64(value)
|
||||
end
|
||||
|
||||
def self.encode64s(value)
|
||||
ActiveSupport::Deprecation.warn "ActiveSupport::Base64.encode64s " \
|
||||
"is deprecated. Use Base64.strict_encode64 instead", caller
|
||||
::Base64.strict_encode64(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
55
activesupport/lib/active_support/benchmarkable.rb
Normal file
55
activesupport/lib/active_support/benchmarkable.rb
Normal file
@@ -0,0 +1,55 @@
|
||||
require 'active_support/core_ext/benchmark'
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
|
||||
module ActiveSupport
|
||||
module Benchmarkable
|
||||
# Allows you to measure the execution time of a block in a template and records the result to
|
||||
# the log. Wrap this block around expensive operations or possible bottlenecks to get a time
|
||||
# reading for the operation. For example, let's say you thought your file processing method
|
||||
# was taking too long; you could wrap it in a benchmark block.
|
||||
#
|
||||
# <% benchmark "Process data files" do %>
|
||||
# <%= expensive_files_operation %>
|
||||
# <% end %>
|
||||
#
|
||||
# That would add something like "Process data files (345.2ms)" to the log, which you can then
|
||||
# use to compare timings when optimizing your code.
|
||||
#
|
||||
# You may give an optional logger level (:debug, :info, :warn, :error) as the :level option.
|
||||
# The default logger level value is :info.
|
||||
#
|
||||
# <% benchmark "Low-level files", :level => :debug do %>
|
||||
# <%= lowlevel_files_operation %>
|
||||
# <% end %>
|
||||
#
|
||||
# Finally, you can pass true as the third argument to silence all log activity (other than the
|
||||
# timing information) from inside the block. This is great for boiling down a noisy block to
|
||||
# just a single statement that produces one log line:
|
||||
#
|
||||
# <% benchmark "Process data files", :level => :info, :silence => true do %>
|
||||
# <%= expensive_and_chatty_files_operation %>
|
||||
# <% end %>
|
||||
def benchmark(message = "Benchmarking", options = {})
|
||||
if logger
|
||||
options.assert_valid_keys(:level, :silence)
|
||||
options[:level] ||= :info
|
||||
|
||||
result = nil
|
||||
ms = Benchmark.ms { result = options[:silence] ? silence { yield } : yield }
|
||||
logger.send(options[:level], '%s (%.1fms)' % [ message, ms ])
|
||||
result
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Silence the logger during the execution of the block.
|
||||
#
|
||||
def silence
|
||||
old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger
|
||||
yield
|
||||
ensure
|
||||
logger.level = old_logger_level if logger
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,9 @@
|
||||
require 'thread'
|
||||
require 'logger'
|
||||
require 'active_support/core_ext/logger'
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/deprecation'
|
||||
require 'fileutils'
|
||||
|
||||
module ActiveSupport
|
||||
# Inspired by the buffered logger idea by Ezra
|
||||
@@ -25,62 +30,69 @@ module ActiveSupport
|
||||
def silence(temporary_level = ERROR)
|
||||
if silencer
|
||||
begin
|
||||
old_logger_level, self.level = level, temporary_level
|
||||
yield self
|
||||
logger = self.class.new @log_dest.dup, temporary_level
|
||||
yield logger
|
||||
ensure
|
||||
self.level = old_logger_level
|
||||
logger.close
|
||||
end
|
||||
else
|
||||
yield self
|
||||
end
|
||||
end
|
||||
deprecate :silence
|
||||
|
||||
attr_accessor :level
|
||||
attr_reader :auto_flushing
|
||||
deprecate :auto_flushing
|
||||
|
||||
def initialize(log, level = DEBUG)
|
||||
@level = level
|
||||
@buffer = {}
|
||||
@auto_flushing = 1
|
||||
@guard = Mutex.new
|
||||
@log_dest = log
|
||||
|
||||
if log.respond_to?(:write)
|
||||
@log = log
|
||||
elsif File.exist?(log)
|
||||
@log = open(log, (File::WRONLY | File::APPEND))
|
||||
@log.sync = true
|
||||
else
|
||||
FileUtils.mkdir_p(File.dirname(log))
|
||||
@log = open(log, (File::WRONLY | File::APPEND | File::CREAT))
|
||||
@log.sync = true
|
||||
@log.write("# Logfile created on %s" % [Time.now.to_s])
|
||||
unless log.respond_to?(:write)
|
||||
unless File.exist?(File.dirname(log))
|
||||
ActiveSupport::Deprecation.warn(<<-eowarn)
|
||||
Automatic directory creation for '#{log}' is deprecated. Please make sure the directory for your log file exists before creating the logger.
|
||||
eowarn
|
||||
FileUtils.mkdir_p(File.dirname(log))
|
||||
end
|
||||
end
|
||||
|
||||
@log = open_logfile log
|
||||
self.level = level
|
||||
end
|
||||
|
||||
def open_log(log, mode)
|
||||
open(log, mode).tap do |open_log|
|
||||
open_log.set_encoding(Encoding::BINARY) if open_log.respond_to?(:set_encoding)
|
||||
open_log.sync = true
|
||||
end
|
||||
end
|
||||
deprecate :open_log
|
||||
|
||||
def level
|
||||
@log.level
|
||||
end
|
||||
|
||||
def level=(l)
|
||||
@log.level = l
|
||||
end
|
||||
|
||||
def add(severity, message = nil, progname = nil, &block)
|
||||
return if @level > severity
|
||||
message = (message || (block && block.call) || progname).to_s
|
||||
# If a newline is necessary then create a new message ending with a newline.
|
||||
# Ensures that the original message is not mutated.
|
||||
message = "#{message}\n" unless message[-1] == ?\n
|
||||
if message.respond_to?(:force_encoding)
|
||||
buffer << message.force_encoding(Encoding.default_external)
|
||||
else
|
||||
buffer << message
|
||||
end
|
||||
auto_flush
|
||||
message
|
||||
@log.add(severity, message, progname, &block)
|
||||
end
|
||||
|
||||
for severity in Severity.constants
|
||||
# Dynamically add methods such as:
|
||||
# def info
|
||||
# def warn
|
||||
# def debug
|
||||
Severity.constants.each do |severity|
|
||||
class_eval <<-EOT, __FILE__, __LINE__ + 1
|
||||
def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
|
||||
add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block)
|
||||
end # end
|
||||
#
|
||||
def #{severity.downcase}? # def debug?
|
||||
#{severity} >= @level # DEBUG >= @level
|
||||
end # end
|
||||
def #{severity.downcase}(message = nil, progname = nil, &block) # def debug(message = nil, progname = nil, &block)
|
||||
add(#{severity}, message, progname, &block) # add(DEBUG, message, progname, &block)
|
||||
end # end
|
||||
|
||||
def #{severity.downcase}? # def debug?
|
||||
#{severity} >= level # DEBUG >= level
|
||||
end # end
|
||||
EOT
|
||||
end
|
||||
|
||||
@@ -89,45 +101,25 @@ module ActiveSupport
|
||||
# never auto-flush. If you turn auto-flushing off, be sure to regularly
|
||||
# flush the log yourself -- it will eat up memory until you do.
|
||||
def auto_flushing=(period)
|
||||
@auto_flushing =
|
||||
case period
|
||||
when true; 1
|
||||
when false, nil, 0; MAX_BUFFER_SIZE
|
||||
when Integer; period
|
||||
else raise ArgumentError, "Unrecognized auto_flushing period: #{period.inspect}"
|
||||
end
|
||||
end
|
||||
deprecate :auto_flushing=
|
||||
|
||||
def flush
|
||||
@guard.synchronize do
|
||||
unless buffer.empty?
|
||||
old_buffer = buffer
|
||||
@log.write(old_buffer.join)
|
||||
end
|
||||
end
|
||||
deprecate :flush
|
||||
|
||||
# Important to do this even if buffer was empty or else @buffer will
|
||||
# accumulate empty arrays for each request where nothing was logged.
|
||||
clear_buffer
|
||||
end
|
||||
def respond_to?(method, include_private = false)
|
||||
return false if method.to_s == "flush"
|
||||
super
|
||||
end
|
||||
|
||||
def close
|
||||
flush
|
||||
@log.close if @log.respond_to?(:close)
|
||||
@log = nil
|
||||
@log.close
|
||||
end
|
||||
|
||||
protected
|
||||
def auto_flush
|
||||
flush if buffer.size >= @auto_flushing
|
||||
end
|
||||
|
||||
def buffer
|
||||
@buffer[Thread.current] ||= []
|
||||
end
|
||||
|
||||
def clear_buffer
|
||||
@buffer.delete(Thread.current)
|
||||
end
|
||||
private
|
||||
def open_logfile(log)
|
||||
Logger.new log
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
6
activesupport/lib/active_support/builder.rb
Normal file
6
activesupport/lib/active_support/builder.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
begin
|
||||
require 'builder'
|
||||
rescue LoadError => e
|
||||
$stderr.puts "You don't have builder installed in your application. Please add it to your Gemfile and run bundle install"
|
||||
raise e
|
||||
end
|
||||
@@ -1,76 +1,99 @@
|
||||
require 'benchmark'
|
||||
require 'zlib'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/benchmark'
|
||||
require 'active_support/core_ext/exception'
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
require 'active_support/core_ext/numeric/bytes'
|
||||
require 'active_support/core_ext/numeric/time'
|
||||
require 'active_support/core_ext/object/to_param'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
module ActiveSupport
|
||||
# See ActiveSupport::Cache::Store for documentation.
|
||||
module Cache
|
||||
autoload :FileStore, 'active_support/cache/file_store'
|
||||
autoload :MemoryStore, 'active_support/cache/memory_store'
|
||||
autoload :SynchronizedMemoryStore, 'active_support/cache/synchronized_memory_store'
|
||||
autoload :DRbStore, 'active_support/cache/drb_store'
|
||||
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
|
||||
autoload :CompressedMemCacheStore, 'active_support/cache/compressed_mem_cache_store'
|
||||
autoload :NullStore, 'active_support/cache/null_store'
|
||||
|
||||
# These options mean something to all cache implementations. Individual cache
|
||||
# implementations may support additional options.
|
||||
UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
|
||||
|
||||
module Strategy
|
||||
autoload :LocalCache, 'active_support/cache/strategy/local_cache'
|
||||
end
|
||||
|
||||
# Creates a new CacheStore object according to the given options.
|
||||
#
|
||||
# If no arguments are passed to this method, then a new
|
||||
# ActiveSupport::Cache::MemoryStore object will be returned.
|
||||
#
|
||||
# If you pass a Symbol as the first argument, then a corresponding cache
|
||||
# store class under the ActiveSupport::Cache namespace will be created.
|
||||
# For example:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemoryStore object
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:drb_store)
|
||||
# # => returns a new ActiveSupport::Cache::DRbStore object
|
||||
#
|
||||
# Any additional arguments will be passed to the corresponding cache store
|
||||
# class's constructor:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:file_store, "/tmp/cache")
|
||||
# # => same as: ActiveSupport::Cache::FileStore.new("/tmp/cache")
|
||||
#
|
||||
# If the first argument is not a Symbol, then it will simply be returned:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
|
||||
# # => returns MyOwnCacheStore.new
|
||||
def self.lookup_store(*store_option)
|
||||
store, *parameters = *([ store_option ].flatten)
|
||||
class << self
|
||||
# Creates a new CacheStore object according to the given options.
|
||||
#
|
||||
# If no arguments are passed to this method, then a new
|
||||
# ActiveSupport::Cache::MemoryStore object will be returned.
|
||||
#
|
||||
# If you pass a Symbol as the first argument, then a corresponding cache
|
||||
# store class under the ActiveSupport::Cache namespace will be created.
|
||||
# For example:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:memory_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemoryStore object
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:mem_cache_store)
|
||||
# # => returns a new ActiveSupport::Cache::MemCacheStore object
|
||||
#
|
||||
# Any additional arguments will be passed to the corresponding cache store
|
||||
# class's constructor:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(:file_store, "/tmp/cache")
|
||||
# # => same as: ActiveSupport::Cache::FileStore.new("/tmp/cache")
|
||||
#
|
||||
# If the first argument is not a Symbol, then it will simply be returned:
|
||||
#
|
||||
# ActiveSupport::Cache.lookup_store(MyOwnCacheStore.new)
|
||||
# # => returns MyOwnCacheStore.new
|
||||
def lookup_store(*store_option)
|
||||
store, *parameters = *Array.wrap(store_option).flatten
|
||||
|
||||
case store
|
||||
when Symbol
|
||||
store_class_name = (store == :drb_store ? "DRbStore" : store.to_s.camelize)
|
||||
store_class = ActiveSupport::Cache.const_get(store_class_name)
|
||||
store_class.new(*parameters)
|
||||
when nil
|
||||
ActiveSupport::Cache::MemoryStore.new
|
||||
else
|
||||
store
|
||||
end
|
||||
end
|
||||
|
||||
def self.expand_cache_key(key, namespace = nil)
|
||||
expanded_cache_key = namespace ? "#{namespace}/" : ""
|
||||
|
||||
if ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
|
||||
expanded_cache_key << "#{ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]}/"
|
||||
case store
|
||||
when Symbol
|
||||
store_class_name = store.to_s.camelize
|
||||
store_class =
|
||||
begin
|
||||
require "active_support/cache/#{store}"
|
||||
rescue LoadError => e
|
||||
raise "Could not find cache store adapter for #{store} (#{e})"
|
||||
else
|
||||
ActiveSupport::Cache.const_get(store_class_name)
|
||||
end
|
||||
store_class.new(*parameters)
|
||||
when nil
|
||||
ActiveSupport::Cache::MemoryStore.new
|
||||
else
|
||||
store
|
||||
end
|
||||
end
|
||||
|
||||
expanded_cache_key << case
|
||||
when key.respond_to?(:cache_key)
|
||||
key.cache_key
|
||||
when key.is_a?(Array)
|
||||
key.collect { |element| expand_cache_key(element) }.to_param
|
||||
when key
|
||||
key.to_param
|
||||
def expand_cache_key(key, namespace = nil)
|
||||
expanded_cache_key = namespace ? "#{namespace}/" : ""
|
||||
|
||||
if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
|
||||
expanded_cache_key << "#{prefix}/"
|
||||
end
|
||||
|
||||
expanded_cache_key << retrieve_cache_key(key)
|
||||
expanded_cache_key
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def retrieve_cache_key(key)
|
||||
case
|
||||
when key.respond_to?(:cache_key) then key.cache_key
|
||||
when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
|
||||
else key.to_param
|
||||
end.to_s
|
||||
|
||||
expanded_cache_key
|
||||
end
|
||||
end
|
||||
|
||||
# An abstract cache store class. There are multiple cache store
|
||||
@@ -79,28 +102,64 @@ module ActiveSupport
|
||||
# ActiveSupport::Cache::MemCacheStore. MemCacheStore is currently the most
|
||||
# popular cache store for large production websites.
|
||||
#
|
||||
# ActiveSupport::Cache::Store is meant for caching strings. Some cache
|
||||
# store implementations, like MemoryStore, are able to cache arbitrary
|
||||
# Ruby objects, but don't count on every cache store to be able to do that.
|
||||
# Some implementations may not support all methods beyond the basic cache
|
||||
# methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
|
||||
#
|
||||
# ActiveSupport::Cache::Store can store any serializable Ruby object.
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new
|
||||
#
|
||||
#
|
||||
# cache.read("city") # => nil
|
||||
# cache.write("city", "Duckburgh")
|
||||
# cache.read("city") # => "Duckburgh"
|
||||
#
|
||||
# Keys are always translated into Strings and are case sensitive. When an
|
||||
# object is specified as a key and has a +cache_key+ method defined, this
|
||||
# method will be called to define the key. Otherwise, the +to_param+
|
||||
# method will be called. Hashes and Arrays can also be used as keys. The
|
||||
# elements will be delimited by slashes, and the elements within a Hash
|
||||
# will be sorted by key so they are consistent.
|
||||
#
|
||||
# cache.read("city") == cache.read(:city) # => true
|
||||
#
|
||||
# Nil values can be cached.
|
||||
#
|
||||
# If your cache is on a shared infrastructure, you can define a namespace
|
||||
# for your cache entries. If a namespace is defined, it will be prefixed on
|
||||
# to every key. The namespace can be either a static value or a Proc. If it
|
||||
# is a Proc, it will be invoked when each key is evaluated so that you can
|
||||
# use application logic to invalidate keys.
|
||||
#
|
||||
# cache.namespace = lambda { @last_mod_time } # Set the namespace to a variable
|
||||
# @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
|
||||
#
|
||||
#
|
||||
# Caches can also store values in a compressed format to save space and
|
||||
# reduce time spent sending data. Since there is overhead, values must be
|
||||
# large enough to warrant compression. To turn on compression either pass
|
||||
# <tt>:compress => true</tt> in the initializer or as an option to +fetch+
|
||||
# or +write+. To specify the threshold at which to compress values, set the
|
||||
# <tt>:compress_threshold</tt> option. The default threshold is 16K.
|
||||
class Store
|
||||
cattr_accessor :logger
|
||||
|
||||
attr_reader :silence, :logger_off
|
||||
cattr_accessor :logger, :instance_writer => true
|
||||
|
||||
attr_reader :silence, :options
|
||||
alias :silence? :silence
|
||||
|
||||
# Create a new cache. The options will be passed to any write method calls except
|
||||
# for :namespace which can be used to set the global namespace for the cache.
|
||||
def initialize(options = nil)
|
||||
@options = options ? options.dup : {}
|
||||
end
|
||||
|
||||
# Silence the logger.
|
||||
def silence!
|
||||
@silence = true
|
||||
self
|
||||
end
|
||||
|
||||
alias silence? silence
|
||||
alias logger_off? logger_off
|
||||
|
||||
# Silence the logger within a block.
|
||||
def mute
|
||||
previous_silence, @silence = defined?(@silence) && @silence, true
|
||||
yield
|
||||
@@ -108,18 +167,27 @@ module ActiveSupport
|
||||
@silence = previous_silence
|
||||
end
|
||||
|
||||
# Set to true if cache stores should be instrumented. Default is false.
|
||||
def self.instrument=(boolean)
|
||||
Thread.current[:instrument_cache_store] = boolean
|
||||
end
|
||||
|
||||
def self.instrument
|
||||
Thread.current[:instrument_cache_store] || false
|
||||
end
|
||||
|
||||
# Fetches data from the cache, using the given key. If there is data in
|
||||
# the cache with the given key, then that data is returned.
|
||||
#
|
||||
# If there is no such data in the cache (a cache miss occurred), then
|
||||
# then nil will be returned. However, if a block has been passed, then
|
||||
# that block will be run in the event of a cache miss. The return value
|
||||
# of the block will be written to the cache under the given cache key,
|
||||
# and that return value will be returned.
|
||||
# If there is no such data in the cache (a cache miss), then nil will be
|
||||
# returned. However, if a block has been passed, that block will be run
|
||||
# in the event of a cache miss. The return value of the block will be
|
||||
# written to the cache under the given cache key, and that return value
|
||||
# will be returned.
|
||||
#
|
||||
# cache.write("today", "Monday")
|
||||
# cache.fetch("today") # => "Monday"
|
||||
#
|
||||
#
|
||||
# cache.fetch("city") # => nil
|
||||
# cache.fetch("city") do
|
||||
# "Duckburgh"
|
||||
@@ -132,42 +200,107 @@ module ActiveSupport
|
||||
# cache.write("today", "Monday")
|
||||
# cache.fetch("today", :force => true) # => nil
|
||||
#
|
||||
# Setting <tt>:compress</tt> will store a large cache entry set by the call
|
||||
# in a compressed format.
|
||||
#
|
||||
#
|
||||
# Setting <tt>:expires_in</tt> will set an expiration time on the cache.
|
||||
# All caches support auto-expiring content after a specified number of
|
||||
# seconds. This value can be specified as an option to the constructor
|
||||
# (in which case all entries will be affected), or it can be supplied to
|
||||
# the +fetch+ or +write+ method to effect just one entry.
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 5.minutes)
|
||||
# cache.write(key, value, :expires_in => 1.minute) # Set a lower value for one entry
|
||||
#
|
||||
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where a cache entry
|
||||
# is used very frequently and is under heavy load. If a cache expires and due to heavy load
|
||||
# seven different processes will try to read data natively and then they all will try to
|
||||
# write to cache. To avoid that case the first process to find an expired cache entry will
|
||||
# bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. Yes
|
||||
# this process is extending the time for a stale value by another few seconds. Because
|
||||
# of extended life of the previous cache, other processes will continue to use slightly
|
||||
# stale data for a just a big longer. In the meantime that first process will go ahead
|
||||
# and will write into cache the new value. After that all the processes will start
|
||||
# getting new value. The key is to keep <tt>:race_condition_ttl</tt> small.
|
||||
#
|
||||
# If the process regenerating the entry errors out, the entry will be regenerated
|
||||
# after the specified number of seconds. Also note that the life of stale cache is
|
||||
# extended only if it expired recently. Otherwise a new value is generated and
|
||||
# <tt>:race_condition_ttl</tt> does not play any role.
|
||||
#
|
||||
# # Set all values to expire after one minute.
|
||||
# cache = ActiveSupport::Cache::MemoryStore.new(:expires_in => 1.minute)
|
||||
#
|
||||
# cache.write("foo", "original value")
|
||||
# val_1 = nil
|
||||
# val_2 = nil
|
||||
# sleep 60
|
||||
#
|
||||
# Thread.new do
|
||||
# val_1 = cache.fetch("foo", :race_condition_ttl => 10) do
|
||||
# sleep 1
|
||||
# "new value 1"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Thread.new do
|
||||
# val_2 = cache.fetch("foo", :race_condition_ttl => 10) do
|
||||
# "new value 2"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # val_1 => "new value 1"
|
||||
# # val_2 => "original value"
|
||||
# # sleep 10 # First thread extend the life of cache by another 10 seconds
|
||||
# # cache.fetch("foo") => "new value 1"
|
||||
#
|
||||
# Other options will be handled by the specific cache store implementation.
|
||||
# Internally, #fetch calls #read, and calls #write on a cache miss.
|
||||
# Internally, #fetch calls #read_entry, and calls #write_entry on a cache miss.
|
||||
# +options+ will be passed to the #read and #write calls.
|
||||
#
|
||||
# For example, MemCacheStore's #write method supports the +:expires_in+
|
||||
# option, which tells the memcached server to automatically expire the
|
||||
# cache item after a certain period. We can use this option with #fetch
|
||||
# too:
|
||||
# For example, MemCacheStore's #write method supports the +:raw+
|
||||
# option, which tells the memcached server to store all values as strings.
|
||||
# We can use this option with #fetch too:
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemCacheStore.new
|
||||
# cache.fetch("foo", :force => true, :expires_in => 5.seconds) do
|
||||
# "bar"
|
||||
# cache.fetch("foo", :force => true, :raw => true) do
|
||||
# :bar
|
||||
# end
|
||||
# cache.fetch("foo") # => "bar"
|
||||
# sleep(6)
|
||||
# cache.fetch("foo") # => nil
|
||||
def fetch(key, options = {})
|
||||
@logger_off = true
|
||||
if !options[:force] && value = read(key, options)
|
||||
@logger_off = false
|
||||
log("hit", key, options)
|
||||
value
|
||||
elsif block_given?
|
||||
@logger_off = false
|
||||
log("miss", key, options)
|
||||
def fetch(name, options = nil)
|
||||
if block_given?
|
||||
options = merged_options(options)
|
||||
key = namespaced_key(name, options)
|
||||
unless options[:force]
|
||||
entry = instrument(:read, name, options) do |payload|
|
||||
payload[:super_operation] = :fetch if payload
|
||||
read_entry(key, options)
|
||||
end
|
||||
end
|
||||
if entry && entry.expired?
|
||||
race_ttl = options[:race_condition_ttl].to_f
|
||||
if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl
|
||||
entry.expires_at = Time.now + race_ttl
|
||||
write_entry(key, entry, :expires_in => race_ttl * 2)
|
||||
else
|
||||
delete_entry(key, options)
|
||||
end
|
||||
entry = nil
|
||||
end
|
||||
|
||||
value = nil
|
||||
ms = Benchmark.ms { value = yield }
|
||||
|
||||
@logger_off = true
|
||||
write(key, value, options)
|
||||
@logger_off = false
|
||||
|
||||
log('write (will save %.2fms)' % ms, key, nil)
|
||||
|
||||
value
|
||||
if entry
|
||||
instrument(:fetch_hit, name, options) { |payload| }
|
||||
entry.value
|
||||
else
|
||||
result = instrument(:generate, name, options) do |payload|
|
||||
yield
|
||||
end
|
||||
write(name, result, options)
|
||||
result
|
||||
end
|
||||
else
|
||||
read(name, options)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -175,73 +308,330 @@ module ActiveSupport
|
||||
# the cache with the given key, then that data is returned. Otherwise,
|
||||
# nil is returned.
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# The specific cache store implementation will decide what to do with
|
||||
# +options+.
|
||||
def read(key, options = nil)
|
||||
log("read", key, options)
|
||||
end
|
||||
|
||||
# Writes the given value to the cache, with the given key.
|
||||
#
|
||||
# You may also specify additional options via the +options+ argument.
|
||||
# The specific cache store implementation will decide what to do with
|
||||
# +options+.
|
||||
#
|
||||
# For example, MemCacheStore supports the +:expires_in+ option, which
|
||||
# tells the memcached server to automatically expire the cache item after
|
||||
# a certain period:
|
||||
#
|
||||
# cache = ActiveSupport::Cache::MemCacheStore.new
|
||||
# cache.write("foo", "bar", :expires_in => 5.seconds)
|
||||
# cache.read("foo") # => "bar"
|
||||
# sleep(6)
|
||||
# cache.read("foo") # => nil
|
||||
def write(key, value, options = nil)
|
||||
log("write", key, options)
|
||||
end
|
||||
|
||||
def delete(key, options = nil)
|
||||
log("delete", key, options)
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
log("delete matched", matcher.inspect, options)
|
||||
end
|
||||
|
||||
def exist?(key, options = nil)
|
||||
log("exist?", key, options)
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
log("incrementing", key, amount)
|
||||
if num = read(key)
|
||||
write(key, num + amount)
|
||||
else
|
||||
nil
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def read(name, options = nil)
|
||||
options = merged_options(options)
|
||||
key = namespaced_key(name, options)
|
||||
instrument(:read, name, options) do |payload|
|
||||
entry = read_entry(key, options)
|
||||
if entry
|
||||
if entry.expired?
|
||||
delete_entry(key, options)
|
||||
payload[:hit] = false if payload
|
||||
nil
|
||||
else
|
||||
payload[:hit] = true if payload
|
||||
entry.value
|
||||
end
|
||||
else
|
||||
payload[:hit] = false if payload
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
log("decrementing", key, amount)
|
||||
if num = read(key)
|
||||
write(key, num - amount)
|
||||
# Read multiple values at once from the cache. Options can be passed
|
||||
# in the last argument.
|
||||
#
|
||||
# Some cache implementation may optimize this method.
|
||||
#
|
||||
# Returns a hash mapping the names provided to the values found.
|
||||
def read_multi(*names)
|
||||
options = names.extract_options!
|
||||
options = merged_options(options)
|
||||
results = {}
|
||||
names.each do |name|
|
||||
key = namespaced_key(name, options)
|
||||
entry = read_entry(key, options)
|
||||
if entry
|
||||
if entry.expired?
|
||||
delete_entry(key, options)
|
||||
else
|
||||
results[name] = entry.value
|
||||
end
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
|
||||
# Writes the value to the cache, with the key.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def write(name, value, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:write, name, options) do |payload|
|
||||
entry = Entry.new(value, options)
|
||||
write_entry(namespaced_key(name, options), entry, options)
|
||||
end
|
||||
end
|
||||
|
||||
# Deletes an entry in the cache. Returns +true+ if an entry is deleted.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def delete(name, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:delete, name) do |payload|
|
||||
delete_entry(namespaced_key(name, options), options)
|
||||
end
|
||||
end
|
||||
|
||||
# Return true if the cache contains an entry for the given key.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
def exist?(name, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:exist?, name) do |payload|
|
||||
entry = read_entry(namespaced_key(name, options), options)
|
||||
if entry && !entry.expired?
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Delete all entries with keys matching the pattern.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def delete_matched(matcher, options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support delete_matched")
|
||||
end
|
||||
|
||||
# Increment an integer value in the cache.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def increment(name, amount = 1, options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support increment")
|
||||
end
|
||||
|
||||
# Increment an integer value in the cache.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support decrement")
|
||||
end
|
||||
|
||||
# Cleanup the cache by removing expired entries.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def cleanup(options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support cleanup")
|
||||
end
|
||||
|
||||
# Clear the entire cache. Be careful with this method since it could
|
||||
# affect other processes if shared cache is being used.
|
||||
#
|
||||
# Options are passed to the underlying cache implementation.
|
||||
#
|
||||
# All implementations may not support this method.
|
||||
def clear(options = nil)
|
||||
raise NotImplementedError.new("#{self.class.name} does not support clear")
|
||||
end
|
||||
|
||||
protected
|
||||
# Add the namespace defined in the options to a pattern designed to match keys.
|
||||
# Implementations that support delete_matched should call this method to translate
|
||||
# a pattern that matches names into one that matches namespaced keys.
|
||||
def key_matcher(pattern, options)
|
||||
prefix = options[:namespace].is_a?(Proc) ? options[:namespace].call : options[:namespace]
|
||||
if prefix
|
||||
source = pattern.source
|
||||
if source.start_with?('^')
|
||||
source = source[1, source.length]
|
||||
else
|
||||
source = ".*#{source[0, source.length]}"
|
||||
end
|
||||
Regexp.new("^#{Regexp.escape(prefix)}:#{source}", pattern.options)
|
||||
else
|
||||
pattern
|
||||
end
|
||||
end
|
||||
|
||||
# Read an entry from the cache implementation. Subclasses must implement this method.
|
||||
def read_entry(key, options) # :nodoc:
|
||||
raise NotImplementedError.new
|
||||
end
|
||||
|
||||
# Write an entry to the cache implementation. Subclasses must implement this method.
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
raise NotImplementedError.new
|
||||
end
|
||||
|
||||
# Delete an entry from the cache implementation. Subclasses must implement this method.
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
raise NotImplementedError.new
|
||||
end
|
||||
|
||||
private
|
||||
# Merge the default options with ones specific to a method call.
|
||||
def merged_options(call_options) # :nodoc:
|
||||
if call_options
|
||||
options.merge(call_options)
|
||||
else
|
||||
options.dup
|
||||
end
|
||||
end
|
||||
|
||||
# Expand key to be a consistent string value. Invoke +cache_key+ if
|
||||
# object responds to +cache_key+. Otherwise, to_param method will be
|
||||
# called. If the key is a Hash, then keys will be sorted alphabetically.
|
||||
def expanded_key(key) # :nodoc:
|
||||
return key.cache_key.to_s if key.respond_to?(:cache_key)
|
||||
|
||||
case key
|
||||
when Array
|
||||
if key.size > 1
|
||||
key = key.collect{|element| expanded_key(element)}
|
||||
else
|
||||
key = key.first
|
||||
end
|
||||
when Hash
|
||||
key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
|
||||
end
|
||||
|
||||
key.to_param
|
||||
end
|
||||
|
||||
# Prefix a key with the namespace. Namespace and key will be delimited with a colon.
|
||||
def namespaced_key(key, options)
|
||||
key = expanded_key(key)
|
||||
namespace = options[:namespace] if options
|
||||
prefix = namespace.is_a?(Proc) ? namespace.call : namespace
|
||||
key = "#{prefix}:#{key}" if prefix
|
||||
key
|
||||
end
|
||||
|
||||
def instrument(operation, key, options = nil)
|
||||
log(operation, key, options)
|
||||
|
||||
if self.class.instrument
|
||||
payload = { :key => key }
|
||||
payload.merge!(options) if options.is_a?(Hash)
|
||||
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
|
||||
else
|
||||
yield(nil)
|
||||
end
|
||||
end
|
||||
|
||||
def log(operation, key, options = nil)
|
||||
return unless logger && logger.debug? && !silence?
|
||||
logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
|
||||
end
|
||||
end
|
||||
|
||||
# Entry that is put into caches. It supports expiration time on entries and can compress values
|
||||
# to save space in the cache.
|
||||
class Entry
|
||||
attr_reader :created_at, :expires_in
|
||||
|
||||
DEFAULT_COMPRESS_LIMIT = 16.kilobytes
|
||||
|
||||
class << self
|
||||
# Create an entry with internal attributes set. This method is intended to be
|
||||
# used by implementations that store cache entries in a native format instead
|
||||
# of as serialized Ruby objects.
|
||||
def create(raw_value, created_at, options = {})
|
||||
entry = new(nil)
|
||||
entry.instance_variable_set(:@value, raw_value)
|
||||
entry.instance_variable_set(:@created_at, created_at.to_f)
|
||||
entry.instance_variable_set(:@compressed, options[:compressed])
|
||||
entry.instance_variable_set(:@expires_in, options[:expires_in])
|
||||
entry
|
||||
end
|
||||
end
|
||||
|
||||
# Create a new cache entry for the specified value. Options supported are
|
||||
# +:compress+, +:compress_threshold+, and +:expires_in+.
|
||||
def initialize(value, options = {})
|
||||
@compressed = false
|
||||
@expires_in = options[:expires_in]
|
||||
@expires_in = @expires_in.to_f if @expires_in
|
||||
@created_at = Time.now.to_f
|
||||
if value.nil?
|
||||
@value = nil
|
||||
else
|
||||
nil
|
||||
@value = Marshal.dump(value)
|
||||
if should_compress?(@value, options)
|
||||
@value = Zlib::Deflate.deflate(@value)
|
||||
@compressed = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Get the raw value. This value may be serialized and compressed.
|
||||
def raw_value
|
||||
@value
|
||||
end
|
||||
|
||||
# Get the value stored in the cache.
|
||||
def value
|
||||
# If the original value was exactly false @value is still true because
|
||||
# it is marshalled and eventually compressed. Both operations yield
|
||||
# strings.
|
||||
if @value
|
||||
# In rails 3.1 and earlier values in entries did not marshaled without
|
||||
# options[:compress] and if it's Numeric.
|
||||
# But after commit a263f377978fc07515b42808ebc1f7894fafaa3a
|
||||
# all values in entries are marshalled. And after that code below expects
|
||||
# that all values in entries will be marshaled (and will be strings).
|
||||
# So here we need a check for old ones.
|
||||
begin
|
||||
Marshal.load(compressed? ? Zlib::Inflate.inflate(@value) : @value)
|
||||
rescue TypeError
|
||||
compressed? ? Zlib::Inflate.inflate(@value) : @value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def compressed?
|
||||
@compressed
|
||||
end
|
||||
|
||||
# Check if the entry is expired. The +expires_in+ parameter can override the
|
||||
# value set when the entry was created.
|
||||
def expired?
|
||||
@expires_in && @created_at + @expires_in <= Time.now.to_f
|
||||
end
|
||||
|
||||
# Set a new time when the entry will expire.
|
||||
def expires_at=(time)
|
||||
if time
|
||||
@expires_in = time.to_f - @created_at
|
||||
else
|
||||
@expires_in = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Seconds since the epoch when the entry will expire.
|
||||
def expires_at
|
||||
@expires_in ? @created_at + @expires_in : nil
|
||||
end
|
||||
|
||||
# Returns the size of the cached value. This could be less than value.size
|
||||
# if the data is compressed.
|
||||
def size
|
||||
if @value.nil?
|
||||
0
|
||||
else
|
||||
@value.bytesize
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def expires_in(options)
|
||||
expires_in = options && options[:expires_in]
|
||||
|
||||
raise ":expires_in must be a number" if expires_in && !expires_in.is_a?(Numeric)
|
||||
|
||||
expires_in || 0
|
||||
end
|
||||
|
||||
def log(operation, key, options)
|
||||
logger.debug("Cache #{operation}: #{key}#{options ? " (#{options.inspect})" : ""}") if logger && !silence? && !logger_off?
|
||||
def should_compress?(serialized_value, options)
|
||||
if options[:compress]
|
||||
compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
|
||||
return true if serialized_value.size >= compress_threshold
|
||||
end
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
class CompressedMemCacheStore < MemCacheStore
|
||||
def read(name, options = nil)
|
||||
if value = super(name, (options || {}).merge(:raw => true))
|
||||
if raw?(options)
|
||||
value
|
||||
else
|
||||
Marshal.load(ActiveSupport::Gzip.decompress(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
value = ActiveSupport::Gzip.compress(Marshal.dump(value)) unless raw?(options)
|
||||
super(name, value, (options || {}).merge(:raw => true))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
class DRbStore < MemoryStore #:nodoc:
|
||||
attr_reader :address
|
||||
|
||||
def initialize(address = 'druby://localhost:9192')
|
||||
require 'drb' unless defined?(DRbObject)
|
||||
super()
|
||||
@address = address
|
||||
@data = DRbObject.new(nil, address)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
176
activesupport/lib/active_support/cache/file_store.rb
vendored
176
activesupport/lib/active_support/cache/file_store.rb
vendored
@@ -1,64 +1,170 @@
|
||||
require 'active_support/core_ext/file/atomic'
|
||||
require 'active_support/core_ext/string/conversions'
|
||||
require 'active_support/core_ext/object/inclusion'
|
||||
require 'rack/utils'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores everything on the filesystem.
|
||||
#
|
||||
# FileStore implements the Strategy::LocalCache strategy which implements
|
||||
# an in-memory cache inside of a block.
|
||||
class FileStore < Store
|
||||
attr_reader :cache_path
|
||||
|
||||
def initialize(cache_path)
|
||||
@cache_path = cache_path
|
||||
DIR_FORMATTER = "%03X"
|
||||
FILENAME_MAX_SIZE = 230 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
|
||||
EXCLUDED_DIRS = ['.', '..'].freeze
|
||||
|
||||
def initialize(cache_path, options = nil)
|
||||
super(options)
|
||||
@cache_path = cache_path.to_s
|
||||
extend Strategy::LocalCache
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
super
|
||||
File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil
|
||||
def clear(options = nil)
|
||||
root_dirs = Dir.entries(cache_path).reject{|f| f.in?(EXCLUDED_DIRS)}
|
||||
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
super
|
||||
ensure_cache_path(File.dirname(real_file_path(name)))
|
||||
File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
|
||||
value
|
||||
rescue => e
|
||||
logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
|
||||
def cleanup(options = nil)
|
||||
options = merged_options(options)
|
||||
each_key(options) do |key|
|
||||
entry = read_entry(key, options)
|
||||
delete_entry(key, options) if entry && entry.expired?
|
||||
end
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
super
|
||||
File.delete(real_file_path(name))
|
||||
rescue SystemCallError => e
|
||||
# If there's no cache, then there's nothing to complain about
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
super
|
||||
search_dir(@cache_path) do |f|
|
||||
if f =~ matcher
|
||||
begin
|
||||
File.delete(f)
|
||||
rescue SystemCallError => e
|
||||
# If there's no cache, then there's nothing to complain about
|
||||
end
|
||||
def increment(name, amount = 1, options = nil)
|
||||
file_name = key_file_path(namespaced_key(name, options))
|
||||
lock_file(file_name) do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i + amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exist?(name, options = nil)
|
||||
super
|
||||
File.exist?(real_file_path(name))
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
file_name = key_file_path(namespaced_key(name, options))
|
||||
lock_file(file_name) do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i - amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def real_file_path(name)
|
||||
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
|
||||
def delete_matched(matcher, options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:delete_matched, matcher.inspect) do
|
||||
matcher = key_matcher(matcher, options)
|
||||
search_dir(cache_path) do |path|
|
||||
key = file_path_key(path)
|
||||
delete_entry(key, options) if key.match(matcher)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def read_entry(key, options)
|
||||
file_name = key_file_path(key)
|
||||
if File.exist?(file_name)
|
||||
File.open(file_name) { |f| Marshal.load(f) }
|
||||
end
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options)
|
||||
file_name = key_file_path(key)
|
||||
ensure_cache_path(File.dirname(file_name))
|
||||
File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
|
||||
true
|
||||
end
|
||||
|
||||
def delete_entry(key, options)
|
||||
file_name = key_file_path(key)
|
||||
if File.exist?(file_name)
|
||||
begin
|
||||
File.delete(file_name)
|
||||
delete_empty_directories(File.dirname(file_name))
|
||||
true
|
||||
rescue => e
|
||||
# Just in case the error was caused by another process deleting the file first.
|
||||
raise e if File.exist?(file_name)
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
# Lock a file for a block so only one process can modify it at a time.
|
||||
def lock_file(file_name, &block) # :nodoc:
|
||||
if File.exist?(file_name)
|
||||
File.open(file_name, 'r+') do |f|
|
||||
begin
|
||||
f.flock File::LOCK_EX
|
||||
yield
|
||||
ensure
|
||||
f.flock File::LOCK_UN
|
||||
end
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# Translate a key into a file path.
|
||||
def key_file_path(key)
|
||||
fname = Rack::Utils.escape(key)
|
||||
hash = Zlib.adler32(fname)
|
||||
hash, dir_1 = hash.divmod(0x1000)
|
||||
dir_2 = hash.modulo(0x1000)
|
||||
fname_paths = []
|
||||
|
||||
# Make sure file name doesn't exceed file system limits.
|
||||
begin
|
||||
fname_paths << fname[0, FILENAME_MAX_SIZE]
|
||||
fname = fname[FILENAME_MAX_SIZE..-1]
|
||||
end until fname.blank?
|
||||
|
||||
File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths)
|
||||
end
|
||||
|
||||
# Translate a file path into a key.
|
||||
def file_path_key(path)
|
||||
fname = path[cache_path.size, path.size].split(File::SEPARATOR, 4).last
|
||||
Rack::Utils.unescape(fname)
|
||||
end
|
||||
|
||||
# Delete empty directories in the cache.
|
||||
def delete_empty_directories(dir)
|
||||
return if dir == cache_path
|
||||
if Dir.entries(dir).reject{|f| f.in?(EXCLUDED_DIRS)}.empty?
|
||||
File.delete(dir) rescue nil
|
||||
delete_empty_directories(File.dirname(dir))
|
||||
end
|
||||
end
|
||||
|
||||
# Make sure a file path's directories exist.
|
||||
def ensure_cache_path(path)
|
||||
FileUtils.makedirs(path) unless File.exist?(path)
|
||||
end
|
||||
|
||||
def search_dir(dir, &callback)
|
||||
return if !File.exist?(dir)
|
||||
Dir.foreach(dir) do |d|
|
||||
next if d == "." || d == ".."
|
||||
next if d.in?(EXCLUDED_DIRS)
|
||||
name = File.join(dir, d)
|
||||
if File.directory?(name)
|
||||
search_dir(name, &callback)
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
require 'memcache'
|
||||
begin
|
||||
require 'memcache'
|
||||
rescue LoadError => e
|
||||
$stderr.puts "You don't have memcache-client installed in your application. Please add it to your Gemfile and run bundle install"
|
||||
raise e
|
||||
end
|
||||
|
||||
require 'digest/md5'
|
||||
require 'active_support/core_ext/string/encoding'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores data in Memcached:
|
||||
# http://www.danga.com/memcached/
|
||||
# http://memcached.org/
|
||||
#
|
||||
# This is currently the most popular cache store for production websites.
|
||||
#
|
||||
# Special features:
|
||||
# - Clustering and load balancing. One can specify multiple memcached servers,
|
||||
# and MemCacheStore will load balance between all available servers. If a
|
||||
# server goes down, then MemCacheStore will ignore it until it goes back
|
||||
# online.
|
||||
# - Time-based expiry support. See #write and the +:expires_in+ option.
|
||||
# - Per-request in memory cache for all communication with the MemCache server(s).
|
||||
# server goes down, then MemCacheStore will ignore it until it comes back up.
|
||||
#
|
||||
# MemCacheStore implements the Strategy::LocalCache strategy which implements
|
||||
# an in-memory cache inside of a block.
|
||||
class MemCacheStore < Store
|
||||
module Response # :nodoc:
|
||||
STORED = "STORED\r\n"
|
||||
@@ -23,10 +31,12 @@ module ActiveSupport
|
||||
DELETED = "DELETED\r\n"
|
||||
end
|
||||
|
||||
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
|
||||
|
||||
def self.build_mem_cache(*addresses)
|
||||
addresses = addresses.flatten
|
||||
options = addresses.extract_options!
|
||||
addresses = ["localhost"] if addresses.empty?
|
||||
addresses = ["localhost:11211"] if addresses.empty?
|
||||
MemCache.new(addresses, options)
|
||||
end
|
||||
|
||||
@@ -44,100 +54,153 @@ module ActiveSupport
|
||||
# require 'memcached' # gem install memcached; uses C bindings to libmemcached
|
||||
# ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
|
||||
def initialize(*addresses)
|
||||
addresses = addresses.flatten
|
||||
options = addresses.extract_options!
|
||||
super(options)
|
||||
|
||||
if addresses.first.respond_to?(:get)
|
||||
@data = addresses.first
|
||||
else
|
||||
@data = self.class.build_mem_cache(*addresses)
|
||||
mem_cache_options = options.dup
|
||||
UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)}
|
||||
@data = self.class.build_mem_cache(*(addresses + [mem_cache_options]))
|
||||
end
|
||||
|
||||
extend Strategy::LocalCache
|
||||
extend LocalCacheWithRaw
|
||||
end
|
||||
|
||||
# Reads multiple keys from the cache.
|
||||
def read_multi(*keys)
|
||||
@data.get_multi keys
|
||||
# Reads multiple values from the cache using a single call to the
|
||||
# servers for all keys. Options can be passed in the last argument.
|
||||
def read_multi(*names)
|
||||
options = names.extract_options!
|
||||
options = merged_options(options)
|
||||
keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}]
|
||||
raw_values = @data.get_multi(keys_to_names.keys, :raw => true)
|
||||
values = {}
|
||||
raw_values.each do |key, value|
|
||||
entry = deserialize_entry(value)
|
||||
values[keys_to_names[key]] = entry.value unless entry.expired?
|
||||
end
|
||||
values
|
||||
end
|
||||
|
||||
def read(key, options = nil) # :nodoc:
|
||||
super
|
||||
@data.get(key, raw?(options))
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
# Writes a value to the cache.
|
||||
#
|
||||
# Possible options:
|
||||
# - +:unless_exist+ - set to true if you don't want to update the cache
|
||||
# if the key is already set.
|
||||
# - +:expires_in+ - the number of seconds that this value may stay in
|
||||
# the cache. See ActiveSupport::Cache::Store#write for an example.
|
||||
def write(key, value, options = nil)
|
||||
super
|
||||
method = options && options[:unless_exist] ? :add : :set
|
||||
# memcache-client will break the connection if you send it an integer
|
||||
# in raw mode, so we convert it to a string to be sure it continues working.
|
||||
value = value.to_s if raw?(options)
|
||||
response = @data.send(method, key, value, expires_in(options), raw?(options))
|
||||
response == Response::STORED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def delete(key, options = nil) # :nodoc:
|
||||
super
|
||||
response = @data.delete(key, expires_in(options))
|
||||
response == Response::DELETED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}")
|
||||
false
|
||||
end
|
||||
|
||||
def exist?(key, options = nil) # :nodoc:
|
||||
# Doesn't call super, cause exist? in memcache is in fact a read
|
||||
# But who cares? Reading is very fast anyway
|
||||
# Local cache is checked first, if it doesn't know then memcache itself is read from
|
||||
!read(key, options).nil?
|
||||
end
|
||||
|
||||
def increment(key, amount = 1) # :nodoc:
|
||||
log("incrementing", key, amount)
|
||||
|
||||
response = @data.incr(key, amount)
|
||||
response == Response::NOT_FOUND ? nil : response
|
||||
# Increment a cached value. This method uses the memcached incr atomic
|
||||
# operator and can only be used on values written with the :raw option.
|
||||
# Calling it on a value not stored with :raw will initialize that value
|
||||
# to zero.
|
||||
def increment(name, amount = 1, options = nil) # :nodoc:
|
||||
options = merged_options(options)
|
||||
response = instrument(:increment, name, :amount => amount) do
|
||||
@data.incr(escape_key(namespaced_key(name, options)), amount)
|
||||
end
|
||||
response == Response::NOT_FOUND ? nil : response.to_i
|
||||
rescue MemCache::MemCacheError
|
||||
nil
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1) # :nodoc:
|
||||
log("decrement", key, amount)
|
||||
response = @data.decr(key, amount)
|
||||
response == Response::NOT_FOUND ? nil : response
|
||||
# Decrement a cached value. This method uses the memcached decr atomic
|
||||
# operator and can only be used on values written with the :raw option.
|
||||
# Calling it on a value not stored with :raw will initialize that value
|
||||
# to zero.
|
||||
def decrement(name, amount = 1, options = nil) # :nodoc:
|
||||
options = merged_options(options)
|
||||
response = instrument(:decrement, name, :amount => amount) do
|
||||
@data.decr(escape_key(namespaced_key(name, options)), amount)
|
||||
end
|
||||
response == Response::NOT_FOUND ? nil : response.to_i
|
||||
rescue MemCache::MemCacheError
|
||||
nil
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil) # :nodoc:
|
||||
# don't do any local caching at present, just pass
|
||||
# through and let the error happen
|
||||
super
|
||||
raise "Not supported by Memcache"
|
||||
end
|
||||
|
||||
def clear
|
||||
# Clear the entire cache on all memcached servers. This method should
|
||||
# be used with care when shared cache is being used.
|
||||
def clear(options = nil)
|
||||
@data.flush_all
|
||||
end
|
||||
|
||||
# Get the statistics from the memcached servers.
|
||||
def stats
|
||||
@data.stats
|
||||
end
|
||||
|
||||
private
|
||||
def raw?(options)
|
||||
options && options[:raw]
|
||||
protected
|
||||
# Read an entry from the cache.
|
||||
def read_entry(key, options) # :nodoc:
|
||||
deserialize_entry(@data.get(escape_key(key), true))
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}") if logger
|
||||
nil
|
||||
end
|
||||
|
||||
# Write an entry to the cache.
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
method = options && options[:unless_exist] ? :add : :set
|
||||
value = options[:raw] ? entry.value.to_s : entry
|
||||
expires_in = options[:expires_in].to_i
|
||||
if expires_in > 0 && !options[:raw]
|
||||
# Set the memcache expire a few minutes in the future to support race condition ttls on read
|
||||
expires_in += 5.minutes
|
||||
end
|
||||
response = @data.send(method, escape_key(key), value, expires_in, options[:raw])
|
||||
response == Response::STORED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}") if logger
|
||||
false
|
||||
end
|
||||
|
||||
# Delete an entry from the cache.
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
response = @data.delete(escape_key(key))
|
||||
response == Response::DELETED
|
||||
rescue MemCache::MemCacheError => e
|
||||
logger.error("MemCacheError (#{e}): #{e.message}") if logger
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Memcache keys are binaries. So we need to force their encoding to binary
|
||||
# before applying the regular expression to ensure we are escaping all
|
||||
# characters properly.
|
||||
def escape_key(key)
|
||||
key = key.to_s.dup
|
||||
key = key.force_encoding("BINARY") if key.encoding_aware?
|
||||
key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" }
|
||||
key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
|
||||
key
|
||||
end
|
||||
|
||||
def deserialize_entry(raw_value)
|
||||
if raw_value
|
||||
entry = Marshal.load(raw_value) rescue raw_value
|
||||
entry.is_a?(Entry) ? entry : Entry.new(entry)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Provide support for raw values in the local cache strategy.
|
||||
module LocalCacheWithRaw # :nodoc:
|
||||
protected
|
||||
def read_entry(key, options)
|
||||
entry = super
|
||||
if options[:raw] && local_cache && entry
|
||||
entry = deserialize_entry(entry.value)
|
||||
end
|
||||
entry
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
retval = super
|
||||
if options[:raw] && local_cache && retval
|
||||
raw_entry = Entry.new(entry.value.to_s)
|
||||
raw_entry.expires_at = entry.expires_at
|
||||
local_cache.write_entry(key, raw_entry, options)
|
||||
end
|
||||
retval
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,58 +1,159 @@
|
||||
require 'monitor'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which stores everything into memory in the
|
||||
# same process. If you're running multiple Ruby on Rails server processes
|
||||
# (which is the case if you're using mongrel_cluster or Phusion Passenger),
|
||||
# then this means that your Rails server process instances won't be able
|
||||
# to share cache data with each other. If your application never performs
|
||||
# manual cache item expiry (e.g. when you're using generational cache keys),
|
||||
# then using MemoryStore is ok. Otherwise, consider carefully whether you
|
||||
# should be using this cache store.
|
||||
# then this means that Rails server process instances won't be able
|
||||
# to share cache data with each other and this may not be the most
|
||||
# appropriate cache in that scenario.
|
||||
#
|
||||
# MemoryStore is not only able to store strings, but also arbitrary Ruby
|
||||
# objects.
|
||||
# This cache has a bounded size specified by the :size options to the
|
||||
# initializer (default is 32Mb). When the cache exceeds the allotted size,
|
||||
# a cleanup will occur which tries to prune the cache down to three quarters
|
||||
# of the maximum size by removing the least recently used entries.
|
||||
#
|
||||
# MemoryStore is not thread-safe. Use SynchronizedMemoryStore instead
|
||||
# if you need thread-safety.
|
||||
# MemoryStore is thread-safe.
|
||||
class MemoryStore < Store
|
||||
def initialize
|
||||
def initialize(options = nil)
|
||||
options ||= {}
|
||||
super(options)
|
||||
@data = {}
|
||||
@key_access = {}
|
||||
@max_size = options[:size] || 32.megabytes
|
||||
@max_prune_time = options[:max_prune_time] || 2
|
||||
@cache_size = 0
|
||||
@monitor = Monitor.new
|
||||
@pruning = false
|
||||
end
|
||||
|
||||
def read_multi(*names)
|
||||
results = {}
|
||||
names.each { |n| results[n] = read(n) }
|
||||
results
|
||||
def clear(options = nil)
|
||||
synchronize do
|
||||
@data.clear
|
||||
@key_access.clear
|
||||
@cache_size = 0
|
||||
end
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
super
|
||||
@data[name]
|
||||
def cleanup(options = nil)
|
||||
options = merged_options(options)
|
||||
instrument(:cleanup, :size => @data.size) do
|
||||
keys = synchronize{ @data.keys }
|
||||
keys.each do |key|
|
||||
entry = @data[key]
|
||||
delete_entry(key, options) if entry && entry.expired?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
super
|
||||
@data[name] = value.freeze
|
||||
# To ensure entries fit within the specified memory prune the cache by removing the least
|
||||
# recently accessed entries.
|
||||
def prune(target_size, max_time = nil)
|
||||
return if pruning?
|
||||
@pruning = true
|
||||
begin
|
||||
start_time = Time.now
|
||||
cleanup
|
||||
instrument(:prune, target_size, :from => @cache_size) do
|
||||
keys = synchronize{ @key_access.keys.sort{|a,b| @key_access[a].to_f <=> @key_access[b].to_f} }
|
||||
keys.each do |key|
|
||||
delete_entry(key, options)
|
||||
return if @cache_size <= target_size || (max_time && Time.now - start_time > max_time)
|
||||
end
|
||||
end
|
||||
ensure
|
||||
@pruning = false
|
||||
end
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
super
|
||||
@data.delete(name)
|
||||
# Returns true if the cache is currently being pruned.
|
||||
def pruning?
|
||||
@pruning
|
||||
end
|
||||
|
||||
# Increment an integer value in the cache.
|
||||
def increment(name, amount = 1, options = nil)
|
||||
synchronize do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i + amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Decrement an integer value in the cache.
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
synchronize do
|
||||
options = merged_options(options)
|
||||
if num = read(name, options)
|
||||
num = num.to_i - amount
|
||||
write(name, num, options)
|
||||
num
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
super
|
||||
@data.delete_if { |k,v| k =~ matcher }
|
||||
options = merged_options(options)
|
||||
instrument(:delete_matched, matcher.inspect) do
|
||||
matcher = key_matcher(matcher, options)
|
||||
keys = synchronize { @data.keys }
|
||||
keys.each do |key|
|
||||
delete_entry(key, options) if key.match(matcher)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def exist?(name, options = nil)
|
||||
super
|
||||
@data.has_key?(name)
|
||||
def inspect # :nodoc:
|
||||
"<##{self.class.name} entries=#{@data.size}, size=#{@cache_size}, options=#{@options.inspect}>"
|
||||
end
|
||||
|
||||
def clear
|
||||
@data.clear
|
||||
# Synchronize calls to the cache. This should be called wherever the underlying cache implementation
|
||||
# is not thread safe.
|
||||
def synchronize(&block) # :nodoc:
|
||||
@monitor.synchronize(&block)
|
||||
end
|
||||
|
||||
protected
|
||||
def read_entry(key, options) # :nodoc:
|
||||
entry = @data[key]
|
||||
synchronize do
|
||||
if entry
|
||||
@key_access[key] = Time.now.to_f
|
||||
else
|
||||
@key_access.delete(key)
|
||||
end
|
||||
end
|
||||
entry
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
synchronize do
|
||||
old_entry = @data[key]
|
||||
@cache_size -= old_entry.size if old_entry
|
||||
@cache_size += entry.size
|
||||
@key_access[key] = Time.now.to_f
|
||||
@data[key] = entry
|
||||
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
synchronize do
|
||||
@key_access.delete(key)
|
||||
entry = @data.delete(key)
|
||||
@cache_size -= entry.size if entry
|
||||
!!entry
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
44
activesupport/lib/active_support/cache/null_store.rb
vendored
Normal file
44
activesupport/lib/active_support/cache/null_store.rb
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# A cache store implementation which doesn't actually store anything. Useful in
|
||||
# development and test environments where you don't want caching turned on but
|
||||
# need to go through the caching interface.
|
||||
#
|
||||
# This cache does implement the local cache strategy, so values will actually
|
||||
# be cached inside blocks that utilize this strategy. See
|
||||
# ActiveSupport::Cache::Strategy::LocalCache for more details.
|
||||
class NullStore < Store
|
||||
def initialize(options = nil)
|
||||
super(options)
|
||||
extend Strategy::LocalCache
|
||||
end
|
||||
|
||||
def clear(options = nil)
|
||||
end
|
||||
|
||||
def cleanup(options = nil)
|
||||
end
|
||||
|
||||
def increment(name, amount = 1, options = nil)
|
||||
end
|
||||
|
||||
def decrement(name, amount = 1, options = nil)
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
end
|
||||
|
||||
protected
|
||||
def read_entry(key, options) # :nodoc:
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
true
|
||||
end
|
||||
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,103 +1,168 @@
|
||||
require 'active_support/core_ext/object/duplicable'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
module Strategy
|
||||
# Caches that implement LocalCache will be backed by an in-memory cache for the
|
||||
# duration of a block. Repeated calls to the cache for the same key will hit the
|
||||
# in-memory cache for faster access.
|
||||
module LocalCache
|
||||
# this allows caching of the fact that there is nothing in the remote cache
|
||||
NULL = 'remote_cache_store:null'
|
||||
|
||||
def with_local_cache
|
||||
Thread.current[thread_local_key] = MemoryStore.new
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = nil
|
||||
end
|
||||
|
||||
def middleware
|
||||
@middleware ||= begin
|
||||
klass = Class.new
|
||||
klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def initialize(app)
|
||||
@app = app
|
||||
end
|
||||
|
||||
def call(env)
|
||||
Thread.current[:#{thread_local_key}] = MemoryStore.new
|
||||
@app.call(env)
|
||||
ensure
|
||||
Thread.current[:#{thread_local_key}] = nil
|
||||
end
|
||||
EOS
|
||||
klass
|
||||
# Simple memory backed cache. This cache is not thread safe and is intended only
|
||||
# for serving as a temporary memory cache for a single thread.
|
||||
class LocalStore < Store
|
||||
def initialize
|
||||
super
|
||||
@data = {}
|
||||
end
|
||||
end
|
||||
|
||||
def read(key, options = nil)
|
||||
value = local_cache && local_cache.read(key)
|
||||
if value == NULL
|
||||
nil
|
||||
elsif value.nil?
|
||||
value = super
|
||||
local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
|
||||
value.duplicable? ? value.dup : value
|
||||
else
|
||||
# forcing the value to be immutable
|
||||
value.duplicable? ? value.dup : value
|
||||
# Don't allow synchronizing since it isn't thread safe,
|
||||
def synchronize # :nodoc:
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def write(key, value, options = nil)
|
||||
value = value.to_s if respond_to?(:raw?) && raw?(options)
|
||||
local_cache.mute { local_cache.write(key, value || NULL) } if local_cache
|
||||
super
|
||||
end
|
||||
def clear(options = nil)
|
||||
@data.clear
|
||||
end
|
||||
|
||||
def delete(key, options = nil)
|
||||
local_cache.mute { local_cache.write(key, NULL) } if local_cache
|
||||
super
|
||||
end
|
||||
def read_entry(key, options)
|
||||
@data[key]
|
||||
end
|
||||
|
||||
def exist(key, options = nil)
|
||||
value = local_cache.read(key) if local_cache
|
||||
if value == NULL
|
||||
false
|
||||
elsif value
|
||||
def write_entry(key, value, options)
|
||||
@data[key] = value
|
||||
true
|
||||
else
|
||||
end
|
||||
|
||||
def delete_entry(key, options)
|
||||
!!@data.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
# Use a local cache for the duration of block.
|
||||
def with_local_cache
|
||||
save_val = Thread.current[thread_local_key]
|
||||
begin
|
||||
Thread.current[thread_local_key] = LocalStore.new
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = save_val
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# This class wraps up local storage for middlewares. Only the middleware method should
|
||||
# construct them.
|
||||
class Middleware # :nodoc:
|
||||
attr_reader :name, :thread_local_key
|
||||
|
||||
def initialize(name, thread_local_key)
|
||||
@name = name
|
||||
@thread_local_key = thread_local_key
|
||||
@app = nil
|
||||
end
|
||||
|
||||
def new(app)
|
||||
@app = app
|
||||
self
|
||||
end
|
||||
|
||||
def call(env)
|
||||
Thread.current[thread_local_key] = LocalStore.new
|
||||
@app.call(env)
|
||||
ensure
|
||||
Thread.current[thread_local_key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
# Middleware class can be inserted as a Rack handler to be local cache for the
|
||||
# duration of request.
|
||||
def middleware
|
||||
@middleware ||= Middleware.new(
|
||||
"ActiveSupport::Cache::Strategy::LocalCache",
|
||||
thread_local_key)
|
||||
end
|
||||
|
||||
def clear(options = nil) # :nodoc:
|
||||
local_cache.clear(options) if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
def cleanup(options = nil) # :nodoc:
|
||||
local_cache.clear(options) if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
def increment(name, amount = 1, options = nil) # :nodoc:
|
||||
value = bypass_local_cache{super}
|
||||
if local_cache
|
||||
local_cache.mute do
|
||||
if value
|
||||
local_cache.write(name, value, options)
|
||||
else
|
||||
local_cache.delete(name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
def decrement(name, amount = 1, options = nil) # :nodoc:
|
||||
value = bypass_local_cache{super}
|
||||
if local_cache
|
||||
local_cache.mute do
|
||||
if value
|
||||
local_cache.write(name, value, options)
|
||||
else
|
||||
local_cache.delete(name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
value
|
||||
end
|
||||
|
||||
protected
|
||||
def read_entry(key, options) # :nodoc:
|
||||
if local_cache
|
||||
entry = local_cache.read_entry(key, options)
|
||||
unless entry
|
||||
entry = super
|
||||
local_cache.write_entry(key, entry, options)
|
||||
end
|
||||
entry
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def write_entry(key, entry, options) # :nodoc:
|
||||
local_cache.write_entry(key, entry, options) if local_cache
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
if value = super
|
||||
local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
|
||||
value
|
||||
else
|
||||
nil
|
||||
def delete_entry(key, options) # :nodoc:
|
||||
local_cache.delete_entry(key, options) if local_cache
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
if value = super
|
||||
local_cache.mute { local_cache.write(key, value.to_s) } if local_cache
|
||||
value
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def clear
|
||||
local_cache.clear if local_cache
|
||||
super
|
||||
end
|
||||
|
||||
private
|
||||
def thread_local_key
|
||||
@thread_local_key ||= "#{self.class.name.underscore}_local_cache".gsub("/", "_").to_sym
|
||||
@thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
|
||||
end
|
||||
|
||||
def local_cache
|
||||
Thread.current[thread_local_key]
|
||||
end
|
||||
|
||||
def bypass_local_cache
|
||||
save_cache = Thread.current[thread_local_key]
|
||||
begin
|
||||
Thread.current[thread_local_key] = nil
|
||||
yield
|
||||
ensure
|
||||
Thread.current[thread_local_key] = save_cache
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
module ActiveSupport
|
||||
module Cache
|
||||
# Like MemoryStore, but thread-safe.
|
||||
class SynchronizedMemoryStore < MemoryStore
|
||||
def initialize
|
||||
super
|
||||
@guard = Monitor.new
|
||||
end
|
||||
|
||||
def fetch(key, options = {})
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def read(name, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def write(name, value, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def delete(name, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def delete_matched(matcher, options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def exist?(name,options = nil)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def increment(key, amount = 1)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def decrement(key, amount = 1)
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
|
||||
def clear
|
||||
@guard.synchronize { super }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,279 +1,626 @@
|
||||
require 'active_support/concern'
|
||||
require 'active_support/descendants_tracker'
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/class/attribute'
|
||||
require 'active_support/core_ext/kernel/reporting'
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/object/inclusion'
|
||||
|
||||
module ActiveSupport
|
||||
# Callbacks are hooks into the lifecycle of an object that allow you to trigger logic
|
||||
# before or after an alteration of the object state.
|
||||
# \Callbacks are code hooks that are run at key points in an object's lifecycle.
|
||||
# The typical use case is to have a base class define a set of callbacks relevant
|
||||
# to the other functionality it supplies, so that subclasses can install callbacks
|
||||
# that enhance or modify the base functionality without needing to override
|
||||
# or redefine methods of the base class.
|
||||
#
|
||||
# Mixing in this module allows you to define callbacks in your class.
|
||||
# Mixing in this module allows you to define the events in the object's lifecycle
|
||||
# that will support callbacks (via +ClassMethods.define_callbacks+), set the instance
|
||||
# methods, procs, or callback objects to be called (via +ClassMethods.set_callback+),
|
||||
# and run the installed callbacks at the appropriate times (via +run_callbacks+).
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# Three kinds of callbacks are supported: before callbacks, run before a certain event;
|
||||
# after callbacks, run after the event; and around callbacks, blocks that surround the
|
||||
# event, triggering it when they yield. Callback code can be contained in instance
|
||||
# methods, procs or lambdas, or callback objects that respond to certain predetermined
|
||||
# methods. See +ClassMethods.set_callback+ for details.
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class Record
|
||||
# include ActiveSupport::Callbacks
|
||||
# define_callbacks :save
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
# def save
|
||||
# run_callbacks :save do
|
||||
# puts "- save"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :saving_message
|
||||
# class PersonRecord < Record
|
||||
# set_callback :save, :before, :saving_message
|
||||
# def saving_message
|
||||
# puts "saving..."
|
||||
# end
|
||||
#
|
||||
# after_save do |object|
|
||||
# set_callback :save, :after do |object|
|
||||
# puts "saved"
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# run_callbacks(:before_save)
|
||||
# puts "- save"
|
||||
# run_callbacks(:after_save)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
# person = PersonRecord.new
|
||||
# person.save
|
||||
#
|
||||
# Output:
|
||||
# saving...
|
||||
# - save
|
||||
# saved
|
||||
#
|
||||
# Callbacks from parent classes are inherited.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
#
|
||||
# before_save :prepare
|
||||
# def prepare
|
||||
# puts "preparing save"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :saving_message
|
||||
# def saving_message
|
||||
# puts "saving..."
|
||||
# end
|
||||
#
|
||||
# after_save do |object|
|
||||
# puts "saved"
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# run_callbacks(:before_save)
|
||||
# puts "- save"
|
||||
# run_callbacks(:after_save)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# preparing save
|
||||
# saving...
|
||||
# - save
|
||||
# saved
|
||||
module Callbacks
|
||||
class CallbackChain < Array
|
||||
def self.build(kind, *methods, &block)
|
||||
methods, options = extract_options(*methods, &block)
|
||||
methods.map! { |method| Callback.new(kind, method, options) }
|
||||
new(methods)
|
||||
end
|
||||
extend Concern
|
||||
|
||||
def run(object, options = {}, &terminator)
|
||||
enumerator = options[:enumerator] || :each
|
||||
|
||||
unless block_given?
|
||||
send(enumerator) { |callback| callback.call(object) }
|
||||
else
|
||||
send(enumerator) do |callback|
|
||||
result = callback.call(object)
|
||||
break result if terminator.call(result, object)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Decompose into more Array like behavior
|
||||
def replace_or_append!(chain)
|
||||
if index = index(chain)
|
||||
self[index] = chain
|
||||
else
|
||||
self << chain
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
def find(callback, &block)
|
||||
select { |c| c == callback && (!block_given? || yield(c)) }.first
|
||||
end
|
||||
|
||||
def delete(callback)
|
||||
super(callback.is_a?(Callback) ? callback : find(callback))
|
||||
end
|
||||
|
||||
private
|
||||
def self.extract_options(*methods, &block)
|
||||
methods.flatten!
|
||||
options = methods.extract_options!
|
||||
methods << block if block_given?
|
||||
return methods, options
|
||||
end
|
||||
|
||||
def extract_options(*methods, &block)
|
||||
self.class.extract_options(*methods, &block)
|
||||
end
|
||||
included do
|
||||
extend ActiveSupport::DescendantsTracker
|
||||
end
|
||||
|
||||
class Callback
|
||||
attr_reader :kind, :method, :identifier, :options
|
||||
# Runs the callbacks for the given event.
|
||||
#
|
||||
# Calls the before and around callbacks in the order they were set, yields
|
||||
# the block (if given one), and then runs the after callbacks in reverse order.
|
||||
# Optionally accepts a key, which will be used to compile an optimized callback
|
||||
# method for each key. See +ClassMethods.define_callbacks+ for more information.
|
||||
#
|
||||
# If the callback chain was halted, returns +false+. Otherwise returns the result
|
||||
# of the block, or +true+ if no block is given.
|
||||
#
|
||||
# run_callbacks :save do
|
||||
# save
|
||||
# end
|
||||
#
|
||||
def run_callbacks(kind, *args, &block)
|
||||
send("_run_#{kind}_callbacks", *args, &block)
|
||||
end
|
||||
|
||||
def initialize(kind, method, options = {})
|
||||
@kind = kind
|
||||
@method = method
|
||||
@identifier = options[:identifier]
|
||||
@options = options
|
||||
private
|
||||
|
||||
# A hook invoked everytime a before callback is halted.
|
||||
# This can be overriden in AS::Callback implementors in order
|
||||
# to provide better debugging/logging.
|
||||
def halted_callback_hook(filter)
|
||||
end
|
||||
|
||||
class Callback #:nodoc:#
|
||||
@@_callback_sequence = 0
|
||||
|
||||
attr_accessor :chain, :filter, :kind, :options, :per_key, :klass, :raw_filter
|
||||
|
||||
def initialize(chain, filter, kind, options, klass)
|
||||
@chain, @kind, @klass = chain, kind, klass
|
||||
normalize_options!(options)
|
||||
|
||||
@per_key = options.delete(:per_key)
|
||||
@raw_filter, @options = filter, options
|
||||
@filter = _compile_filter(filter)
|
||||
@compiled_options = _compile_options(options)
|
||||
@callback_id = next_id
|
||||
|
||||
_compile_per_key_options
|
||||
end
|
||||
|
||||
def ==(other)
|
||||
case other
|
||||
when Callback
|
||||
(self.identifier && self.identifier == other.identifier) || self.method == other.method
|
||||
else
|
||||
(self.identifier && self.identifier == other) || self.method == other
|
||||
end
|
||||
def clone(chain, klass)
|
||||
obj = super()
|
||||
obj.chain = chain
|
||||
obj.klass = klass
|
||||
obj.per_key = @per_key.dup
|
||||
obj.options = @options.dup
|
||||
obj.per_key[:if] = @per_key[:if].dup
|
||||
obj.per_key[:unless] = @per_key[:unless].dup
|
||||
obj.options[:if] = @options[:if].dup
|
||||
obj.options[:unless] = @options[:unless].dup
|
||||
obj
|
||||
end
|
||||
|
||||
def eql?(other)
|
||||
self == other
|
||||
def normalize_options!(options)
|
||||
options[:if] = Array.wrap(options[:if])
|
||||
options[:unless] = Array.wrap(options[:unless])
|
||||
|
||||
options[:per_key] ||= {}
|
||||
options[:per_key][:if] = Array.wrap(options[:per_key][:if])
|
||||
options[:per_key][:unless] = Array.wrap(options[:per_key][:unless])
|
||||
end
|
||||
|
||||
def dup
|
||||
self.class.new(@kind, @method, @options.dup)
|
||||
def name
|
||||
chain.name
|
||||
end
|
||||
|
||||
def hash
|
||||
if @identifier
|
||||
@identifier.hash
|
||||
else
|
||||
@method.hash
|
||||
end
|
||||
def next_id
|
||||
@@_callback_sequence += 1
|
||||
end
|
||||
|
||||
def call(*args, &block)
|
||||
evaluate_method(method, *args, &block) if should_run_callback?(*args)
|
||||
rescue LocalJumpError
|
||||
raise ArgumentError,
|
||||
"Cannot yield from a Proc type filter. The Proc must take two " +
|
||||
"arguments and execute #call on the second argument."
|
||||
def matches?(_kind, _filter)
|
||||
@kind == _kind && @filter == _filter
|
||||
end
|
||||
|
||||
private
|
||||
def evaluate_method(method, *args, &block)
|
||||
case method
|
||||
when Symbol
|
||||
object = args.shift
|
||||
object.send(method, *args, &block)
|
||||
when String
|
||||
eval(method, args.first.instance_eval { binding })
|
||||
when Proc, Method
|
||||
method.call(*args, &block)
|
||||
else
|
||||
if method.respond_to?(kind)
|
||||
method.send(kind, *args, &block)
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Callbacks must be a symbol denoting the method to call, a string to be evaluated, " +
|
||||
"a block to be invoked, or an object responding to the callback method."
|
||||
def _update_filter(filter_options, new_options)
|
||||
filter_options[:if].push(new_options[:unless]) if new_options.key?(:unless)
|
||||
filter_options[:unless].push(new_options[:if]) if new_options.key?(:if)
|
||||
end
|
||||
|
||||
def recompile!(_options, _per_key)
|
||||
_update_filter(self.options, _options)
|
||||
_update_filter(self.per_key, _per_key)
|
||||
|
||||
@callback_id = next_id
|
||||
@filter = _compile_filter(@raw_filter)
|
||||
@compiled_options = _compile_options(@options)
|
||||
_compile_per_key_options
|
||||
end
|
||||
|
||||
def _compile_per_key_options
|
||||
key_options = _compile_options(@per_key)
|
||||
|
||||
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def _one_time_conditions_valid_#{@callback_id}?
|
||||
true if #{key_options}
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
|
||||
# This will supply contents for before and around filters, and no
|
||||
# contents for after filters (for the forward pass).
|
||||
def start(key=nil, object=nil)
|
||||
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
||||
|
||||
# options[0] is the compiled form of supplied conditions
|
||||
# options[1] is the "end" for the conditional
|
||||
#
|
||||
case @kind
|
||||
when :before
|
||||
# if condition # before_save :filter_name, :if => :condition
|
||||
# filter_name
|
||||
# end
|
||||
<<-RUBY_EVAL
|
||||
if !halted && #{@compiled_options}
|
||||
# This double assignment is to prevent warnings in 1.9.3 as
|
||||
# the `result` variable is not always used except if the
|
||||
# terminator code refers to it.
|
||||
result = result = #{@filter}
|
||||
halted = (#{chain.config[:terminator]})
|
||||
if halted
|
||||
halted_callback_hook(#{@raw_filter.inspect.inspect})
|
||||
end
|
||||
end
|
||||
RUBY_EVAL
|
||||
when :around
|
||||
# Compile around filters with conditions into proxy methods
|
||||
# that contain the conditions.
|
||||
#
|
||||
# For `around_save :filter_name, :if => :condition':
|
||||
#
|
||||
# def _conditional_callback_save_17
|
||||
# if condition
|
||||
# filter_name do
|
||||
# yield self
|
||||
# end
|
||||
# else
|
||||
# yield self
|
||||
# end
|
||||
# end
|
||||
#
|
||||
name = "_conditional_callback_#{@kind}_#{next_id}"
|
||||
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{name}(halted)
|
||||
if #{@compiled_options} && !halted
|
||||
#{@filter} do
|
||||
yield self
|
||||
end
|
||||
else
|
||||
yield self
|
||||
end
|
||||
end
|
||||
RUBY_EVAL
|
||||
"#{name}(halted) do"
|
||||
end
|
||||
end
|
||||
|
||||
# This will supply contents for around and after filters, but not
|
||||
# before filters (for the backward pass).
|
||||
def end(key=nil, object=nil)
|
||||
return if key && !object.send("_one_time_conditions_valid_#{@callback_id}?")
|
||||
|
||||
case @kind
|
||||
when :after
|
||||
# after_save :filter_name, :if => :condition
|
||||
<<-RUBY_EVAL
|
||||
if #{@compiled_options}
|
||||
#{@filter}
|
||||
end
|
||||
RUBY_EVAL
|
||||
when :around
|
||||
<<-RUBY_EVAL
|
||||
value
|
||||
end
|
||||
RUBY_EVAL
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Options support the same options as filters themselves (and support
|
||||
# symbols, string, procs, and objects), so compile a conditional
|
||||
# expression based on the options
|
||||
def _compile_options(options)
|
||||
conditions = ["true"]
|
||||
|
||||
unless options[:if].empty?
|
||||
conditions << Array.wrap(_compile_filter(options[:if]))
|
||||
end
|
||||
|
||||
unless options[:unless].empty?
|
||||
conditions << Array.wrap(_compile_filter(options[:unless])).map {|f| "!#{f}"}
|
||||
end
|
||||
|
||||
conditions.flatten.join(" && ")
|
||||
end
|
||||
|
||||
# Filters support:
|
||||
#
|
||||
# Arrays:: Used in conditions. This is used to specify
|
||||
# multiple conditions. Used internally to
|
||||
# merge conditions from skip_* filters
|
||||
# Symbols:: A method to call
|
||||
# Strings:: Some content to evaluate
|
||||
# Procs:: A proc to call with the object
|
||||
# Objects:: An object with a before_foo method on it to call
|
||||
#
|
||||
# All of these objects are compiled into methods and handled
|
||||
# the same after this point:
|
||||
#
|
||||
# Arrays:: Merged together into a single filter
|
||||
# Symbols:: Already methods
|
||||
# Strings:: class_eval'ed into methods
|
||||
# Procs:: define_method'ed into methods
|
||||
# Objects::
|
||||
# a method is created that calls the before_foo method
|
||||
# on the object.
|
||||
#
|
||||
def _compile_filter(filter)
|
||||
method_name = "_callback_#{@kind}_#{next_id}"
|
||||
case filter
|
||||
when Array
|
||||
filter.map {|f| _compile_filter(f)}
|
||||
when Symbol
|
||||
filter
|
||||
when String
|
||||
"(#{filter})"
|
||||
when Proc
|
||||
@klass.send(:define_method, method_name, &filter)
|
||||
return method_name if filter.arity <= 0
|
||||
|
||||
method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
|
||||
else
|
||||
@klass.send(:define_method, "#{method_name}_object") { filter }
|
||||
|
||||
_normalize_legacy_filter(kind, filter)
|
||||
scopes = Array.wrap(chain.config[:scope])
|
||||
method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
|
||||
|
||||
@klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{method_name}(&blk)
|
||||
#{method_name}_object.send(:#{method_to_call}, self, &blk)
|
||||
end
|
||||
RUBY_EVAL
|
||||
|
||||
method_name
|
||||
end
|
||||
end
|
||||
|
||||
def _normalize_legacy_filter(kind, filter)
|
||||
if !filter.respond_to?(kind) && filter.respond_to?(:filter)
|
||||
filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{kind}(context, &block) filter(context, &block) end
|
||||
RUBY_EVAL
|
||||
elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around
|
||||
def filter.around(context)
|
||||
should_continue = before(context)
|
||||
yield if should_continue
|
||||
after(context)
|
||||
end
|
||||
end
|
||||
|
||||
def should_run_callback?(*args)
|
||||
[options[:if]].flatten.compact.all? { |a| evaluate_method(a, *args) } &&
|
||||
![options[:unless]].flatten.compact.any? { |a| evaluate_method(a, *args) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(base)
|
||||
base.extend ClassMethods
|
||||
# An Array with a compile method
|
||||
class CallbackChain < Array #:nodoc:#
|
||||
attr_reader :name, :config
|
||||
|
||||
def initialize(name, config)
|
||||
@name = name
|
||||
@config = {
|
||||
:terminator => "false",
|
||||
:rescuable => false,
|
||||
:scope => [ :kind ]
|
||||
}.merge(config)
|
||||
end
|
||||
|
||||
def compile(key=nil, object=nil)
|
||||
method = []
|
||||
method << "value = nil"
|
||||
method << "halted = false"
|
||||
|
||||
each do |callback|
|
||||
method << callback.start(key, object)
|
||||
end
|
||||
|
||||
if config[:rescuable]
|
||||
method << "rescued_error = nil"
|
||||
method << "begin"
|
||||
end
|
||||
|
||||
method << "value = yield if block_given? && !halted"
|
||||
|
||||
if config[:rescuable]
|
||||
method << "rescue Exception => e"
|
||||
method << "rescued_error = e"
|
||||
method << "end"
|
||||
end
|
||||
|
||||
reverse_each do |callback|
|
||||
method << callback.end(key, object)
|
||||
end
|
||||
|
||||
method << "raise rescued_error if rescued_error" if config[:rescuable]
|
||||
method << "halted ? false : (block_given? ? value : true)"
|
||||
method.compact.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Generate the internal runner method called by +run_callbacks+.
|
||||
def __define_runner(symbol) #:nodoc:
|
||||
runner_method = "_run_#{symbol}_callbacks"
|
||||
unless private_method_defined?(runner_method)
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{runner_method}(key = nil, &blk)
|
||||
self.class.__run_callback(key, :#{symbol}, self, &blk)
|
||||
end
|
||||
private :#{runner_method}
|
||||
RUBY_EVAL
|
||||
end
|
||||
end
|
||||
|
||||
# This method calls the callback method for the given key.
|
||||
# If this called first time it creates a new callback method for the key,
|
||||
# calculating which callbacks can be omitted because of per_key conditions.
|
||||
#
|
||||
def __run_callback(key, kind, object, &blk) #:nodoc:
|
||||
name = __callback_runner_name(key, kind)
|
||||
unless object.respond_to?(name, true)
|
||||
str = object.send("_#{kind}_callbacks").compile(key, object)
|
||||
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
||||
def #{name}() #{str} end
|
||||
protected :#{name}
|
||||
RUBY_EVAL
|
||||
end
|
||||
object.send(name, &blk)
|
||||
end
|
||||
|
||||
def __reset_runner(symbol)
|
||||
name = __callback_runner_name(nil, symbol)
|
||||
undef_method(name) if method_defined?(name)
|
||||
end
|
||||
|
||||
def __callback_runner_name(key, kind)
|
||||
"_run__#{self.name.hash.abs}__#{kind}__#{key.hash.abs}__callbacks"
|
||||
end
|
||||
|
||||
# This is used internally to append, prepend and skip callbacks to the
|
||||
# CallbackChain.
|
||||
#
|
||||
def __update_callbacks(name, filters = [], block = nil) #:nodoc:
|
||||
type = filters.first.in?([:before, :after, :around]) ? filters.shift : :before
|
||||
options = filters.last.is_a?(Hash) ? filters.pop : {}
|
||||
filters.unshift(block) if block
|
||||
|
||||
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
|
||||
chain = target.send("_#{name}_callbacks")
|
||||
yield target, chain.dup, type, filters, options
|
||||
target.__reset_runner(name)
|
||||
end
|
||||
end
|
||||
|
||||
# Install a callback for the given event.
|
||||
#
|
||||
# set_callback :save, :before, :before_meth
|
||||
# set_callback :save, :after, :after_meth, :if => :condition
|
||||
# set_callback :save, :around, lambda { |r| stuff; result = yield; stuff }
|
||||
#
|
||||
# The second arguments indicates whether the callback is to be run +:before+,
|
||||
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
|
||||
# means the first example above can also be written as:
|
||||
#
|
||||
# set_callback :save, :before_meth
|
||||
#
|
||||
# The callback can specified as a symbol naming an instance method; as a proc,
|
||||
# lambda, or block; as a string to be instance evaluated; or as an object that
|
||||
# responds to a certain method determined by the <tt>:scope</tt> argument to
|
||||
# +define_callback+.
|
||||
#
|
||||
# If a proc, lambda, or block is given, its body is evaluated in the context
|
||||
# of the current object. It can also optionally accept the current object as
|
||||
# an argument.
|
||||
#
|
||||
# Before and around callbacks are called in the order that they are set; after
|
||||
# callbacks are called in the reverse order.
|
||||
#
|
||||
# Around callbacks can access the return value from the event, if it
|
||||
# wasn't halted, from the +yield+ call.
|
||||
#
|
||||
# ===== Options
|
||||
#
|
||||
# * <tt>:if</tt> - A symbol naming an instance method or a proc; the callback
|
||||
# will be called only when it returns a true value.
|
||||
# * <tt>:unless</tt> - A symbol naming an instance method or a proc; the callback
|
||||
# will be called only when it returns a false value.
|
||||
# * <tt>:prepend</tt> - If true, the callback will be prepended to the existing
|
||||
# chain rather than appended.
|
||||
# * <tt>:per_key</tt> - A hash with <tt>:if</tt> and <tt>:unless</tt> options;
|
||||
# see "Per-key conditions" below.
|
||||
#
|
||||
# ===== Per-key conditions
|
||||
#
|
||||
# When creating or skipping callbacks, you can specify conditions that
|
||||
# are always the same for a given key. For instance, in Action Pack,
|
||||
# we convert :only and :except conditions into per-key conditions.
|
||||
#
|
||||
# before_filter :authenticate, :except => "index"
|
||||
#
|
||||
# becomes
|
||||
#
|
||||
# set_callback :process_action, :before, :authenticate, :per_key => {:unless => proc {|c| c.action_name == "index"}}
|
||||
#
|
||||
# Per-key conditions are evaluated only once per use of a given key.
|
||||
# In the case of the above example, you would do:
|
||||
#
|
||||
# run_callbacks(:process_action, action_name) { ... dispatch stuff ... }
|
||||
#
|
||||
# In that case, each action_name would get its own compiled callback
|
||||
# method that took into consideration the per_key conditions. This
|
||||
# is a speed improvement for ActionPack.
|
||||
#
|
||||
def set_callback(name, *filter_list, &block)
|
||||
mapped = nil
|
||||
|
||||
__update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
|
||||
mapped ||= filters.map do |filter|
|
||||
Callback.new(chain, filter, type, options.dup, self)
|
||||
end
|
||||
|
||||
filters.each do |filter|
|
||||
chain.delete_if {|c| c.matches?(type, filter) }
|
||||
end
|
||||
|
||||
options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped)
|
||||
|
||||
target.send("_#{name}_callbacks=", chain)
|
||||
end
|
||||
end
|
||||
|
||||
# Skip a previously set callback. Like +set_callback+, <tt>:if</tt> or <tt>:unless</tt>
|
||||
# options may be passed in order to control when the callback is skipped.
|
||||
#
|
||||
# class Writer < Person
|
||||
# skip_callback :validate, :before, :check_membership, :if => lambda { self.age > 18 }
|
||||
# end
|
||||
#
|
||||
def skip_callback(name, *filter_list, &block)
|
||||
__update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
|
||||
filters.each do |filter|
|
||||
filter = chain.find {|c| c.matches?(type, filter) }
|
||||
|
||||
if filter && options.any?
|
||||
new_filter = filter.clone(chain, self)
|
||||
chain.insert(chain.index(filter), new_filter)
|
||||
new_filter.recompile!(options, options[:per_key] || {})
|
||||
end
|
||||
|
||||
chain.delete(filter)
|
||||
end
|
||||
target.send("_#{name}_callbacks=", chain)
|
||||
end
|
||||
end
|
||||
|
||||
# Remove all set callbacks for the given event.
|
||||
#
|
||||
def reset_callbacks(symbol)
|
||||
callbacks = send("_#{symbol}_callbacks")
|
||||
|
||||
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
|
||||
chain = target.send("_#{symbol}_callbacks").dup
|
||||
callbacks.each { |c| chain.delete(c) }
|
||||
target.send("_#{symbol}_callbacks=", chain)
|
||||
target.__reset_runner(symbol)
|
||||
end
|
||||
|
||||
self.send("_#{symbol}_callbacks=", callbacks.dup.clear)
|
||||
|
||||
__reset_runner(symbol)
|
||||
end
|
||||
|
||||
# Define sets of events in the object lifecycle that support callbacks.
|
||||
#
|
||||
# define_callbacks :validate
|
||||
# define_callbacks :initialize, :save, :destroy
|
||||
#
|
||||
# ===== Options
|
||||
#
|
||||
# * <tt>:terminator</tt> - Determines when a before filter will halt the callback
|
||||
# chain, preventing following callbacks from being called and the event from being
|
||||
# triggered. This is a string to be eval'ed. The result of the callback is available
|
||||
# in the <tt>result</tt> variable.
|
||||
#
|
||||
# define_callbacks :validate, :terminator => "result == false"
|
||||
#
|
||||
# In this example, if any before validate callbacks returns +false+,
|
||||
# other callbacks are not executed. Defaults to "false", meaning no value
|
||||
# halts the chain.
|
||||
#
|
||||
# * <tt>:rescuable</tt> - By default, after filters are not executed if
|
||||
# the given block or a before filter raises an error. By setting this option
|
||||
# to <tt>true</tt> exception raised by given block is stored and after
|
||||
# executing all the after callbacks the stored exception is raised.
|
||||
#
|
||||
# * <tt>:scope</tt> - Indicates which methods should be executed when an object
|
||||
# is used as a callback.
|
||||
#
|
||||
# class Audit
|
||||
# def before(caller)
|
||||
# puts 'Audit: before is called'
|
||||
# end
|
||||
#
|
||||
# def before_save(caller)
|
||||
# puts 'Audit: before_save is called'
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class Account
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :save
|
||||
# set_callback :save, :before, Audit.new
|
||||
#
|
||||
# def save
|
||||
# run_callbacks :save do
|
||||
# puts 'save in main'
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# In the above case whenever you save an account the method <tt>Audit#before</tt> will
|
||||
# be called. On the other hand
|
||||
#
|
||||
# define_callbacks :save, :scope => [:kind, :name]
|
||||
#
|
||||
# would trigger <tt>Audit#before_save</tt> instead. That's constructed by calling
|
||||
# <tt>#{kind}_#{name}</tt> on the given instance. In this case "kind" is "before" and
|
||||
# "name" is "save". In this context +:kind+ and +:name+ have special meanings: +:kind+
|
||||
# refers to the kind of callback (before/after/around) and +:name+ refers to the
|
||||
# method on which callbacks are being defined.
|
||||
#
|
||||
# A declaration like
|
||||
#
|
||||
# define_callbacks :save, :scope => [:name]
|
||||
#
|
||||
# would call <tt>Audit#save</tt>.
|
||||
#
|
||||
def define_callbacks(*callbacks)
|
||||
config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
|
||||
callbacks.each do |callback|
|
||||
class_eval <<-"end_eval"
|
||||
def self.#{callback}(*methods, &block) # def self.before_save(*methods, &block)
|
||||
callbacks = CallbackChain.build(:#{callback}, *methods, &block) # callbacks = CallbackChain.build(:before_save, *methods, &block)
|
||||
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
|
||||
@#{callback}_callbacks.concat callbacks # @before_save_callbacks.concat callbacks
|
||||
end # end
|
||||
#
|
||||
def self.#{callback}_callback_chain # def self.before_save_callback_chain
|
||||
@#{callback}_callbacks ||= CallbackChain.new # @before_save_callbacks ||= CallbackChain.new
|
||||
#
|
||||
if superclass.respond_to?(:#{callback}_callback_chain) # if superclass.respond_to?(:before_save_callback_chain)
|
||||
CallbackChain.new( # CallbackChain.new(
|
||||
superclass.#{callback}_callback_chain + # superclass.before_save_callback_chain +
|
||||
@#{callback}_callbacks # @before_save_callbacks
|
||||
) # )
|
||||
else # else
|
||||
@#{callback}_callbacks # @before_save_callbacks
|
||||
end # end
|
||||
end # end
|
||||
end_eval
|
||||
class_attribute "_#{callback}_callbacks"
|
||||
send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
|
||||
__define_runner(callback)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Runs all the callbacks defined for the given options.
|
||||
#
|
||||
# If a block is given it will be called after each callback receiving as arguments:
|
||||
#
|
||||
# * the result from the callback
|
||||
# * the object which has the callback
|
||||
#
|
||||
# If the result from the block evaluates to false, the callback chain is stopped.
|
||||
#
|
||||
# Example:
|
||||
# class Storage
|
||||
# include ActiveSupport::Callbacks
|
||||
#
|
||||
# define_callbacks :before_save, :after_save
|
||||
# end
|
||||
#
|
||||
# class ConfigStorage < Storage
|
||||
# before_save :pass
|
||||
# before_save :pass
|
||||
# before_save :stop
|
||||
# before_save :pass
|
||||
#
|
||||
# def pass
|
||||
# puts "pass"
|
||||
# end
|
||||
#
|
||||
# def stop
|
||||
# puts "stop"
|
||||
# return false
|
||||
# end
|
||||
#
|
||||
# def save
|
||||
# result = run_callbacks(:before_save) { |result, object| result == false }
|
||||
# puts "- save" if result
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# config = ConfigStorage.new
|
||||
# config.save
|
||||
#
|
||||
# Output:
|
||||
# pass
|
||||
# pass
|
||||
# stop
|
||||
def run_callbacks(kind, options = {}, &block)
|
||||
self.class.send("#{kind}_callback_chain").run(self, options, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'active_support/deprecation'
|
||||
|
||||
module ActiveSupport
|
||||
# A typical module looks like this:
|
||||
#
|
||||
@@ -5,7 +7,7 @@ module ActiveSupport
|
||||
# def self.included(base)
|
||||
# base.extend ClassMethods
|
||||
# base.class_eval do
|
||||
# scope :disabled, -> { where(disabled: true) }
|
||||
# scope :disabled, where(:disabled => true)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
@@ -14,8 +16,7 @@ module ActiveSupport
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be
|
||||
# written as:
|
||||
# By using <tt>ActiveSupport::Concern</tt> the above module could instead be written as:
|
||||
#
|
||||
# require 'active_support/concern'
|
||||
#
|
||||
@@ -23,7 +24,7 @@ module ActiveSupport
|
||||
# extend ActiveSupport::Concern
|
||||
#
|
||||
# included do
|
||||
# scope :disabled, -> { where(disabled: true) }
|
||||
# scope :disabled, where(:disabled => true)
|
||||
# end
|
||||
#
|
||||
# module ClassMethods
|
||||
@@ -31,9 +32,8 @@ module ActiveSupport
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module
|
||||
# and a +Bar+ module which depends on the former, we would typically write the
|
||||
# following:
|
||||
# Moreover, it gracefully handles module dependencies. Given a +Foo+ module and a +Bar+
|
||||
# module which depends on the former, we would typically write the following:
|
||||
#
|
||||
# module Foo
|
||||
# def self.included(base)
|
||||
@@ -56,11 +56,11 @@ module ActiveSupport
|
||||
# include Bar # Bar is the module that Host really needs
|
||||
# end
|
||||
#
|
||||
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
|
||||
# could try to hide these from +Host+ directly including +Foo+ in +Bar+:
|
||||
# But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We could try to hide
|
||||
# these from +Host+ directly including +Foo+ in +Bar+:
|
||||
#
|
||||
# module Bar
|
||||
# include Foo
|
||||
# include Foo
|
||||
# def self.included(base)
|
||||
# base.method_injected_by_foo
|
||||
# end
|
||||
@@ -70,17 +70,18 @@ module ActiveSupport
|
||||
# include Bar
|
||||
# end
|
||||
#
|
||||
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
|
||||
# is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
|
||||
# module dependencies are properly resolved:
|
||||
# Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt> is the +Bar+ module,
|
||||
# not the +Host+ class. With <tt>ActiveSupport::Concern</tt>, module dependencies are properly resolved:
|
||||
#
|
||||
# require 'active_support/concern'
|
||||
#
|
||||
# module Foo
|
||||
# extend ActiveSupport::Concern
|
||||
# included do
|
||||
# def self.method_injected_by_foo
|
||||
# ...
|
||||
# class_eval do
|
||||
# def self.method_injected_by_foo
|
||||
# ...
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
@@ -97,38 +98,36 @@ module ActiveSupport
|
||||
# class Host
|
||||
# include Bar # works, Bar takes care now of its dependencies
|
||||
# end
|
||||
#
|
||||
module Concern
|
||||
class MultipleIncludedBlocks < StandardError #:nodoc:
|
||||
def initialize
|
||||
super "Cannot define multiple 'included' blocks for a Concern"
|
||||
end
|
||||
end
|
||||
|
||||
def self.extended(base) #:nodoc:
|
||||
base.instance_variable_set(:@_dependencies, [])
|
||||
def self.extended(base)
|
||||
base.instance_variable_set("@_dependencies", [])
|
||||
end
|
||||
|
||||
def append_features(base)
|
||||
if base.instance_variable_defined?(:@_dependencies)
|
||||
base.instance_variable_get(:@_dependencies) << self
|
||||
if base.instance_variable_defined?("@_dependencies")
|
||||
base.instance_variable_get("@_dependencies") << self
|
||||
return false
|
||||
else
|
||||
return false if base < self
|
||||
@_dependencies.each { |dep| base.send(:include, dep) }
|
||||
super
|
||||
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
|
||||
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
|
||||
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
|
||||
if const_defined?("InstanceMethods")
|
||||
base.send :include, const_get("InstanceMethods")
|
||||
ActiveSupport::Deprecation.warn "The InstanceMethods module inside ActiveSupport::Concern will be " \
|
||||
"no longer included automatically. Please define instance methods directly in #{self} instead.", caller
|
||||
end
|
||||
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
|
||||
end
|
||||
end
|
||||
|
||||
def included(base = nil, &block)
|
||||
if base.nil?
|
||||
raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
|
||||
|
||||
@_included_block = block
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
90
activesupport/lib/active_support/configurable.rb
Normal file
90
activesupport/lib/active_support/configurable.rb
Normal file
@@ -0,0 +1,90 @@
|
||||
require 'active_support/concern'
|
||||
require 'active_support/ordered_options'
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/module/delegation'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
|
||||
module ActiveSupport
|
||||
# Configurable provides a <tt>config</tt> method to store and retrieve
|
||||
# configuration options as an <tt>OrderedHash</tt>.
|
||||
module Configurable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class Configuration < ActiveSupport::InheritableOptions
|
||||
def compile_methods!
|
||||
self.class.compile_methods!(keys)
|
||||
end
|
||||
|
||||
# compiles reader methods so we don't have to go through method_missing
|
||||
def self.compile_methods!(keys)
|
||||
keys.reject { |m| method_defined?(m) }.each do |key|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def #{key}; _get(#{key.inspect}); end
|
||||
RUBY
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def config
|
||||
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
|
||||
superclass.config.inheritable_copy
|
||||
else
|
||||
# create a new "anonymous" class that will host the compiled reader methods
|
||||
Class.new(Configuration).new
|
||||
end
|
||||
end
|
||||
|
||||
def configure
|
||||
yield config
|
||||
end
|
||||
|
||||
# Allows you to add shortcut so that you don't have to refer to attribute through config.
|
||||
# Also look at the example for config to contrast.
|
||||
#
|
||||
# class User
|
||||
# include ActiveSupport::Configurable
|
||||
# config_accessor :allowed_access
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
# user.allowed_access = true
|
||||
# user.allowed_access # => true
|
||||
#
|
||||
def config_accessor(*names)
|
||||
options = names.extract_options!
|
||||
|
||||
names.each do |name|
|
||||
reader, line = "def #{name}; config.#{name}; end", __LINE__
|
||||
writer, line = "def #{name}=(value); config.#{name} = value; end", __LINE__
|
||||
|
||||
singleton_class.class_eval reader, __FILE__, line
|
||||
singleton_class.class_eval writer, __FILE__, line
|
||||
class_eval reader, __FILE__, line unless options[:instance_reader] == false
|
||||
class_eval writer, __FILE__, line unless options[:instance_writer] == false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Reads and writes attributes from a configuration <tt>OrderedHash</tt>.
|
||||
#
|
||||
# require 'active_support/configurable'
|
||||
#
|
||||
# class User
|
||||
# include ActiveSupport::Configurable
|
||||
# end
|
||||
#
|
||||
# user = User.new
|
||||
#
|
||||
# user.config.allowed_access = true
|
||||
# user.config.level = 1
|
||||
#
|
||||
# user.config.allowed_access # => true
|
||||
# user.config.level # => 1
|
||||
#
|
||||
def config
|
||||
@_config ||= self.class.config.inheritable_copy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
filenames = Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.map do |path|
|
||||
File.basename(path, '.rb')
|
||||
Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path|
|
||||
require "active_support/core_ext/#{File.basename(path, '.rb')}"
|
||||
end
|
||||
|
||||
# deprecated
|
||||
filenames -= %w(blank)
|
||||
|
||||
filenames.each { |filename| require "active_support/core_ext/#{filename}" }
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/array/access'
|
||||
require 'active_support/core_ext/array/uniq_by'
|
||||
require 'active_support/core_ext/array/conversions'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
require 'active_support/core_ext/array/grouping'
|
||||
require 'active_support/core_ext/array/random_access'
|
||||
require 'active_support/core_ext/array/wrapper'
|
||||
|
||||
class Array #:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Array::Access
|
||||
include ActiveSupport::CoreExtensions::Array::Conversions
|
||||
include ActiveSupport::CoreExtensions::Array::ExtractOptions
|
||||
include ActiveSupport::CoreExtensions::Array::Grouping
|
||||
include ActiveSupport::CoreExtensions::Array::RandomAccess
|
||||
extend ActiveSupport::CoreExtensions::Array::Wrapper
|
||||
end
|
||||
require 'active_support/core_ext/array/prepend_and_append'
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
# Makes it easier to access parts of an array.
|
||||
module Access
|
||||
# Returns the tail of the array from +position+.
|
||||
#
|
||||
# %w( a b c d ).from(0) # => %w( a b c d )
|
||||
# %w( a b c d ).from(2) # => %w( c d )
|
||||
# %w( a b c d ).from(10) # => nil
|
||||
# %w().from(0) # => nil
|
||||
def from(position)
|
||||
self[position..-1]
|
||||
end
|
||||
|
||||
# Returns the beginning of the array up to +position+.
|
||||
#
|
||||
# %w( a b c d ).to(0) # => %w( a )
|
||||
# %w( a b c d ).to(2) # => %w( a b c )
|
||||
# %w( a b c d ).to(10) # => %w( a b c d )
|
||||
# %w().to(0) # => %w()
|
||||
def to(position)
|
||||
self[0..position]
|
||||
end
|
||||
class Array
|
||||
# Returns the tail of the array from +position+.
|
||||
#
|
||||
# %w( a b c d ).from(0) # => %w( a b c d )
|
||||
# %w( a b c d ).from(2) # => %w( c d )
|
||||
# %w( a b c d ).from(10) # => %w()
|
||||
# %w().from(0) # => %w()
|
||||
def from(position)
|
||||
self[position, length] || []
|
||||
end
|
||||
|
||||
# Equal to <tt>self[1]</tt>.
|
||||
def second
|
||||
self[1]
|
||||
end
|
||||
# Returns the beginning of the array up to +position+.
|
||||
#
|
||||
# %w( a b c d ).to(0) # => %w( a )
|
||||
# %w( a b c d ).to(2) # => %w( a b c )
|
||||
# %w( a b c d ).to(10) # => %w( a b c d )
|
||||
# %w().to(0) # => %w()
|
||||
def to(position)
|
||||
self.first position + 1
|
||||
end
|
||||
|
||||
# Equal to <tt>self[2]</tt>.
|
||||
def third
|
||||
self[2]
|
||||
end
|
||||
# Equal to <tt>self[1]</tt>.
|
||||
def second
|
||||
self[1]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[3]</tt>.
|
||||
def fourth
|
||||
self[3]
|
||||
end
|
||||
# Equal to <tt>self[2]</tt>.
|
||||
def third
|
||||
self[2]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[4]</tt>.
|
||||
def fifth
|
||||
self[4]
|
||||
end
|
||||
# Equal to <tt>self[3]</tt>.
|
||||
def fourth
|
||||
self[3]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
||||
def forty_two
|
||||
self[41]
|
||||
end
|
||||
end
|
||||
end
|
||||
# Equal to <tt>self[4]</tt>.
|
||||
def fifth
|
||||
self[4]
|
||||
end
|
||||
|
||||
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
|
||||
def forty_two
|
||||
self[41]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,197 +1,164 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Conversions
|
||||
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
|
||||
# * <tt>:words_connector</tt> - The sign or word used to join the elements in arrays with two or more elements (default: ", ")
|
||||
# * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ")
|
||||
# * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ")
|
||||
def to_sentence(options = {})
|
||||
default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale])
|
||||
default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale])
|
||||
default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
|
||||
require 'active_support/xml_mini'
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
require 'active_support/core_ext/hash/reverse_merge'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
# Try to emulate to_senteces previous to 2.3
|
||||
if options.has_key?(:connector) || options.has_key?(:skip_last_comma)
|
||||
::ActiveSupport::Deprecation.warn(":connector has been deprecated. Use :words_connector instead", caller) if options.has_key? :connector
|
||||
::ActiveSupport::Deprecation.warn(":skip_last_comma has been deprecated. Use :last_word_connector instead", caller) if options.has_key? :skip_last_comma
|
||||
class Array
|
||||
# Converts the array to a comma-separated sentence where the last element is joined by the connector word. Options:
|
||||
# * <tt>:words_connector</tt> - The sign or word used to join the elements in arrays with two or more elements (default: ", ")
|
||||
# * <tt>:two_words_connector</tt> - The sign or word used to join the elements in arrays with two elements (default: " and ")
|
||||
# * <tt>:last_word_connector</tt> - The sign or word used to join the last element in arrays with three or more elements (default: ", and ")
|
||||
def to_sentence(options = {})
|
||||
if defined?(I18n)
|
||||
default_words_connector = I18n.translate(:'support.array.words_connector', :locale => options[:locale])
|
||||
default_two_words_connector = I18n.translate(:'support.array.two_words_connector', :locale => options[:locale])
|
||||
default_last_word_connector = I18n.translate(:'support.array.last_word_connector', :locale => options[:locale])
|
||||
else
|
||||
default_words_connector = ", "
|
||||
default_two_words_connector = " and "
|
||||
default_last_word_connector = ", and "
|
||||
end
|
||||
|
||||
skip_last_comma = options.delete :skip_last_comma
|
||||
if connector = options.delete(:connector)
|
||||
options[:last_word_connector] ||= skip_last_comma ? connector : ", #{connector}"
|
||||
else
|
||||
options[:last_word_connector] ||= skip_last_comma ? default_two_words_connector : default_last_word_connector
|
||||
end
|
||||
end
|
||||
|
||||
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
|
||||
options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector
|
||||
|
||||
case length
|
||||
when 0
|
||||
""
|
||||
when 1
|
||||
self[0].to_s
|
||||
when 2
|
||||
"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
|
||||
else
|
||||
"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
|
||||
end
|
||||
end
|
||||
|
||||
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
|
||||
options.reverse_merge! :words_connector => default_words_connector, :two_words_connector => default_two_words_connector, :last_word_connector => default_last_word_connector
|
||||
|
||||
# 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
|
||||
|
||||
# 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%5B%5D=Rails&hobbies%5B%5D=coding"
|
||||
def to_query(key)
|
||||
prefix = "#{key}[]"
|
||||
collect { |value| value.to_query(prefix) }.join '&'
|
||||
end
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
end
|
||||
end
|
||||
|
||||
# Converts a collection of elements into a formatted string by calling
|
||||
# <tt>to_s</tt> on all elements and joining them:
|
||||
#
|
||||
# Blog.find(:all).to_formatted_s # => "First PostSecond PostThird Post"
|
||||
#
|
||||
# Adding in the <tt>:db</tt> argument as the format yields a prettier
|
||||
# output:
|
||||
#
|
||||
# Blog.find(:all).to_formatted_s(:db) # => "First Post,Second Post,Third Post"
|
||||
def to_formatted_s(format = :default)
|
||||
case format
|
||||
when :db
|
||||
if respond_to?(:empty?) && self.empty?
|
||||
"null"
|
||||
else
|
||||
collect { |element| element.id }.join(",")
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a string that represents this array in XML by sending +to_xml+
|
||||
# to each element. Active Record collections delegate their representation
|
||||
# in XML to this method.
|
||||
#
|
||||
# All elements are expected to respond to +to_xml+, if any of them does
|
||||
# not an exception is raised.
|
||||
#
|
||||
# The root node reflects the class name of the first element in plural
|
||||
# if all elements belong to the same type and that's not Hash:
|
||||
#
|
||||
# customer.projects.to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array">
|
||||
# <project>
|
||||
# <amount type="decimal">20000.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-09</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# <project>
|
||||
# <amount type="decimal">57230.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-15</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# </projects>
|
||||
#
|
||||
# Otherwise the root element is "records":
|
||||
#
|
||||
# [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <records type="array">
|
||||
# <record>
|
||||
# <bar type="integer">2</bar>
|
||||
# <foo type="integer">1</foo>
|
||||
# </record>
|
||||
# <record>
|
||||
# <baz type="integer">3</baz>
|
||||
# </record>
|
||||
# </records>
|
||||
#
|
||||
# If the collection is empty the root element is "nil-classes" by default:
|
||||
#
|
||||
# [].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <nil-classes type="array"/>
|
||||
#
|
||||
# To ensure a meaningful root element use the <tt>:root</tt> option:
|
||||
#
|
||||
# customer_with_no_projects.projects.to_xml(:root => "projects")
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array"/>
|
||||
#
|
||||
# By default root children have as node name the one of the root
|
||||
# singularized. You can change it with the <tt>:children</tt> option.
|
||||
#
|
||||
# The +options+ hash is passed downwards:
|
||||
#
|
||||
# Message.all.to_xml(:skip_types => true)
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <messages>
|
||||
# <message>
|
||||
# <created-at>2008-03-07T09:58:18+01:00</created-at>
|
||||
# <id>1</id>
|
||||
# <name>1</name>
|
||||
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
|
||||
# <user-id>1</user-id>
|
||||
# </message>
|
||||
# </messages>
|
||||
#
|
||||
def to_xml(options = {})
|
||||
raise "Not all elements respond to to_xml" unless all? { |e| e.respond_to? :to_xml }
|
||||
require 'builder' unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:root] ||= all? { |e| e.is_a?(first.class) && first.class.to_s != "Hash" } ? first.class.to_s.underscore.pluralize.tr('/', '-') : "records"
|
||||
options[:children] ||= options[:root].singularize
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
|
||||
root = options.delete(:root).to_s
|
||||
children = options.delete(:children)
|
||||
|
||||
if !options.has_key?(:dasherize) || options[:dasherize]
|
||||
root = root.dasherize
|
||||
end
|
||||
|
||||
options[:builder].instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
opts = options.merge({ :root => children })
|
||||
|
||||
xml = options[:builder]
|
||||
if empty?
|
||||
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"})
|
||||
else
|
||||
xml.tag!(root, options[:skip_types] ? {} : {:type => "array"}) {
|
||||
yield xml if block_given?
|
||||
each { |e| e.to_xml(opts.merge({ :skip_instruct => true })) }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
case length
|
||||
when 0
|
||||
""
|
||||
when 1
|
||||
self[0].to_s.dup
|
||||
when 2
|
||||
"#{self[0]}#{options[:two_words_connector]}#{self[1]}"
|
||||
else
|
||||
"#{self[0...-1].join(options[:words_connector])}#{options[:last_word_connector]}#{self[-1]}"
|
||||
end
|
||||
end
|
||||
|
||||
# Converts a collection of elements into a formatted string by calling
|
||||
# <tt>to_s</tt> on all elements and joining them:
|
||||
#
|
||||
# Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
|
||||
#
|
||||
# Adding in the <tt>:db</tt> argument as the format yields a comma separated
|
||||
# id list:
|
||||
#
|
||||
# Blog.all.to_formatted_s(:db) # => "1,2,3"
|
||||
def to_formatted_s(format = :default)
|
||||
case format
|
||||
when :db
|
||||
if respond_to?(:empty?) && self.empty?
|
||||
"null"
|
||||
else
|
||||
collect { |element| element.id }.join(",")
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Returns a string that represents the array in XML by invoking +to_xml+
|
||||
# on each element. Active Record collections delegate their representation
|
||||
# in XML to this method.
|
||||
#
|
||||
# All elements are expected to respond to +to_xml+, if any of them does
|
||||
# not then an exception is raised.
|
||||
#
|
||||
# The root node reflects the class name of the first element in plural
|
||||
# if all elements belong to the same type and that's not Hash:
|
||||
#
|
||||
# customer.projects.to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array">
|
||||
# <project>
|
||||
# <amount type="decimal">20000.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-09</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# <project>
|
||||
# <amount type="decimal">57230.0</amount>
|
||||
# <customer-id type="integer">1567</customer-id>
|
||||
# <deal-date type="date">2008-04-15</deal-date>
|
||||
# ...
|
||||
# </project>
|
||||
# </projects>
|
||||
#
|
||||
# Otherwise the root element is "records":
|
||||
#
|
||||
# [{:foo => 1, :bar => 2}, {:baz => 3}].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <records type="array">
|
||||
# <record>
|
||||
# <bar type="integer">2</bar>
|
||||
# <foo type="integer">1</foo>
|
||||
# </record>
|
||||
# <record>
|
||||
# <baz type="integer">3</baz>
|
||||
# </record>
|
||||
# </records>
|
||||
#
|
||||
# If the collection is empty the root element is "nil-classes" by default:
|
||||
#
|
||||
# [].to_xml
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <nil-classes type="array"/>
|
||||
#
|
||||
# To ensure a meaningful root element use the <tt>:root</tt> option:
|
||||
#
|
||||
# customer_with_no_projects.projects.to_xml(:root => "projects")
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <projects type="array"/>
|
||||
#
|
||||
# By default name of the node for the children of root is <tt>root.singularize</tt>.
|
||||
# You can change it with the <tt>:children</tt> option.
|
||||
#
|
||||
# The +options+ hash is passed downwards:
|
||||
#
|
||||
# Message.all.to_xml(:skip_types => true)
|
||||
#
|
||||
# <?xml version="1.0" encoding="UTF-8"?>
|
||||
# <messages>
|
||||
# <message>
|
||||
# <created-at>2008-03-07T09:58:18+01:00</created-at>
|
||||
# <id>1</id>
|
||||
# <name>1</name>
|
||||
# <updated-at>2008-03-07T09:58:18+01:00</updated-at>
|
||||
# <user-id>1</user-id>
|
||||
# </message>
|
||||
# </messages>
|
||||
#
|
||||
def to_xml(options = {})
|
||||
require 'active_support/builder' unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:indent] ||= 2
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
options[:root] ||= if first.class.to_s != "Hash" && all? { |e| e.is_a?(first.class) }
|
||||
underscored = ActiveSupport::Inflector.underscore(first.class.name)
|
||||
ActiveSupport::Inflector.pluralize(underscored).tr('/', '_')
|
||||
else
|
||||
"objects"
|
||||
end
|
||||
|
||||
builder = options[:builder]
|
||||
builder.instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
|
||||
children = options.delete(:children) || root.singularize
|
||||
|
||||
attributes = options[:skip_types] ? {} : {:type => "array"}
|
||||
return builder.tag!(root, attributes) if empty?
|
||||
|
||||
builder.__send__(:method_missing, root, attributes) do
|
||||
each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
|
||||
yield builder if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module ExtractOptions
|
||||
# Extracts options from a set of arguments. Removes and returns the last
|
||||
# element in the array if it's a hash, otherwise returns a blank hash.
|
||||
#
|
||||
# def options(*args)
|
||||
# args.extract_options!
|
||||
# end
|
||||
#
|
||||
# options(1, 2) # => {}
|
||||
# options(1, 2, :a => :b) # => {:a=>:b}
|
||||
def extract_options!
|
||||
last.is_a?(::Hash) ? pop : {}
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
# By default, only instances of Hash itself are extractable.
|
||||
# Subclasses of Hash may implement this method and return
|
||||
# true to declare themselves as extractable. If a Hash
|
||||
# is extractable, Array#extract_options! pops it from
|
||||
# the Array when it is the last element of the Array.
|
||||
def extractable_options?
|
||||
instance_of?(Hash)
|
||||
end
|
||||
end
|
||||
|
||||
class Array
|
||||
# Extracts options from a set of arguments. Removes and returns the last
|
||||
# element in the array if it's a hash, otherwise returns a blank hash.
|
||||
#
|
||||
# def options(*args)
|
||||
# args.extract_options!
|
||||
# end
|
||||
#
|
||||
# options(1, 2) # => {}
|
||||
# options(1, 2, :a => :b) # => {:a=>:b}
|
||||
def extract_options!
|
||||
if last.is_a?(Hash) && last.extractable_options?
|
||||
pop
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,106 +1,100 @@
|
||||
require 'enumerator'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Grouping
|
||||
# Splits or iterates over the array in groups of size +number+,
|
||||
# padding any remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", "6"]
|
||||
# ["7", nil, nil]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, ' ') {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", " "]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, false) {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3"]
|
||||
def in_groups_of(number, fill_with = nil)
|
||||
if fill_with == false
|
||||
collection = self
|
||||
else
|
||||
# size % number gives how many extra we have;
|
||||
# subtracting from number gives how many to add;
|
||||
# modulo number ensures we don't add group of just fill.
|
||||
padding = (number - size % number) % number
|
||||
collection = dup.concat([fill_with] * padding)
|
||||
end
|
||||
class Array
|
||||
# Splits or iterates over the array in groups of size +number+,
|
||||
# padding any remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups_of(3) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", "6"]
|
||||
# ["7", nil, nil]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, ' ') {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3", " "]
|
||||
#
|
||||
# %w(1 2 3).in_groups_of(2, false) {|group| p group}
|
||||
# ["1", "2"]
|
||||
# ["3"]
|
||||
def in_groups_of(number, fill_with = nil)
|
||||
if fill_with == false
|
||||
collection = self
|
||||
else
|
||||
# size % number gives how many extra we have;
|
||||
# subtracting from number gives how many to add;
|
||||
# modulo number ensures we don't add group of just fill.
|
||||
padding = (number - size % number) % number
|
||||
collection = dup.concat([fill_with] * padding)
|
||||
end
|
||||
|
||||
if block_given?
|
||||
collection.each_slice(number) { |slice| yield(slice) }
|
||||
else
|
||||
[].tap do |groups|
|
||||
collection.each_slice(number) { |group| groups << group }
|
||||
end
|
||||
end
|
||||
end
|
||||
if block_given?
|
||||
collection.each_slice(number) { |slice| yield(slice) }
|
||||
else
|
||||
groups = []
|
||||
collection.each_slice(number) { |group| groups << group }
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
||||
# Splits or iterates over the array in +number+ of groups, padding any
|
||||
# remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", nil]
|
||||
# ["8", "9", "10", nil]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", " "]
|
||||
# ["6", "7", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5"]
|
||||
# ["6", "7"]
|
||||
def in_groups(number, fill_with = nil)
|
||||
# size / number gives minor group size;
|
||||
# size % number gives how many objects need extra accomodation;
|
||||
# each group hold either division or division + 1 items.
|
||||
division = size / number
|
||||
modulo = size % number
|
||||
# Splits or iterates over the array in +number+ of groups, padding any
|
||||
# remaining slots with +fill_with+ unless it is +false+.
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7 8 9 10).in_groups(3) {|group| p group}
|
||||
# ["1", "2", "3", "4"]
|
||||
# ["5", "6", "7", nil]
|
||||
# ["8", "9", "10", nil]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, ' ') {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5", " "]
|
||||
# ["6", "7", " "]
|
||||
#
|
||||
# %w(1 2 3 4 5 6 7).in_groups(3, false) {|group| p group}
|
||||
# ["1", "2", "3"]
|
||||
# ["4", "5"]
|
||||
# ["6", "7"]
|
||||
def in_groups(number, fill_with = nil)
|
||||
# size / number gives minor group size;
|
||||
# size % number gives how many objects need extra accommodation;
|
||||
# each group hold either division or division + 1 items.
|
||||
division = size / number
|
||||
modulo = size % number
|
||||
|
||||
# create a new array avoiding dup
|
||||
groups = []
|
||||
start = 0
|
||||
# create a new array avoiding dup
|
||||
groups = []
|
||||
start = 0
|
||||
|
||||
number.times do |index|
|
||||
length = division + (modulo > 0 && modulo > index ? 1 : 0)
|
||||
padding = fill_with != false &&
|
||||
modulo > 0 && length == division ? 1 : 0
|
||||
groups << slice(start, length).concat([fill_with] * padding)
|
||||
start += length
|
||||
end
|
||||
number.times do |index|
|
||||
length = division + (modulo > 0 && modulo > index ? 1 : 0)
|
||||
padding = fill_with != false &&
|
||||
modulo > 0 && length == division ? 1 : 0
|
||||
groups << slice(start, length).concat([fill_with] * padding)
|
||||
start += length
|
||||
end
|
||||
|
||||
if block_given?
|
||||
groups.each{|g| yield(g) }
|
||||
else
|
||||
groups
|
||||
end
|
||||
end
|
||||
if block_given?
|
||||
groups.each { |g| yield(g) }
|
||||
else
|
||||
groups
|
||||
end
|
||||
end
|
||||
|
||||
# Divides the array into one or more subarrays based on a delimiting +value+
|
||||
# or the result of an optional block.
|
||||
#
|
||||
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
|
||||
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
|
||||
def split(value = nil)
|
||||
using_block = block_given?
|
||||
# Divides the array into one or more subarrays based on a delimiting +value+
|
||||
# or the result of an optional block.
|
||||
#
|
||||
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
|
||||
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
|
||||
def split(value = nil)
|
||||
using_block = block_given?
|
||||
|
||||
inject([[]]) do |results, element|
|
||||
if (using_block && yield(element)) || (value == element)
|
||||
results << []
|
||||
else
|
||||
results.last << element
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
inject([[]]) do |results, element|
|
||||
if (using_block && yield(element)) || (value == element)
|
||||
results << []
|
||||
else
|
||||
results.last << element
|
||||
end
|
||||
|
||||
results
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
class Array
|
||||
# The human way of thinking about adding stuff to the end of a list is with append
|
||||
alias_method :append, :<<
|
||||
|
||||
# The human way of thinking about adding stuff to the beginning of a list is with prepend
|
||||
alias_method :prepend, :unshift
|
||||
end
|
||||
@@ -1,42 +1,30 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module RandomAccess
|
||||
# This method is deprecated because it masks Kernel#rand within the Array class itself,
|
||||
# which may be used by a 3rd party library extending Array in turn. See
|
||||
#
|
||||
# https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/4555
|
||||
#
|
||||
def rand # :nodoc:
|
||||
ActiveSupport::Deprecation.warn 'Array#rand is deprecated and will be removed in Rails 3. Use Array#sample instead', caller
|
||||
sample
|
||||
end
|
||||
|
||||
# Returns a random element from the array.
|
||||
def random_element # :nodoc:
|
||||
ActiveSupport::Deprecation.warn 'Array#random_element is deprecated and will be removed in Rails 3. Use Array#sample instead', caller
|
||||
sample
|
||||
end
|
||||
|
||||
# Backport of Array#sample based on Marc-Andre Lafortune's http://github.com/marcandre/backports/
|
||||
def sample(n=nil)
|
||||
return self[Kernel.rand(size)] if n.nil?
|
||||
n = n.to_int
|
||||
rescue Exception => e
|
||||
raise TypeError, "Coercion error: #{n.inspect}.to_int => Integer failed:\n(#{e.message})"
|
||||
else
|
||||
raise TypeError, "Coercion error: #{n}.to_int did NOT return an Integer (was #{n.class})" unless n.kind_of? ::Integer
|
||||
raise ArgumentError, "negative array size" if n < 0
|
||||
n = size if n > size
|
||||
result = ::Array.new(self)
|
||||
n.times do |i|
|
||||
r = i + Kernel.rand(size - i)
|
||||
result[i], result[r] = result[r], result[i]
|
||||
end
|
||||
result[n..size] = []
|
||||
result
|
||||
end unless method_defined? :sample
|
||||
end
|
||||
class Array
|
||||
# Backport of Array#sample based on Marc-Andre Lafortune's https://github.com/marcandre/backports/
|
||||
# Returns a random element or +n+ random elements from the array.
|
||||
# If the array is empty and +n+ is nil, returns <tt>nil</tt>.
|
||||
# If +n+ is passed and its value is less than 0, it raises an +ArgumentError+ exception.
|
||||
# If the value of +n+ is equal or greater than 0 it returns <tt>[]</tt>.
|
||||
#
|
||||
# [1,2,3,4,5,6].sample # => 4
|
||||
# [1,2,3,4,5,6].sample(3) # => [2, 4, 5]
|
||||
# [1,2,3,4,5,6].sample(-3) # => ArgumentError: negative array size
|
||||
# [].sample # => nil
|
||||
# [].sample(3) # => []
|
||||
def sample(n=nil)
|
||||
return self[Kernel.rand(size)] if n.nil?
|
||||
n = n.to_int
|
||||
rescue Exception => e
|
||||
raise TypeError, "Coercion error: #{n.inspect}.to_int => Integer failed:\n(#{e.message})"
|
||||
else
|
||||
raise TypeError, "Coercion error: obj.to_int did NOT return an Integer (was #{n.class})" unless n.kind_of? Integer
|
||||
raise ArgumentError, "negative array size" if n < 0
|
||||
n = size if n > size
|
||||
result = Array.new(self)
|
||||
n.times do |i|
|
||||
r = i + Kernel.rand(size - i)
|
||||
result[i], result[r] = result[r], result[i]
|
||||
end
|
||||
end
|
||||
result[n..size] = []
|
||||
result
|
||||
end unless method_defined? :sample
|
||||
end
|
||||
|
||||
16
activesupport/lib/active_support/core_ext/array/uniq_by.rb
Normal file
16
activesupport/lib/active_support/core_ext/array/uniq_by.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class Array
|
||||
# Returns an unique array based on the criteria given as a +Proc+.
|
||||
#
|
||||
# [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2]
|
||||
#
|
||||
def uniq_by
|
||||
hash, array = {}, []
|
||||
each { |i| hash[yield(i)] ||= (array << i) }
|
||||
array
|
||||
end
|
||||
|
||||
# Same as uniq_by, but modifies self.
|
||||
def uniq_by!
|
||||
replace(uniq_by{ |i| yield(i) })
|
||||
end
|
||||
end
|
||||
48
activesupport/lib/active_support/core_ext/array/wrap.rb
Normal file
48
activesupport/lib/active_support/core_ext/array/wrap.rb
Normal file
@@ -0,0 +1,48 @@
|
||||
class Array
|
||||
# Wraps its argument in an array unless it is already an array (or array-like).
|
||||
#
|
||||
# Specifically:
|
||||
#
|
||||
# * If the argument is +nil+ an empty list is returned.
|
||||
# * Otherwise, if the argument responds to +to_ary+ it is invoked, and its result returned.
|
||||
# * Otherwise, returns an array with the argument as its single element.
|
||||
#
|
||||
# Array.wrap(nil) # => []
|
||||
# Array.wrap([1, 2, 3]) # => [1, 2, 3]
|
||||
# Array.wrap(0) # => [0]
|
||||
#
|
||||
# This method is similar in purpose to <tt>Kernel#Array</tt>, but there are some differences:
|
||||
#
|
||||
# * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
|
||||
# moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
|
||||
# such a +nil+ right away.
|
||||
# * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
|
||||
# raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
|
||||
# * It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array.
|
||||
#
|
||||
# The last point is particularly worth comparing for some enumerables:
|
||||
#
|
||||
# Array(:foo => :bar) # => [[:foo, :bar]]
|
||||
# Array.wrap(:foo => :bar) # => [{:foo => :bar}]
|
||||
#
|
||||
# Array("foo\nbar") # => ["foo\n", "bar"], in Ruby 1.8
|
||||
# Array.wrap("foo\nbar") # => ["foo\nbar"]
|
||||
#
|
||||
# There's also a related idiom that uses the splat operator:
|
||||
#
|
||||
# [*object]
|
||||
#
|
||||
# which returns <tt>[nil]</tt> for +nil+, and calls to <tt>Array(object)</tt> otherwise.
|
||||
#
|
||||
# Thus, in this case the behavior is different for +nil+, and the differences with
|
||||
# <tt>Kernel#Array</tt> explained above apply to the rest of +object+s.
|
||||
def self.wrap(object)
|
||||
if object.nil?
|
||||
[]
|
||||
elsif object.respond_to?(:to_ary)
|
||||
object.to_ary || [object]
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Array #:nodoc:
|
||||
module Wrapper
|
||||
# Wraps the object in an Array unless it's an Array. Converts the
|
||||
# object to an Array using #to_ary if it implements that.
|
||||
def wrap(object)
|
||||
case object
|
||||
when nil
|
||||
[]
|
||||
when self
|
||||
object
|
||||
else
|
||||
if object.respond_to?(:to_ary)
|
||||
object.to_ary
|
||||
else
|
||||
[object]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,4 +0,0 @@
|
||||
require 'active_support/base64'
|
||||
require 'active_support/core_ext/base64/encoding'
|
||||
|
||||
ActiveSupport::Base64.extend ActiveSupport::CoreExtensions::Base64::Encoding
|
||||
@@ -1,16 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Base64 #:nodoc:
|
||||
module Encoding
|
||||
# Encodes the value as base64 without the newline breaks. This makes the base64 encoding readily usable as URL parameters
|
||||
# or memcache keys without further processing.
|
||||
#
|
||||
# ActiveSupport::Base64.encode64s("Original unencoded string")
|
||||
# # => "T3JpZ2luYWwgdW5lbmNvZGVkIHN0cmluZw=="
|
||||
def encode64s(value)
|
||||
encode64(value).gsub(/\n/, '')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +1,6 @@
|
||||
require 'benchmark'
|
||||
|
||||
class << Benchmark
|
||||
# Earlier Ruby had a slower implementation.
|
||||
if RUBY_VERSION < '1.8.7'
|
||||
remove_method :realtime
|
||||
|
||||
def realtime
|
||||
r0 = Time.now
|
||||
yield
|
||||
r1 = Time.now
|
||||
r1.to_f - r0.to_f
|
||||
end
|
||||
end
|
||||
|
||||
def ms
|
||||
1000 * realtime { yield }
|
||||
end
|
||||
|
||||
1
activesupport/lib/active_support/core_ext/big_decimal.rb
Normal file
1
activesupport/lib/active_support/core_ext/big_decimal.rb
Normal file
@@ -0,0 +1 @@
|
||||
require 'active_support/core_ext/big_decimal/conversions'
|
||||
@@ -0,0 +1,45 @@
|
||||
require 'bigdecimal'
|
||||
|
||||
begin
|
||||
require 'psych'
|
||||
rescue LoadError
|
||||
end
|
||||
|
||||
require 'yaml'
|
||||
|
||||
class BigDecimal
|
||||
YAML_TAG = 'tag:yaml.org,2002:float'
|
||||
YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
|
||||
|
||||
# This emits the number without any scientific notation.
|
||||
# This is better than self.to_f.to_s since it doesn't lose precision.
|
||||
#
|
||||
# Note that reconstituting YAML floats to native floats may lose precision.
|
||||
def to_yaml(opts = {})
|
||||
return super if defined?(YAML::ENGINE) && !YAML::ENGINE.syck?
|
||||
|
||||
YAML.quick_emit(nil, opts) do |out|
|
||||
string = to_s
|
||||
out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain)
|
||||
end
|
||||
end
|
||||
|
||||
def encode_with(coder)
|
||||
string = to_s
|
||||
coder.represent_scalar(nil, YAML_MAPPING[string] || string)
|
||||
end
|
||||
|
||||
# Backport this method if it doesn't exist
|
||||
unless method_defined?(:to_d)
|
||||
def to_d
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
DEFAULT_STRING_FORMAT = 'F'
|
||||
def to_formatted_s(format = DEFAULT_STRING_FORMAT)
|
||||
_original_to_s(format)
|
||||
end
|
||||
alias_method :_original_to_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
require 'bigdecimal'
|
||||
require 'active_support/core_ext/bigdecimal/conversions'
|
||||
|
||||
class BigDecimal#:nodoc:
|
||||
include ActiveSupport::CoreExtensions::BigDecimal::Conversions
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
require 'yaml'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module BigDecimal #:nodoc:
|
||||
module Conversions
|
||||
DEFAULT_STRING_FORMAT = 'F'.freeze
|
||||
YAML_TAG = 'tag:yaml.org,2002:float'.freeze
|
||||
YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :_original_to_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
yaml_as YAML_TAG
|
||||
end
|
||||
end
|
||||
|
||||
def to_formatted_s(format = DEFAULT_STRING_FORMAT)
|
||||
_original_to_s(format)
|
||||
end
|
||||
|
||||
# This emits the number without any scientific notation.
|
||||
# This is better than self.to_f.to_s since it doesn't lose precision.
|
||||
#
|
||||
# Note that reconstituting YAML floats to native floats may lose precision.
|
||||
def to_yaml(opts = {})
|
||||
YAML.quick_emit(nil, opts) do |out|
|
||||
string = to_s
|
||||
out.scalar(YAML_TAG, YAML_MAPPING[string] || string, :plain)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,2 +0,0 @@
|
||||
require 'active_support/core_ext/object/blank'
|
||||
ActiveSupport::Deprecation.warn 'require "active_support/core_ext/blank" is deprecated and will be removed in Rails 3. Use require "active_support/core_ext/object/blank" instead.'
|
||||
@@ -1,5 +0,0 @@
|
||||
require 'active_support/core_ext/cgi/escape_skipping_slashes'
|
||||
|
||||
class CGI #:nodoc:
|
||||
extend ActiveSupport::CoreExtensions::CGI::EscapeSkippingSlashes
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module CGI #:nodoc:
|
||||
module EscapeSkippingSlashes #:nodoc:
|
||||
if RUBY_VERSION >= '1.9'
|
||||
def escape_skipping_slashes(str)
|
||||
str = str.join('/') if str.respond_to? :join
|
||||
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||
"%#{$1.unpack('H2' * $1.bytesize).join('%').upcase}"
|
||||
end.tr(' ', '+')
|
||||
end
|
||||
else
|
||||
def escape_skipping_slashes(str)
|
||||
str = str.join('/') if str.respond_to? :join
|
||||
str.gsub(/([^ \/a-zA-Z0-9_.-])/n) do
|
||||
"%#{$1.unpack('H2').first.upcase}"
|
||||
end.tr(' ', '+')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
require 'active_support/core_ext/class/attribute_accessors'
|
||||
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/attribute_accessors'
|
||||
require 'active_support/core_ext/class/delegating_attributes'
|
||||
require 'active_support/core_ext/class/inheritable_attributes'
|
||||
require 'active_support/core_ext/class/subclasses'
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/module/remove_method'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
|
||||
class Class
|
||||
# Declare a class-level attribute whose value is inheritable and
|
||||
# overwritable by subclasses:
|
||||
# Declare a class-level attribute whose value is inheritable by subclasses.
|
||||
# Subclasses can change their own value and it will not impact parent class.
|
||||
#
|
||||
# class Base
|
||||
# class_attribute :setting
|
||||
@@ -18,12 +19,34 @@ class Class
|
||||
# Subclass.setting # => false
|
||||
# Base.setting # => true
|
||||
#
|
||||
# In the above case as long as Subclass does not assign a value to setting
|
||||
# by performing <tt>Subclass.setting = _something_ </tt>, <tt>Subclass.setting</tt>
|
||||
# would read value assigned to parent class. Once Subclass assigns a value then
|
||||
# the value assigned by Subclass would be returned.
|
||||
#
|
||||
# This matches normal Ruby method inheritance: think of writing an attribute
|
||||
# on a subclass as overriding the reader method.
|
||||
# on a subclass as overriding the reader method. However, you need to be aware
|
||||
# when using +class_attribute+ with mutable structures as +Array+ or +Hash+.
|
||||
# In such cases, you don't want to do changes in places but use setters:
|
||||
#
|
||||
# Base.setting = []
|
||||
# Base.setting # => []
|
||||
# Subclass.setting # => []
|
||||
#
|
||||
# # Appending in child changes both parent and child because it is the same object:
|
||||
# Subclass.setting << :foo
|
||||
# Base.setting # => [:foo]
|
||||
# Subclass.setting # => [:foo]
|
||||
#
|
||||
# # Use setters to not propagate changes:
|
||||
# Base.setting = []
|
||||
# Subclass.setting += [:foo]
|
||||
# Base.setting # => []
|
||||
# Subclass.setting # => [:foo]
|
||||
#
|
||||
# For convenience, a query method is defined as well:
|
||||
#
|
||||
# Subclass.setting? # => false
|
||||
# Subclass.setting? # => false
|
||||
#
|
||||
# Instances may overwrite the class value in the same way:
|
||||
#
|
||||
@@ -34,11 +57,18 @@ class Class
|
||||
# object.setting # => false
|
||||
# Base.setting # => true
|
||||
#
|
||||
# To opt out of the instance reader method, pass :instance_reader => false.
|
||||
#
|
||||
# object.setting # => NoMethodError
|
||||
# object.setting? # => NoMethodError
|
||||
#
|
||||
# To opt out of the instance writer method, pass :instance_writer => false.
|
||||
#
|
||||
# object.setting = false # => NoMethodError
|
||||
def class_attribute(*attrs)
|
||||
instance_writer = !attrs.last.is_a?(Hash) || attrs.pop[:instance_writer]
|
||||
options = attrs.extract_options!
|
||||
instance_reader = options.fetch(:instance_reader, true)
|
||||
instance_writer = options.fetch(:instance_writer, true)
|
||||
|
||||
attrs.each do |name|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
@@ -50,18 +80,36 @@ class Class
|
||||
remove_possible_method(:#{name})
|
||||
define_method(:#{name}) { val }
|
||||
end
|
||||
|
||||
if singleton_class?
|
||||
class_eval do
|
||||
remove_possible_method(:#{name})
|
||||
def #{name}
|
||||
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
|
||||
end
|
||||
end
|
||||
end
|
||||
val
|
||||
end
|
||||
|
||||
def #{name}
|
||||
defined?(@#{name}) ? @#{name} : singleton_class.#{name}
|
||||
end
|
||||
if instance_reader
|
||||
remove_possible_method :#{name}
|
||||
def #{name}
|
||||
defined?(@#{name}) ? @#{name} : self.class.#{name}
|
||||
end
|
||||
|
||||
def #{name}?
|
||||
!!#{name}
|
||||
def #{name}?
|
||||
!!#{name}
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
attr_writer name if instance_writer
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def singleton_class?
|
||||
ancestors.first != self
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
|
||||
# Extends the class object with class and instance accessors for class attributes,
|
||||
# just like the native attr* accessors for instance attributes.
|
||||
#
|
||||
# class Person
|
||||
# cattr_accessor :hair_colors
|
||||
# end
|
||||
#
|
||||
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
||||
class Class
|
||||
# Defines a class attribute if it's not defined and creates a reader method that
|
||||
# returns the attribute value.
|
||||
#
|
||||
# class Person
|
||||
# cattr_reader :hair_colors
|
||||
# end
|
||||
#
|
||||
# Person.class_variable_set("@@hair_colors", [:brown, :black])
|
||||
# Person.hair_colors # => [:brown, :black]
|
||||
# Person.new.hair_colors # => [:brown, :black]
|
||||
#
|
||||
# The attribute name must be a valid method name in Ruby.
|
||||
#
|
||||
# class Person
|
||||
# cattr_reader :"1_Badname "
|
||||
# end
|
||||
# # => NameError: invalid attribute name
|
||||
#
|
||||
# If you want to opt out the instance reader method, you can pass <tt>:instance_reader => false</tt>
|
||||
# or <tt>:instance_accessor => false</tt>.
|
||||
#
|
||||
# class Person
|
||||
# cattr_reader :hair_colors, :instance_reader => false
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors # => NoMethodError
|
||||
def cattr_reader(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
next if sym.is_a?(Hash)
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
unless defined? @@#{sym}
|
||||
@@#{sym} = nil
|
||||
@@ -21,7 +42,7 @@ class Class
|
||||
end
|
||||
EOS
|
||||
|
||||
unless options[:instance_reader] == false
|
||||
unless options[:instance_reader] == false || options[:instance_accessor] == false
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}
|
||||
@@#{sym}
|
||||
@@ -31,6 +52,43 @@ class Class
|
||||
end
|
||||
end
|
||||
|
||||
# Defines a class attribute if it's not defined and creates a writer method to allow
|
||||
# assignment to the attribute.
|
||||
#
|
||||
# class Person
|
||||
# cattr_writer :hair_colors
|
||||
# end
|
||||
#
|
||||
# Person.hair_colors = [:brown, :black]
|
||||
# Person.class_variable_get("@@hair_colors") # => [:brown, :black]
|
||||
# Person.new.hair_colors = [:blonde, :red]
|
||||
# Person.class_variable_get("@@hair_colors") # => [:blonde, :red]
|
||||
#
|
||||
# The attribute name must be a valid method name in Ruby.
|
||||
#
|
||||
# class Person
|
||||
# cattr_writer :"1_Badname "
|
||||
# end
|
||||
# # => NameError: invalid attribute name
|
||||
#
|
||||
# If you want to opt out the instance writer method, pass <tt>:instance_writer => false</tt>
|
||||
# or <tt>:instance_accessor => false</tt>.
|
||||
#
|
||||
# class Person
|
||||
# cattr_writer :hair_colors, :instance_writer => false
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors = [:blonde, :red] # => NoMethodError
|
||||
#
|
||||
# Also, you can pass a block to set up the attribute with a default value.
|
||||
#
|
||||
# class Person
|
||||
# cattr_writer :hair_colors do
|
||||
# [:brown, :black, :blonde, :red]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
|
||||
def cattr_writer(*syms)
|
||||
options = syms.extract_options!
|
||||
syms.each do |sym|
|
||||
@@ -44,18 +102,67 @@ class Class
|
||||
end
|
||||
EOS
|
||||
|
||||
unless options[:instance_writer] == false
|
||||
unless options[:instance_writer] == false || options[:instance_accessor] == false
|
||||
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
|
||||
def #{sym}=(obj)
|
||||
@@#{sym} = obj
|
||||
end
|
||||
EOS
|
||||
end
|
||||
self.send("#{sym}=", yield) if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
def cattr_accessor(*syms)
|
||||
# Defines both class and instance accessors for class attributes.
|
||||
#
|
||||
# class Person
|
||||
# cattr_accessor :hair_colors
|
||||
# end
|
||||
#
|
||||
# Person.hair_colors = [:brown, :black, :blonde, :red]
|
||||
# Person.hair_colors # => [:brown, :black, :blonde, :red]
|
||||
# Person.new.hair_colors # => [:brown, :black, :blonde, :red]
|
||||
#
|
||||
# If a subclass changes the value then that would also change the value for
|
||||
# parent class. Similarly if parent class changes the value then that would
|
||||
# change the value of subclasses too.
|
||||
#
|
||||
# class Male < Person
|
||||
# end
|
||||
#
|
||||
# Male.hair_colors << :blue
|
||||
# Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
|
||||
#
|
||||
# To opt out of the instance writer method, pass <tt>:instance_writer => false</tt>.
|
||||
# To opt out of the instance reader method, pass <tt>:instance_reader => false</tt>.
|
||||
#
|
||||
# class Person
|
||||
# cattr_accessor :hair_colors, :instance_writer => false, :instance_reader => false
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors = [:brown] # => NoMethodError
|
||||
# Person.new.hair_colors # => NoMethodError
|
||||
#
|
||||
# Or pass <tt>:instance_accessor => false</tt>, to opt out both instance methods.
|
||||
#
|
||||
# class Person
|
||||
# cattr_accessor :hair_colors, :instance_accessor => false
|
||||
# end
|
||||
#
|
||||
# Person.new.hair_colors = [:brown] # => NoMethodError
|
||||
# Person.new.hair_colors # => NoMethodError
|
||||
#
|
||||
# Also you can pass a block to set up the attribute with a default value.
|
||||
#
|
||||
# class Person
|
||||
# cattr_accessor :hair_colors do
|
||||
# [:brown, :black, :blonde, :red]
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red]
|
||||
def cattr_accessor(*syms, &blk)
|
||||
cattr_reader(*syms)
|
||||
cattr_writer(*syms)
|
||||
cattr_writer(*syms, &blk)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,47 +1,44 @@
|
||||
# These class attributes behave something like the class
|
||||
# inheritable accessors. But instead of copying the hash over at
|
||||
# the time the subclass is first defined, the accessors simply
|
||||
# delegate to their superclass unless they have been given a
|
||||
# specific value. This stops the strange situation where values
|
||||
# set after class definition don't get applied to subclasses.
|
||||
require 'active_support/core_ext/object/blank'
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
require 'active_support/core_ext/kernel/singleton_class'
|
||||
require 'active_support/core_ext/module/remove_method'
|
||||
|
||||
class Class
|
||||
def superclass_delegating_reader(*names)
|
||||
class_name_to_stop_searching_on = self.superclass.name.blank? ? "Object" : self.superclass.name
|
||||
names.each do |name|
|
||||
class_eval <<-EOS
|
||||
def self.#{name} # def self.only_reader
|
||||
if defined?(@#{name}) # if defined?(@only_reader)
|
||||
@#{name} # @only_reader
|
||||
elsif superclass < #{class_name_to_stop_searching_on} && # elsif superclass < Object &&
|
||||
superclass.respond_to?(:#{name}) # superclass.respond_to?(:only_reader)
|
||||
superclass.#{name} # superclass.only_reader
|
||||
end # end
|
||||
end # end
|
||||
def #{name} # def only_reader
|
||||
self.class.#{name} # self.class.only_reader
|
||||
end # end
|
||||
def self.#{name}? # def self.only_reader?
|
||||
!!#{name} # !!only_reader
|
||||
end # end
|
||||
def #{name}? # def only_reader?
|
||||
!!#{name} # !!only_reader
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
def superclass_delegating_accessor(name, options = {})
|
||||
# Create private _name and _name= methods that can still be used if the public
|
||||
# methods are overridden. This allows
|
||||
_superclass_delegating_accessor("_#{name}")
|
||||
|
||||
# Generate the public methods name, name=, and name?
|
||||
# These methods dispatch to the private _name, and _name= methods, making them
|
||||
# overridable
|
||||
singleton_class.send(:define_method, name) { send("_#{name}") }
|
||||
singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") }
|
||||
singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) }
|
||||
|
||||
# If an instance_reader is needed, generate methods for name and name= on the
|
||||
# class itself, so instances will be able to see them
|
||||
define_method(name) { send("_#{name}") } if options[:instance_reader] != false
|
||||
define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false
|
||||
end
|
||||
|
||||
def superclass_delegating_writer(*names)
|
||||
names.each do |name|
|
||||
class_eval <<-EOS
|
||||
def self.#{name}=(value) # def self.only_writer=(value)
|
||||
@#{name} = value # @only_writer = value
|
||||
end # end
|
||||
EOS
|
||||
end
|
||||
private
|
||||
|
||||
# Take the object being set and store it in a method. This gives us automatic
|
||||
# inheritance behavior, without having to store the object in an instance
|
||||
# variable and look up the superclass chain manually.
|
||||
def _stash_object_in_method(object, method, instance_reader = true)
|
||||
singleton_class.remove_possible_method(method)
|
||||
singleton_class.send(:define_method, method) { object }
|
||||
remove_possible_method(method)
|
||||
define_method(method) { object } if instance_reader
|
||||
end
|
||||
|
||||
def superclass_delegating_accessor(*names)
|
||||
superclass_delegating_reader(*names)
|
||||
superclass_delegating_writer(*names)
|
||||
def _superclass_delegating_accessor(name, options = {})
|
||||
singleton_class.send(:define_method, "#{name}=") do |value|
|
||||
_stash_object_in_method(value, name, options[:instance_reader] != false)
|
||||
end
|
||||
send("#{name}=", nil)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
# Retain for backward compatibility. Methods are now included in Class.
|
||||
module ClassInheritableAttributes # :nodoc:
|
||||
end
|
||||
|
||||
# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
|
||||
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
|
||||
# to, for example, an array without those additions being shared with either their parent, siblings, or
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
class Class #:nodoc:
|
||||
|
||||
# Unassociates the class with its subclasses and removes the subclasses
|
||||
# themselves.
|
||||
#
|
||||
# Integer.remove_subclasses # => [Bignum, Fixnum]
|
||||
# Fixnum # => NameError: uninitialized constant Fixnum
|
||||
def remove_subclasses
|
||||
Object.remove_subclasses_of(self)
|
||||
end
|
||||
|
||||
# Returns an array with the names of the subclasses of +self+ as strings.
|
||||
#
|
||||
# Integer.subclasses # => ["Bignum", "Fixnum"]
|
||||
def subclasses
|
||||
Object.subclasses_of(self).map { |o| o.to_s }
|
||||
end
|
||||
|
||||
# Removes the classes in +klasses+ from their parent module.
|
||||
#
|
||||
# Ordinary classes belong to some module via a constant. This method computes
|
||||
# that constant name from the class name and removes it from the module it
|
||||
# belongs to.
|
||||
#
|
||||
# Object.remove_class(Integer) # => [Integer]
|
||||
# Integer # => NameError: uninitialized constant Integer
|
||||
#
|
||||
# Take into account that in general the class object could be still stored
|
||||
# somewhere else.
|
||||
#
|
||||
# i = Integer # => Integer
|
||||
# Object.remove_class(Integer) # => [Integer]
|
||||
# Integer # => NameError: uninitialized constant Integer
|
||||
# i.subclasses # => ["Bignum", "Fixnum"]
|
||||
# Fixnum.superclass # => Integer
|
||||
def remove_class(*klasses)
|
||||
klasses.flatten.each do |klass|
|
||||
# Skip this class if there is nothing bound to this name
|
||||
next unless defined?(klass.name)
|
||||
|
||||
basename = klass.to_s.split("::").last
|
||||
parent = klass.parent
|
||||
|
||||
# Skip this class if it does not match the current one bound to this name
|
||||
next unless parent.const_defined?(basename) && klass = parent.const_get(basename)
|
||||
|
||||
parent.instance_eval { remove_const basename } unless parent == klass
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,36 @@
|
||||
require 'active_support/core_ext/module/anonymous'
|
||||
require 'active_support/core_ext/module/reachable'
|
||||
|
||||
class Class #:nodoc:
|
||||
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
|
||||
@@ -1,10 +0,0 @@
|
||||
require 'date'
|
||||
require 'active_support/core_ext/date/behavior'
|
||||
require 'active_support/core_ext/date/calculations'
|
||||
require 'active_support/core_ext/date/conversions'
|
||||
|
||||
class Date#:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Date::Behavior
|
||||
include ActiveSupport::CoreExtensions::Date::Calculations
|
||||
include ActiveSupport::CoreExtensions::Date::Conversions
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
require 'active_support/core_ext/object/acts_like'
|
||||
|
||||
class Date
|
||||
# Duck-types as a Date-like class. See Object#acts_like?.
|
||||
def acts_like_date?
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -1,42 +0,0 @@
|
||||
require 'date'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Date #:nodoc:
|
||||
module Behavior
|
||||
# Enable more predictable duck-typing on Date-like classes. See
|
||||
# Object#acts_like?.
|
||||
def acts_like_date?
|
||||
true
|
||||
end
|
||||
|
||||
# Date memoizes some instance methods using metaprogramming to wrap
|
||||
# the methods with one that caches the result in an instance variable.
|
||||
#
|
||||
# If a Date is frozen but the memoized method hasn't been called, the
|
||||
# first call will result in a frozen object error since the memo
|
||||
# instance variable is uninitialized.
|
||||
#
|
||||
# Work around by eagerly memoizing before freezing.
|
||||
#
|
||||
# Ruby 1.9 uses a preinitialized instance variable so it's unaffected.
|
||||
# This hack is as close as we can get to feature detection:
|
||||
begin
|
||||
::Date.today.freeze.jd
|
||||
rescue => frozen_object_error
|
||||
if frozen_object_error.message =~ /frozen/
|
||||
def freeze #:nodoc:
|
||||
self.class.private_instance_methods(false).each do |m|
|
||||
if m.to_s =~ /\A__\d+__\Z/
|
||||
instance_variable_set(:"@#{m}", [send(m)])
|
||||
end
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,241 +1,276 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Date #:nodoc:
|
||||
# Enables the use of time calculations within Date itself
|
||||
module Calculations
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend ClassMethods
|
||||
require 'date'
|
||||
require 'active_support/duration'
|
||||
require 'active_support/core_ext/object/acts_like'
|
||||
require 'active_support/core_ext/date/zones'
|
||||
require 'active_support/core_ext/time/zones'
|
||||
|
||||
base.instance_eval do
|
||||
alias_method :plus_without_duration, :+
|
||||
alias_method :+, :plus_with_duration
|
||||
class Date
|
||||
DAYS_INTO_WEEK = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6 }
|
||||
|
||||
alias_method :minus_without_duration, :-
|
||||
alias_method :-, :minus_with_duration
|
||||
end
|
||||
end
|
||||
if RUBY_VERSION < '1.9'
|
||||
undef :>>
|
||||
|
||||
module ClassMethods
|
||||
# Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
|
||||
def yesterday
|
||||
::Date.today.yesterday
|
||||
end
|
||||
|
||||
# Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
|
||||
def tomorrow
|
||||
::Date.today.tomorrow
|
||||
end
|
||||
|
||||
# Returns Time.zone.today when config.time_zone is set, otherwise just returns Date.today.
|
||||
def current
|
||||
::Time.zone_default ? ::Time.zone.today : ::Date.today
|
||||
end
|
||||
end
|
||||
|
||||
# Tells whether the Date object's date lies in the past
|
||||
def past?
|
||||
self < ::Date.current
|
||||
end
|
||||
|
||||
# Tells whether the Date object's date is today
|
||||
def today?
|
||||
self.to_date == ::Date.current # we need the to_date because of DateTime
|
||||
end
|
||||
|
||||
# Tells whether the Date object's date lies in the future
|
||||
def future?
|
||||
self > ::Date.current
|
||||
end
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
# and then subtracts the specified number of seconds
|
||||
def ago(seconds)
|
||||
to_time.since(-seconds)
|
||||
end
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
# and then adds the specified number of seconds
|
||||
def since(seconds)
|
||||
to_time.since(seconds)
|
||||
end
|
||||
alias :in :since
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
def beginning_of_day
|
||||
to_time
|
||||
end
|
||||
alias :midnight :beginning_of_day
|
||||
alias :at_midnight :beginning_of_day
|
||||
alias :at_beginning_of_day :beginning_of_day
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
|
||||
def end_of_day
|
||||
to_time.end_of_day
|
||||
end
|
||||
|
||||
def plus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
other.since(self)
|
||||
else
|
||||
plus_without_duration(other)
|
||||
end
|
||||
end
|
||||
|
||||
def minus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
plus_with_duration(-other)
|
||||
else
|
||||
minus_without_duration(other)
|
||||
end
|
||||
end
|
||||
|
||||
# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
|
||||
# any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
|
||||
def advance(options)
|
||||
options = options.dup
|
||||
d = self
|
||||
d = d >> options.delete(:years) * 12 if options[:years]
|
||||
d = d >> options.delete(:months) if options[:months]
|
||||
d = d + options.delete(:weeks) * 7 if options[:weeks]
|
||||
d = d + options.delete(:days) if options[:days]
|
||||
d
|
||||
end
|
||||
|
||||
# Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Date.new(2007, 5, 12).change(:day => 1) # => Date.new(2007, 5, 1)
|
||||
# Date.new(2007, 5, 12).change(:year => 2005, :month => 1) # => Date.new(2005, 1, 12)
|
||||
def change(options)
|
||||
::Date.new(
|
||||
options[:year] || self.year,
|
||||
options[:month] || self.month,
|
||||
options[:day] || self.day
|
||||
)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified months ago
|
||||
def months_ago(months)
|
||||
advance(:months => -months)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified months in the future
|
||||
def months_since(months)
|
||||
advance(:months => months)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified years ago
|
||||
def years_ago(years)
|
||||
advance(:years => -years)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified years in the future
|
||||
def years_since(years)
|
||||
advance(:years => years)
|
||||
end
|
||||
|
||||
def last_year # :nodoc:
|
||||
ActiveSupport::Deprecation.warn("Date#last_year is deprecated and has been removed in Rails 3, please use Date#prev_year instead", caller)
|
||||
prev_year
|
||||
end
|
||||
|
||||
# Short-hand for years_ago(1)
|
||||
def prev_year
|
||||
years_ago(1)
|
||||
end unless method_defined?(:prev_year)
|
||||
|
||||
# Short-hand for years_since(1)
|
||||
def next_year
|
||||
years_since(1)
|
||||
end
|
||||
|
||||
def last_month # :nodoc:
|
||||
ActiveSupport::Deprecation.warn("Date#last_month is deprecated and has been removed in Rails 3, please use Date#prev_month instead", caller)
|
||||
prev_month
|
||||
end
|
||||
|
||||
# Short-hand for months_ago(1)
|
||||
def prev_month
|
||||
months_ago(1)
|
||||
end unless method_defined?(:prev_month)
|
||||
|
||||
# Short-hand for months_since(1)
|
||||
def next_month
|
||||
months_since(1)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the "start" of this week (i.e, Monday; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_week
|
||||
days_to_monday = self.wday!=0 ? self.wday-1 : 6
|
||||
result = self - days_to_monday
|
||||
self.acts_like?(:time) ? result.midnight : result
|
||||
end
|
||||
alias :monday :beginning_of_week
|
||||
alias :at_beginning_of_week :beginning_of_week
|
||||
|
||||
# Returns a new Date/DateTime representing the end of this week (Sunday, DateTime objects will have time set to 23:59:59)
|
||||
def end_of_week
|
||||
days_to_sunday = self.wday!=0 ? 7-self.wday : 0
|
||||
result = self + days_to_sunday.days
|
||||
self.acts_like?(:time) ? result.end_of_day : result
|
||||
end
|
||||
alias :at_end_of_week :end_of_week
|
||||
|
||||
# Returns a new Date/DateTime representing the start of the given day in next week (default is Monday).
|
||||
def next_week(day = :monday)
|
||||
days_into_week = { :monday => 0, :tuesday => 1, :wednesday => 2, :thursday => 3, :friday => 4, :saturday => 5, :sunday => 6}
|
||||
result = (self + 7).beginning_of_week + days_into_week[day]
|
||||
self.acts_like?(:time) ? result.change(:hour => 0) : result
|
||||
end
|
||||
|
||||
# Returns a new ; DateTime objects will have time set to 0:00DateTime representing the start of the month (1st of the month; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_month
|
||||
self.acts_like?(:time) ? change(:day => 1,:hour => 0, :min => 0, :sec => 0) : change(:day => 1)
|
||||
end
|
||||
alias :at_beginning_of_month :beginning_of_month
|
||||
|
||||
# Returns a new Date/DateTime representing the end of the month (last day of the month; DateTime objects will have time set to 0:00)
|
||||
def end_of_month
|
||||
last_day = ::Time.days_in_month( self.month, self.year )
|
||||
self.acts_like?(:time) ? change(:day => last_day, :hour => 23, :min => 59, :sec => 59) : change(:day => last_day)
|
||||
end
|
||||
alias :at_end_of_month :end_of_month
|
||||
|
||||
# Returns a new Date/DateTime representing the start of the quarter (1st of january, april, july, october; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_quarter
|
||||
beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month })
|
||||
end
|
||||
alias :at_beginning_of_quarter :beginning_of_quarter
|
||||
|
||||
# Returns a new Date/DateTime representing the end of the quarter (last day of march, june, september, december; DateTime objects will have time set to 23:59:59)
|
||||
def end_of_quarter
|
||||
beginning_of_month.change(:month => [3, 6, 9, 12].detect { |m| m >= self.month }).end_of_month
|
||||
end
|
||||
alias :at_end_of_quarter :end_of_quarter
|
||||
|
||||
# Returns a new Date/DateTime representing the start of the year (1st of january; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_year
|
||||
self.acts_like?(:time) ? change(:month => 1, :day => 1, :hour => 0, :min => 0, :sec => 0) : change(:month => 1, :day => 1)
|
||||
end
|
||||
alias :at_beginning_of_year :beginning_of_year
|
||||
|
||||
# Returns a new Time representing the end of the year (31st of december; DateTime objects will have time set to 23:59:59)
|
||||
def end_of_year
|
||||
self.acts_like?(:time) ? change(:month => 12,:day => 31,:hour => 23, :min => 59, :sec => 59) : change(:month => 12, :day => 31)
|
||||
end
|
||||
alias :at_end_of_year :end_of_year
|
||||
|
||||
# Convenience method which returns a new Date/DateTime representing the time 1 day ago
|
||||
def yesterday
|
||||
self - 1
|
||||
end
|
||||
|
||||
# Convenience method which returns a new Date/DateTime representing the time 1 day since the instance time
|
||||
def tomorrow
|
||||
self + 1
|
||||
end
|
||||
# Backported from 1.9. The one in 1.8 leads to incorrect next_month and
|
||||
# friends for dates where the calendar reform is involved. It additionally
|
||||
# prevents an infinite loop fixed in r27013.
|
||||
def >>(n)
|
||||
y, m = (year * 12 + (mon - 1) + n).divmod(12)
|
||||
m, = (m + 1) .divmod(1)
|
||||
d = mday
|
||||
until jd2 = self.class.valid_civil?(y, m, d, start)
|
||||
d -= 1
|
||||
raise ArgumentError, 'invalid date' unless d > 0
|
||||
end
|
||||
self + (jd2 - jd)
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
# Returns a new Date representing the date 1 day ago (i.e. yesterday's date).
|
||||
def yesterday
|
||||
::Date.current.yesterday
|
||||
end
|
||||
|
||||
# Returns a new Date representing the date 1 day after today (i.e. tomorrow's date).
|
||||
def tomorrow
|
||||
::Date.current.tomorrow
|
||||
end
|
||||
|
||||
# Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
|
||||
def current
|
||||
::Time.zone ? ::Time.zone.today : ::Date.today
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the Date object's date lies in the past. Otherwise returns false.
|
||||
def past?
|
||||
self < ::Date.current
|
||||
end
|
||||
|
||||
# Returns true if the Date object's date is today.
|
||||
def today?
|
||||
self.to_date == ::Date.current # we need the to_date because of DateTime
|
||||
end
|
||||
|
||||
# Returns true if the Date object's date lies in the future.
|
||||
def future?
|
||||
self > ::Date.current
|
||||
end
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
# and then subtracts the specified number of seconds.
|
||||
def ago(seconds)
|
||||
to_time_in_current_zone.since(-seconds)
|
||||
end
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
# and then adds the specified number of seconds
|
||||
def since(seconds)
|
||||
to_time_in_current_zone.since(seconds)
|
||||
end
|
||||
alias :in :since
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
|
||||
def beginning_of_day
|
||||
to_time_in_current_zone
|
||||
end
|
||||
alias :midnight :beginning_of_day
|
||||
alias :at_midnight :beginning_of_day
|
||||
alias :at_beginning_of_day :beginning_of_day
|
||||
|
||||
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
|
||||
def end_of_day
|
||||
to_time_in_current_zone.end_of_day
|
||||
end
|
||||
|
||||
def plus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
other.since(self)
|
||||
else
|
||||
plus_without_duration(other)
|
||||
end
|
||||
end
|
||||
alias_method :plus_without_duration, :+
|
||||
alias_method :+, :plus_with_duration
|
||||
|
||||
def minus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
plus_with_duration(-other)
|
||||
else
|
||||
minus_without_duration(other)
|
||||
end
|
||||
end
|
||||
alias_method :minus_without_duration, :-
|
||||
alias_method :-, :minus_with_duration
|
||||
|
||||
# Provides precise Date calculations for years, months, and days. The +options+ parameter takes a hash with
|
||||
# any of these keys: <tt>:years</tt>, <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>.
|
||||
def advance(options)
|
||||
options = options.dup
|
||||
d = self
|
||||
d = d >> options.delete(:years) * 12 if options[:years]
|
||||
d = d >> options.delete(:months) if options[:months]
|
||||
d = d + options.delete(:weeks) * 7 if options[:weeks]
|
||||
d = d + options.delete(:days) if options[:days]
|
||||
d
|
||||
end
|
||||
|
||||
# Returns a new Date where one or more of the elements have been changed according to the +options+ parameter.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# Date.new(2007, 5, 12).change(:day => 1) # => Date.new(2007, 5, 1)
|
||||
# Date.new(2007, 5, 12).change(:year => 2005, :month => 1) # => Date.new(2005, 1, 12)
|
||||
def change(options)
|
||||
::Date.new(
|
||||
options[:year] || self.year,
|
||||
options[:month] || self.month,
|
||||
options[:day] || self.day
|
||||
)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified weeks ago.
|
||||
def weeks_ago(weeks)
|
||||
advance(:weeks => -weeks)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified months ago.
|
||||
def months_ago(months)
|
||||
advance(:months => -months)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified months in the future.
|
||||
def months_since(months)
|
||||
advance(:months => months)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified years ago.
|
||||
def years_ago(years)
|
||||
advance(:years => -years)
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the time a number of specified years in the future.
|
||||
def years_since(years)
|
||||
advance(:years => years)
|
||||
end
|
||||
|
||||
# Shorthand for years_ago(1)
|
||||
def prev_year
|
||||
years_ago(1)
|
||||
end unless method_defined?(:prev_year)
|
||||
|
||||
# Shorthand for years_since(1)
|
||||
def next_year
|
||||
years_since(1)
|
||||
end unless method_defined?(:next_year)
|
||||
|
||||
# Shorthand for months_ago(1)
|
||||
def prev_month
|
||||
months_ago(1)
|
||||
end unless method_defined?(:prev_month)
|
||||
|
||||
# Shorthand for months_since(1)
|
||||
def next_month
|
||||
months_since(1)
|
||||
end unless method_defined?(:next_month)
|
||||
|
||||
# Returns number of days to start of this week. Week is assumed to start on
|
||||
# +start_day+, default is +:monday+.
|
||||
def days_to_week_start(start_day = :monday)
|
||||
start_day_number = DAYS_INTO_WEEK[start_day]
|
||||
current_day_number = wday != 0 ? wday - 1 : 6
|
||||
(current_day_number - start_day_number) % 7
|
||||
end
|
||||
|
||||
# Returns a new +Date+/+DateTime+ representing the start of this week. Week is
|
||||
# assumed to start on +start_day+, default is +:monday+. +DateTime+ objects
|
||||
# have their time set to 0:00.
|
||||
def beginning_of_week(start_day = :monday)
|
||||
days_to_start = days_to_week_start(start_day)
|
||||
result = self - days_to_start
|
||||
acts_like?(:time) ? result.midnight : result
|
||||
end
|
||||
alias :at_beginning_of_week :beginning_of_week
|
||||
|
||||
# Returns a new +Date+/+DateTime+ representing the start of this week. Week is
|
||||
# assumed to start on a Monday. +DateTime+ objects have their time set to 0:00.
|
||||
def monday
|
||||
beginning_of_week
|
||||
end
|
||||
|
||||
# Returns a new +Date+/+DateTime+ representing the end of this week. Week is
|
||||
# assumed to start on +start_day+, default is +:monday+. +DateTime+ objects
|
||||
# have their time set to 23:59:59.
|
||||
def end_of_week(start_day = :monday)
|
||||
days_to_end = 6 - days_to_week_start(start_day)
|
||||
result = self + days_to_end.days
|
||||
self.acts_like?(:time) ? result.end_of_day : result
|
||||
end
|
||||
alias :at_end_of_week :end_of_week
|
||||
|
||||
# Returns a new +Date+/+DateTime+ representing the end of this week. Week is
|
||||
# assumed to start on a Monday. +DateTime+ objects have their time set to 23:59:59.
|
||||
def sunday
|
||||
end_of_week
|
||||
end
|
||||
|
||||
# Returns a new +Date+/+DateTime+ representing the given +day+ in the previous
|
||||
# week. Default is +:monday+. +DateTime+ objects have their time set to 0:00.
|
||||
def prev_week(day = :monday)
|
||||
result = (self - 7).beginning_of_week + DAYS_INTO_WEEK[day]
|
||||
self.acts_like?(:time) ? result.change(:hour => 0) : result
|
||||
end
|
||||
|
||||
# Returns a new Date/DateTime representing the start of the given day in next week (default is :monday).
|
||||
def next_week(day = :monday)
|
||||
result = (self + 7).beginning_of_week + DAYS_INTO_WEEK[day]
|
||||
self.acts_like?(:time) ? result.change(:hour => 0) : result
|
||||
end
|
||||
|
||||
# Returns a new ; DateTime objects will have time set to 0:00DateTime representing the start of the month (1st of the month; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_month
|
||||
self.acts_like?(:time) ? change(:day => 1, :hour => 0) : change(:day => 1)
|
||||
end
|
||||
alias :at_beginning_of_month :beginning_of_month
|
||||
|
||||
# Returns a new Date/DateTime representing the end of the month (last day of the month; DateTime objects will have time set to 0:00)
|
||||
def end_of_month
|
||||
last_day = ::Time.days_in_month( self.month, self.year )
|
||||
self.acts_like?(:time) ? change(:day => last_day, :hour => 23, :min => 59, :sec => 59) : change(:day => last_day)
|
||||
end
|
||||
alias :at_end_of_month :end_of_month
|
||||
|
||||
# Returns a new Date/DateTime representing the start of the quarter (1st of january, april, july, october; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_quarter
|
||||
beginning_of_month.change(:month => [10, 7, 4, 1].detect { |m| m <= self.month })
|
||||
end
|
||||
alias :at_beginning_of_quarter :beginning_of_quarter
|
||||
|
||||
# Returns a new Date/DateTime representing the end of the quarter (last day of march, june, september, december; DateTime objects will have time set to 23:59:59)
|
||||
def end_of_quarter
|
||||
beginning_of_month.change(:month => [3, 6, 9, 12].detect { |m| m >= self.month }).end_of_month
|
||||
end
|
||||
alias :at_end_of_quarter :end_of_quarter
|
||||
|
||||
# Returns a new Date/DateTime representing the start of the year (1st of january; DateTime objects will have time set to 0:00)
|
||||
def beginning_of_year
|
||||
self.acts_like?(:time) ? change(:month => 1, :day => 1, :hour => 0) : change(:month => 1, :day => 1)
|
||||
end
|
||||
alias :at_beginning_of_year :beginning_of_year
|
||||
|
||||
# Returns a new Time representing the end of the year (31st of december; DateTime objects will have time set to 23:59:59)
|
||||
def end_of_year
|
||||
self.acts_like?(:time) ? change(:month => 12, :day => 31, :hour => 23, :min => 59, :sec => 59) : change(:month => 12, :day => 31)
|
||||
end
|
||||
alias :at_end_of_year :end_of_year
|
||||
|
||||
# Convenience method which returns a new Date/DateTime representing the time 1 day ago
|
||||
def yesterday
|
||||
self - 1
|
||||
end
|
||||
|
||||
# Convenience method which returns a new Date/DateTime representing the time 1 day since the instance time
|
||||
def tomorrow
|
||||
self + 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,107 +1,106 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Date #:nodoc:
|
||||
# Converting dates to formatted strings, times, and datetimes.
|
||||
module Conversions
|
||||
DATE_FORMATS = {
|
||||
:short => "%e %b",
|
||||
:long => "%B %e, %Y",
|
||||
:db => "%Y-%m-%d",
|
||||
:number => "%Y%m%d",
|
||||
:long_ordinal => lambda { |date| date.strftime("%B #{date.day.ordinalize}, %Y") }, # => "April 25th, 2007"
|
||||
:rfc822 => "%e %b %Y"
|
||||
}
|
||||
require 'date'
|
||||
require 'active_support/inflector/methods'
|
||||
require 'active_support/core_ext/date/zones'
|
||||
require 'active_support/core_ext/module/remove_method'
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.instance_eval do
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
alias_method :default_inspect, :inspect
|
||||
alias_method :inspect, :readable_inspect
|
||||
class Date
|
||||
DATE_FORMATS = {
|
||||
:short => "%e %b",
|
||||
:long => "%B %e, %Y",
|
||||
:db => "%Y-%m-%d",
|
||||
:number => "%Y%m%d",
|
||||
:long_ordinal => lambda { |date| date.strftime("%B #{ActiveSupport::Inflector.ordinalize(date.day)}, %Y") }, # => "April 25th, 2007"
|
||||
:rfc822 => "%e %b %Y"
|
||||
}
|
||||
|
||||
# Ruby 1.9 has Date#to_time which converts to localtime only.
|
||||
remove_method :to_time if base.instance_methods.include?(:to_time)
|
||||
# Ruby 1.9 has Date#to_time which converts to localtime only.
|
||||
remove_possible_method :to_time
|
||||
|
||||
# Ruby 1.9 has Date#xmlschema which converts to a string without the time component.
|
||||
remove_method :xmlschema if base.instance_methods.include?(:xmlschema)
|
||||
end
|
||||
end
|
||||
# Ruby 1.9 has Date#xmlschema which converts to a string without the time component.
|
||||
remove_possible_method :xmlschema
|
||||
|
||||
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
|
||||
#
|
||||
# This method is aliased to <tt>to_s</tt>.
|
||||
#
|
||||
# ==== Examples
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_formatted_s(:db) # => "2007-11-10"
|
||||
# date.to_s(:db) # => "2007-11-10"
|
||||
#
|
||||
# date.to_formatted_s(:short) # => "10 Nov"
|
||||
# date.to_formatted_s(:long) # => "November 10, 2007"
|
||||
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
|
||||
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
|
||||
#
|
||||
# == Adding your own time formats to to_formatted_s
|
||||
# You can add your own formats to the Date::DATE_FORMATS hash.
|
||||
# Use the format name as the hash key and either a strftime string
|
||||
# or Proc instance that takes a date argument as the value.
|
||||
#
|
||||
# # config/initializers/time_formats.rb
|
||||
# Date::DATE_FORMATS[:month_and_year] = "%B %Y"
|
||||
# Date::DATE_FORMATS[:short_ordinal] = lambda { |date| date.strftime("%B #{date.day.ordinalize}") }
|
||||
def to_formatted_s(format = :default)
|
||||
if formatter = DATE_FORMATS[format]
|
||||
if formatter.respond_to?(:call)
|
||||
formatter.call(self).to_s
|
||||
else
|
||||
strftime(formatter)
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
|
||||
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
|
||||
def readable_inspect
|
||||
strftime("%a, %d %b %Y")
|
||||
end
|
||||
|
||||
# A method to keep Time, Date and DateTime instances interchangeable on conversions.
|
||||
# In this case, it simply returns +self+.
|
||||
def to_date
|
||||
self
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
# Converts a Date instance to a Time, where the time is set to the beginning of the day.
|
||||
# The timezone can be either :local or :utc (default :local).
|
||||
#
|
||||
# ==== Examples
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_time # => Sat Nov 10 00:00:00 0800 2007
|
||||
# date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007
|
||||
#
|
||||
# date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007
|
||||
def to_time(form = :local)
|
||||
::Time.send("#{form}_time", year, month, day)
|
||||
end
|
||||
|
||||
# Converts a Date instance to a DateTime, where the time is set to the beginning of the day
|
||||
# and UTC offset is set to 0.
|
||||
#
|
||||
# ==== Examples
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_datetime # => Sat, 10 Nov 2007 00:00:00 0000
|
||||
def to_datetime
|
||||
::DateTime.civil(year, month, day, 0, 0, 0, 0)
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
def xmlschema
|
||||
to_time.xmlschema
|
||||
end
|
||||
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
|
||||
#
|
||||
# This method is aliased to <tt>to_s</tt>.
|
||||
#
|
||||
# ==== Examples
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_formatted_s(:db) # => "2007-11-10"
|
||||
# date.to_s(:db) # => "2007-11-10"
|
||||
#
|
||||
# date.to_formatted_s(:short) # => "10 Nov"
|
||||
# date.to_formatted_s(:long) # => "November 10, 2007"
|
||||
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
|
||||
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
|
||||
#
|
||||
# == Adding your own time formats to to_formatted_s
|
||||
# You can add your own formats to the Date::DATE_FORMATS hash.
|
||||
# Use the format name as the hash key and either a strftime string
|
||||
# or Proc instance that takes a date argument as the value.
|
||||
#
|
||||
# # config/initializers/time_formats.rb
|
||||
# Date::DATE_FORMATS[:month_and_year] = "%B %Y"
|
||||
# Date::DATE_FORMATS[:short_ordinal] = lambda { |date| date.strftime("%B #{date.day.ordinalize}") }
|
||||
def to_formatted_s(format = :default)
|
||||
if formatter = DATE_FORMATS[format]
|
||||
if formatter.respond_to?(:call)
|
||||
formatter.call(self).to_s
|
||||
else
|
||||
strftime(formatter)
|
||||
end
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005"
|
||||
def readable_inspect
|
||||
strftime("%a, %d %b %Y")
|
||||
end
|
||||
alias_method :default_inspect, :inspect
|
||||
alias_method :inspect, :readable_inspect
|
||||
|
||||
# A method to keep Time, Date and DateTime instances interchangeable on conversions.
|
||||
# In this case, it simply returns +self+.
|
||||
def to_date
|
||||
self
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
# Converts a Date instance to a Time, where the time is set to the beginning of the day.
|
||||
# The timezone can be either :local or :utc (default :local).
|
||||
#
|
||||
# ==== Examples
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_time # => Sat Nov 10 00:00:00 0800 2007
|
||||
# date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007
|
||||
#
|
||||
# date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007
|
||||
def to_time(form = :local)
|
||||
::Time.send("#{form}_time", year, month, day)
|
||||
end
|
||||
|
||||
# Converts a Date instance to a DateTime, where the time is set to the beginning of the day
|
||||
# and UTC offset is set to 0.
|
||||
#
|
||||
# ==== Examples
|
||||
# date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007
|
||||
#
|
||||
# date.to_datetime # => Sat, 10 Nov 2007 00:00:00 0000
|
||||
def to_datetime
|
||||
::DateTime.civil(year, month, day, 0, 0, 0, 0)
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
def iso8601
|
||||
strftime('%F')
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
alias_method :rfc3339, :iso8601 if RUBY_VERSION < '1.9'
|
||||
|
||||
def xmlschema
|
||||
to_time_in_current_zone.xmlschema
|
||||
end
|
||||
end
|
||||
|
||||
33
activesupport/lib/active_support/core_ext/date/freeze.rb
Normal file
33
activesupport/lib/active_support/core_ext/date/freeze.rb
Normal file
@@ -0,0 +1,33 @@
|
||||
# Date memoizes some instance methods using metaprogramming to wrap
|
||||
# the methods with one that caches the result in an instance variable.
|
||||
#
|
||||
# If a Date is frozen but the memoized method hasn't been called, the
|
||||
# first call will result in a frozen object error since the memo
|
||||
# instance variable is uninitialized.
|
||||
#
|
||||
# Work around by eagerly memoizing before the first freeze.
|
||||
#
|
||||
# Ruby 1.9 uses a preinitialized instance variable so it's unaffected.
|
||||
# This hack is as close as we can get to feature detection:
|
||||
if RUBY_VERSION < '1.9'
|
||||
require 'date'
|
||||
begin
|
||||
::Date.today.freeze.jd
|
||||
rescue => frozen_object_error
|
||||
if frozen_object_error.message =~ /frozen/
|
||||
class Date #:nodoc:
|
||||
def freeze
|
||||
unless frozen?
|
||||
self.class.private_instance_methods(false).each do |m|
|
||||
if m.to_s =~ /\A__\d+__\Z/
|
||||
instance_variable_set(:"@#{m}", [send(m)])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
14
activesupport/lib/active_support/core_ext/date/zones.rb
Normal file
14
activesupport/lib/active_support/core_ext/date/zones.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
require 'date'
|
||||
require 'active_support/core_ext/time/zones'
|
||||
|
||||
class Date
|
||||
# Converts Date to a TimeWithZone in the current zone if Time.zone or Time.zone_default
|
||||
# is set, otherwise converts Date to a Time via Date#to_time
|
||||
def to_time_in_current_zone
|
||||
if ::Time.zone
|
||||
::Time.zone.local(year, month, day)
|
||||
else
|
||||
to_time
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
require 'date'
|
||||
require 'active_support/core_ext/time/behavior'
|
||||
require 'active_support/core_ext/time/zones'
|
||||
require 'active_support/core_ext/date_time/calculations'
|
||||
require 'active_support/core_ext/date_time/conversions'
|
||||
|
||||
class DateTime
|
||||
include ActiveSupport::CoreExtensions::Time::Behavior
|
||||
include ActiveSupport::CoreExtensions::Time::Zones
|
||||
include ActiveSupport::CoreExtensions::DateTime::Calculations
|
||||
include ActiveSupport::CoreExtensions::DateTime::Conversions
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
require 'active_support/core_ext/object/acts_like'
|
||||
|
||||
class DateTime
|
||||
# Duck-types as a Date-like class. See Object#acts_like?.
|
||||
def acts_like_date?
|
||||
true
|
||||
end
|
||||
|
||||
# Duck-types as a Time-like class. See Object#acts_like?.
|
||||
def acts_like_time?
|
||||
true
|
||||
end
|
||||
end
|
||||
@@ -1,126 +1,143 @@
|
||||
require 'rational'
|
||||
require 'rational' unless RUBY_VERSION >= '1.9.2'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module DateTime #:nodoc:
|
||||
# Enables the use of time calculations within DateTime itself
|
||||
module Calculations
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend ClassMethods
|
||||
class DateTime
|
||||
class << self
|
||||
# DateTimes aren't aware of DST rules, so use a consistent non-DST offset when creating a DateTime with an offset in the local zone
|
||||
def local_offset
|
||||
::Time.local(2012).utc_offset.to_r / 86400
|
||||
end
|
||||
|
||||
base.class_eval do
|
||||
alias_method :compare_without_coercion, :<=>
|
||||
alias_method :<=>, :compare_with_coercion
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# DateTimes aren't aware of DST rules, so use a consistent non-DST offset when creating a DateTime with an offset in the local zone
|
||||
def local_offset
|
||||
::Time.local(2007).utc_offset.to_r / 86400
|
||||
end
|
||||
|
||||
def current
|
||||
::Time.zone_default ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
|
||||
end
|
||||
end
|
||||
|
||||
# Tells whether the DateTime object's datetime lies in the past
|
||||
def past?
|
||||
self < ::DateTime.current
|
||||
end
|
||||
|
||||
# Tells whether the DateTime object's datetime lies in the future
|
||||
def future?
|
||||
self > ::DateTime.current
|
||||
end
|
||||
|
||||
# Seconds since midnight: DateTime.now.seconds_since_midnight
|
||||
def seconds_since_midnight
|
||||
self.sec + (self.min * 60) + (self.hour * 3600)
|
||||
end
|
||||
|
||||
# Returns a new DateTime where one or more of the elements have been changed according to the +options+ parameter. The time options
|
||||
# (hour, minute, sec) reset cascadingly, so if only the hour is passed, then minute and sec is set to 0. If the hour and
|
||||
# minute is passed, then sec is set to 0.
|
||||
def change(options)
|
||||
::DateTime.civil(
|
||||
options[:year] || self.year,
|
||||
options[:month] || self.month,
|
||||
options[:day] || self.day,
|
||||
options[:hour] || self.hour,
|
||||
options[:min] || (options[:hour] ? 0 : self.min),
|
||||
options[:sec] || ((options[:hour] || options[:min]) ? 0 : self.sec),
|
||||
options[:offset] || self.offset,
|
||||
options[:start] || self.start
|
||||
)
|
||||
end
|
||||
|
||||
# Uses Date to provide precise Time calculations for years, months, and days.
|
||||
# The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
|
||||
# <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
|
||||
# <tt>:minutes</tt>, <tt>:seconds</tt>.
|
||||
def advance(options)
|
||||
d = to_date.advance(options)
|
||||
datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
|
||||
seconds_to_advance = (options[:seconds] || 0) + (options[:minutes] || 0) * 60 + (options[:hours] || 0) * 3600
|
||||
seconds_to_advance == 0 ? datetime_advanced_by_date : datetime_advanced_by_date.since(seconds_to_advance)
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the time a number of seconds ago
|
||||
# Do not use this method in combination with x.months, use months_ago instead!
|
||||
def ago(seconds)
|
||||
self.since(-seconds)
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the time a number of seconds since the instance time
|
||||
# Do not use this method in combination with x.months, use months_since instead!
|
||||
def since(seconds)
|
||||
self + Rational(seconds.round, 86400)
|
||||
end
|
||||
alias :in :since
|
||||
|
||||
# Returns a new DateTime representing the start of the day (0:00)
|
||||
def beginning_of_day
|
||||
change(:hour => 0)
|
||||
end
|
||||
alias :midnight :beginning_of_day
|
||||
alias :at_midnight :beginning_of_day
|
||||
alias :at_beginning_of_day :beginning_of_day
|
||||
|
||||
# Returns a new DateTime representing the end of the day (23:59:59)
|
||||
def end_of_day
|
||||
change(:hour => 23, :min => 59, :sec => 59)
|
||||
end
|
||||
|
||||
# Adjusts DateTime to UTC by adding its offset value; offset is set to 0
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600
|
||||
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 +0000
|
||||
def utc
|
||||
new_offset(0)
|
||||
end
|
||||
alias_method :getutc, :utc
|
||||
|
||||
# Returns true if offset == 0
|
||||
def utc?
|
||||
offset == 0
|
||||
end
|
||||
|
||||
# Returns the offset value in seconds
|
||||
def utc_offset
|
||||
(offset * 86400).to_i
|
||||
end
|
||||
|
||||
# Layers additional behavior on DateTime#<=> so that Time and ActiveSupport::TimeWithZone instances can be compared with a DateTime
|
||||
def compare_with_coercion(other)
|
||||
other = other.comparable_time if other.respond_to?(:comparable_time)
|
||||
other = other.to_datetime unless other.acts_like?(:date)
|
||||
compare_without_coercion(other)
|
||||
end
|
||||
end
|
||||
# Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise returns <tt>Time.now.to_datetime</tt>.
|
||||
def current
|
||||
::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
|
||||
end
|
||||
end
|
||||
|
||||
# Tells whether the DateTime object's datetime lies in the past
|
||||
def past?
|
||||
self < ::DateTime.current
|
||||
end
|
||||
|
||||
# Tells whether the DateTime object's datetime lies in the future
|
||||
def future?
|
||||
self > ::DateTime.current
|
||||
end
|
||||
|
||||
# Seconds since midnight: DateTime.now.seconds_since_midnight
|
||||
def seconds_since_midnight
|
||||
sec + (min * 60) + (hour * 3600)
|
||||
end
|
||||
|
||||
# Returns a new DateTime where one or more of the elements have been changed according to the +options+ parameter. The time options
|
||||
# (hour, minute, sec) reset cascadingly, so if only the hour is passed, then minute and sec is set to 0. If the hour and
|
||||
# minute is passed, then sec is set to 0.
|
||||
def change(options)
|
||||
::DateTime.civil(
|
||||
options[:year] || year,
|
||||
options[:month] || month,
|
||||
options[:day] || day,
|
||||
options[:hour] || hour,
|
||||
options[:min] || (options[:hour] ? 0 : min),
|
||||
options[:sec] || ((options[:hour] || options[:min]) ? 0 : sec),
|
||||
options[:offset] || offset,
|
||||
options[:start] || start
|
||||
)
|
||||
end
|
||||
|
||||
# Uses Date to provide precise Time calculations for years, months, and days.
|
||||
# The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
|
||||
# <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
|
||||
# <tt>:minutes</tt>, <tt>:seconds</tt>.
|
||||
def advance(options)
|
||||
d = to_date.advance(options)
|
||||
datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
|
||||
seconds_to_advance = (options[:seconds] || 0) + (options[:minutes] || 0) * 60 + (options[:hours] || 0) * 3600
|
||||
seconds_to_advance == 0 ? datetime_advanced_by_date : datetime_advanced_by_date.since(seconds_to_advance)
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the time a number of seconds ago
|
||||
# Do not use this method in combination with x.months, use months_ago instead!
|
||||
def ago(seconds)
|
||||
since(-seconds)
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the time a number of seconds since the instance time
|
||||
# Do not use this method in combination with x.months, use months_since instead!
|
||||
def since(seconds)
|
||||
self + Rational(seconds.round, 86400)
|
||||
end
|
||||
alias :in :since
|
||||
|
||||
# Returns a new DateTime representing the start of the day (0:00)
|
||||
def beginning_of_day
|
||||
change(:hour => 0)
|
||||
end
|
||||
alias :midnight :beginning_of_day
|
||||
alias :at_midnight :beginning_of_day
|
||||
alias :at_beginning_of_day :beginning_of_day
|
||||
|
||||
# Returns a new DateTime representing the end of the day (23:59:59)
|
||||
def end_of_day
|
||||
change(:hour => 23, :min => 59, :sec => 59)
|
||||
end
|
||||
|
||||
# Returns a new DateTime representing the start of the hour (hh:00:00)
|
||||
def beginning_of_hour
|
||||
change(:min => 0)
|
||||
end
|
||||
alias :at_beginning_of_hour :beginning_of_hour
|
||||
|
||||
# Returns a new DateTime representing the end of the hour (hh:59:59)
|
||||
def end_of_hour
|
||||
change(:min => 59, :sec => 59)
|
||||
end
|
||||
|
||||
# 1.9.3 defines + and - on DateTime, < 1.9.3 do not.
|
||||
if DateTime.public_instance_methods(false).include?(:+)
|
||||
def plus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
other.since(self)
|
||||
else
|
||||
plus_without_duration(other)
|
||||
end
|
||||
end
|
||||
alias_method :plus_without_duration, :+
|
||||
alias_method :+, :plus_with_duration
|
||||
|
||||
def minus_with_duration(other) #:nodoc:
|
||||
if ActiveSupport::Duration === other
|
||||
plus_with_duration(-other)
|
||||
else
|
||||
minus_without_duration(other)
|
||||
end
|
||||
end
|
||||
alias_method :minus_without_duration, :-
|
||||
alias_method :-, :minus_with_duration
|
||||
end
|
||||
|
||||
# Adjusts DateTime to UTC by adding its offset value; offset is set to 0
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600
|
||||
# DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)).utc # => Mon, 21 Feb 2005 16:11:12 +0000
|
||||
def utc
|
||||
new_offset(0)
|
||||
end
|
||||
alias_method :getutc, :utc
|
||||
|
||||
# Returns true if offset == 0
|
||||
def utc?
|
||||
offset == 0
|
||||
end
|
||||
|
||||
# Returns the offset value in seconds
|
||||
def utc_offset
|
||||
(offset * 86400).to_i
|
||||
end
|
||||
|
||||
# Layers additional behavior on DateTime#<=> so that Time and ActiveSupport::TimeWithZone instances can be compared with a DateTime
|
||||
def <=>(other)
|
||||
super other.to_datetime
|
||||
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