mirror of
https://github.com/github/rails.git
synced 2026-01-12 08:08:31 -05:00
Compare commits
15 Commits
github34
...
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"
|
||||
16
Gemfile.sh
16
Gemfile.sh
@@ -1,7 +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 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -212,7 +212,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def load_session(env)
|
||||
|
||||
@@ -201,7 +201,7 @@ 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
|
||||
@@ -213,7 +213,7 @@ module ActionController
|
||||
end
|
||||
|
||||
def generate_sid
|
||||
ActiveSupport::SecureRandom.hex(16)
|
||||
SecureRandom.hex(16)
|
||||
end
|
||||
|
||||
def destroy(env)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
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,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
|
||||
|
||||
@@ -1,107 +1,103 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module DateTime #:nodoc:
|
||||
# Converting datetimes to formatted strings, dates, and times.
|
||||
module Conversions
|
||||
def self.append_features(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :default_inspect, :inspect
|
||||
alias_method :to_default_s, :to_s unless (instance_methods(false) & [:to_s, 'to_s']).empty?
|
||||
require 'active_support/inflector/methods'
|
||||
require 'active_support/core_ext/time/conversions'
|
||||
require 'active_support/core_ext/date_time/calculations'
|
||||
require 'active_support/values/time_zone'
|
||||
|
||||
# Ruby 1.9 has DateTime#to_time which internally relies on Time. We define our own #to_time which allows
|
||||
# DateTimes outside the range of what can be created with Time.
|
||||
remove_method :to_time if instance_methods.include?(:to_time)
|
||||
end
|
||||
class DateTime
|
||||
# Ruby 1.9 has DateTime#to_time which internally relies on Time. We define our own #to_time which allows
|
||||
# DateTimes outside the range of what can be created with Time.
|
||||
remove_method :to_time if instance_methods.include?(:to_time)
|
||||
|
||||
super
|
||||
|
||||
base.class_eval do
|
||||
alias_method :to_s, :to_formatted_s
|
||||
alias_method :inspect, :readable_inspect
|
||||
end
|
||||
end
|
||||
|
||||
# Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
|
||||
#
|
||||
# This method is aliased to <tt>to_s</tt>.
|
||||
#
|
||||
# === Examples
|
||||
# datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
|
||||
#
|
||||
# datetime.to_formatted_s(:db) # => "2007-12-04 00:00:00"
|
||||
# datetime.to_s(:db) # => "2007-12-04 00:00:00"
|
||||
# datetime.to_s(:number) # => "20071204000000"
|
||||
# datetime.to_formatted_s(:short) # => "04 Dec 00:00"
|
||||
# datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
|
||||
# datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
|
||||
# datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
|
||||
#
|
||||
# == Adding your own datetime formats to to_formatted_s
|
||||
# DateTime formats are shared with Time. You can add your own to the
|
||||
# Time::DATE_FORMATS hash. Use the format name as the hash key and
|
||||
# either a strftime string or Proc instance that takes a time or
|
||||
# datetime argument as the value.
|
||||
#
|
||||
# # config/initializers/time_formats.rb
|
||||
# Time::DATE_FORMATS[:month_and_year] = "%B %Y"
|
||||
# Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }
|
||||
def to_formatted_s(format = :default)
|
||||
return to_default_s unless formatter = ::Time::DATE_FORMATS[format]
|
||||
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
|
||||
end
|
||||
|
||||
# Returns the +utc_offset+ as an +HH:MM formatted string. Examples:
|
||||
#
|
||||
# datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24))
|
||||
# datetime.formatted_offset # => "-06:00"
|
||||
# datetime.formatted_offset(false) # => "-0600"
|
||||
def formatted_offset(colon = true, alternate_utc_string = nil)
|
||||
utc? && alternate_utc_string || utc_offset.to_utc_offset_s(colon)
|
||||
end
|
||||
|
||||
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005 14:30:00 +0000"
|
||||
def readable_inspect
|
||||
to_s(:rfc822)
|
||||
end
|
||||
|
||||
# Converts self to a Ruby Date object; time portion is discarded
|
||||
def to_date
|
||||
::Date.new(year, month, day)
|
||||
end
|
||||
|
||||
# Attempts to convert self to a Ruby Time object; returns self if out of range of Ruby Time class
|
||||
# If self has an offset other than 0, self will just be returned unaltered, since there's no clean way to map it to a Time
|
||||
def to_time
|
||||
self.offset == 0 ? ::Time.utc_time(year, month, day, hour, min, sec) : self
|
||||
end
|
||||
|
||||
# To be able to keep Times, Dates and DateTimes interchangeable on conversions
|
||||
def to_datetime
|
||||
self
|
||||
end
|
||||
|
||||
# Converts datetime to an appropriate format for use in XML
|
||||
def xmlschema
|
||||
strftime("%Y-%m-%dT%H:%M:%S%Z")
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
# Converts self to a floating-point number of seconds since the Unix epoch
|
||||
def to_f
|
||||
seconds_since_unix_epoch.to_f
|
||||
end
|
||||
|
||||
# Converts self to an integer number of seconds since the Unix epoch
|
||||
def to_i
|
||||
seconds_since_unix_epoch.to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seconds_since_unix_epoch
|
||||
seconds_per_day = 86_400
|
||||
(self - ::DateTime.civil(1970)) * seconds_per_day
|
||||
end
|
||||
end
|
||||
# Convert to a formatted string. See Time::DATE_FORMATS for predefined formats.
|
||||
#
|
||||
# This method is aliased to <tt>to_s</tt>.
|
||||
#
|
||||
# === Examples
|
||||
# datetime = DateTime.civil(2007, 12, 4, 0, 0, 0, 0) # => Tue, 04 Dec 2007 00:00:00 +0000
|
||||
#
|
||||
# datetime.to_formatted_s(:db) # => "2007-12-04 00:00:00"
|
||||
# datetime.to_s(:db) # => "2007-12-04 00:00:00"
|
||||
# datetime.to_s(:number) # => "20071204000000"
|
||||
# datetime.to_formatted_s(:short) # => "04 Dec 00:00"
|
||||
# datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
|
||||
# datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
|
||||
# datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
|
||||
#
|
||||
# == Adding your own datetime formats to to_formatted_s
|
||||
# DateTime formats are shared with Time. You can add your own to the
|
||||
# Time::DATE_FORMATS hash. Use the format name as the hash key and
|
||||
# either a strftime string or Proc instance that takes a time or
|
||||
# datetime argument as the value.
|
||||
#
|
||||
# # config/initializers/time_formats.rb
|
||||
# Time::DATE_FORMATS[:month_and_year] = "%B %Y"
|
||||
# Time::DATE_FORMATS[:short_ordinal] = lambda { |time| time.strftime("%B #{time.day.ordinalize}") }
|
||||
def to_formatted_s(format = :default)
|
||||
if formatter = ::Time::DATE_FORMATS[format]
|
||||
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
|
||||
else
|
||||
to_default_s
|
||||
end
|
||||
end
|
||||
alias_method :to_default_s, :to_s unless (instance_methods(false) & [:to_s, 'to_s']).empty?
|
||||
alias_method :to_s, :to_formatted_s
|
||||
|
||||
# Returns the +utc_offset+ as an +HH:MM formatted string. Examples:
|
||||
#
|
||||
# datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24))
|
||||
# datetime.formatted_offset # => "-06:00"
|
||||
# datetime.formatted_offset(false) # => "-0600"
|
||||
def formatted_offset(colon = true, alternate_utc_string = nil)
|
||||
utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon)
|
||||
end
|
||||
|
||||
# Overrides the default inspect method with a human readable one, e.g., "Mon, 21 Feb 2005 14:30:00 +0000".
|
||||
def readable_inspect
|
||||
to_s(:rfc822)
|
||||
end
|
||||
alias_method :default_inspect, :inspect
|
||||
alias_method :inspect, :readable_inspect
|
||||
|
||||
# Converts self to a Ruby Date object; time portion is discarded.
|
||||
def to_date
|
||||
::Date.new(year, month, day)
|
||||
end unless instance_methods(false).include?(:to_date)
|
||||
|
||||
# Attempts to convert self to a Ruby Time object; returns self if out of range of Ruby Time class.
|
||||
# If self has an offset other than 0, self will just be returned unaltered, since there's no clean way to map it to a Time.
|
||||
def to_time
|
||||
self.offset == 0 ? ::Time.utc_time(year, month, day, hour, min, sec, sec_fraction * (RUBY_VERSION < '1.9' ? 86400000000 : 1000000)) : self
|
||||
end
|
||||
|
||||
# To be able to keep Times, Dates and DateTimes interchangeable on conversions.
|
||||
def to_datetime
|
||||
self
|
||||
end unless instance_methods(false).include?(:to_datetime)
|
||||
|
||||
def self.civil_from_format(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0)
|
||||
offset = utc_or_local.to_sym == :local ? local_offset : 0
|
||||
civil(year, month, day, hour, min, sec, offset)
|
||||
end
|
||||
|
||||
# Converts datetime to an appropriate format for use in XML.
|
||||
def xmlschema
|
||||
strftime("%Y-%m-%dT%H:%M:%S%Z")
|
||||
end unless instance_methods(false).include?(:xmlschema)
|
||||
|
||||
# Converts self to a floating-point number of seconds since the Unix epoch.
|
||||
def to_f
|
||||
seconds_since_unix_epoch.to_f
|
||||
end
|
||||
|
||||
# Converts self to an integer number of seconds since the Unix epoch.
|
||||
def to_i
|
||||
seconds_since_unix_epoch.to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def seconds_since_unix_epoch
|
||||
seconds_per_day = 86_400
|
||||
(self - ::DateTime.civil(1970)) * seconds_per_day
|
||||
end
|
||||
end
|
||||
|
||||
21
activesupport/lib/active_support/core_ext/date_time/zones.rb
Normal file
21
activesupport/lib/active_support/core_ext/date_time/zones.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
require 'active_support/core_ext/time/zones'
|
||||
|
||||
class DateTime
|
||||
# Returns the simultaneous time in <tt>Time.zone</tt>.
|
||||
#
|
||||
# Time.zone = 'Hawaii' # => 'Hawaii'
|
||||
# DateTime.new(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
|
||||
#
|
||||
# This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
|
||||
# instead of the operating system's time zone.
|
||||
#
|
||||
# You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
|
||||
# and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
|
||||
#
|
||||
# DateTime.new(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
|
||||
def in_time_zone(zone = ::Time.zone)
|
||||
return self unless zone
|
||||
|
||||
ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone))
|
||||
end
|
||||
end
|
||||
@@ -1,43 +0,0 @@
|
||||
class Object
|
||||
# Can you safely .dup this object?
|
||||
# False for nil, false, true, symbols, and numbers; true otherwise.
|
||||
def duplicable?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class NilClass #:nodoc:
|
||||
def duplicable?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class FalseClass #:nodoc:
|
||||
def duplicable?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class TrueClass #:nodoc:
|
||||
def duplicable?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class Symbol #:nodoc:
|
||||
def duplicable?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class Numeric #:nodoc:
|
||||
def duplicable?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class Class #:nodoc:
|
||||
def duplicable?
|
||||
false
|
||||
end
|
||||
end
|
||||
@@ -1,6 +1,41 @@
|
||||
require 'active_support/ordered_hash'
|
||||
|
||||
module Enumerable
|
||||
# Ruby 1.8.7 introduces group_by, but the result isn't ordered. Override it.
|
||||
remove_method(:group_by) if [].respond_to?(:group_by) && RUBY_VERSION < '1.9'
|
||||
|
||||
# Collect an enumerable into sets, grouped by the result of a block. Useful,
|
||||
# for example, for grouping records by date.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# latest_transcripts.group_by(&:day).each do |day, transcripts|
|
||||
# p "#{day} -> #{transcripts.map(&:class).join(', ')}"
|
||||
# end
|
||||
# "2006-03-01 -> Transcript"
|
||||
# "2006-02-28 -> Transcript"
|
||||
# "2006-02-27 -> Transcript, Transcript"
|
||||
# "2006-02-26 -> Transcript, Transcript"
|
||||
# "2006-02-25 -> Transcript"
|
||||
# "2006-02-24 -> Transcript, Transcript"
|
||||
# "2006-02-23 -> Transcript"
|
||||
def group_by
|
||||
return to_enum :group_by unless block_given?
|
||||
assoc = ActiveSupport::OrderedHash.new
|
||||
|
||||
each do |element|
|
||||
key = yield(element)
|
||||
|
||||
if assoc.has_key?(key)
|
||||
assoc[key] << element
|
||||
else
|
||||
assoc[key] = [element]
|
||||
end
|
||||
end
|
||||
|
||||
assoc
|
||||
end unless [].respond_to?(:group_by)
|
||||
|
||||
# Calculates a sum from the elements. Examples:
|
||||
#
|
||||
# payments.sum { |p| p.price * p.tax_rate }
|
||||
@@ -8,7 +43,7 @@ module Enumerable
|
||||
#
|
||||
# The latter is a shortcut for:
|
||||
#
|
||||
# payments.inject { |sum, p| sum + p.price }
|
||||
# payments.inject(0) { |sum, p| sum + p.price }
|
||||
#
|
||||
# It can also calculate the sum without the use of a block.
|
||||
#
|
||||
@@ -24,34 +59,69 @@ module Enumerable
|
||||
if block_given?
|
||||
map(&block).sum(identity)
|
||||
else
|
||||
inject { |sum, element| sum + element } || identity
|
||||
inject(:+) || identity
|
||||
end
|
||||
end
|
||||
|
||||
# Iterates over a collection, passing the current element *and* the
|
||||
# +memo+ to the block. Handy for building up hashes or
|
||||
# reducing collections down to one object. Examples:
|
||||
#
|
||||
# %w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase }
|
||||
# # => {'foo' => 'FOO', 'bar' => 'BAR'}
|
||||
#
|
||||
# *Note* that you can't use immutable objects like numbers, true or false as
|
||||
# the memo. You would think the following returns 120, but since the memo is
|
||||
# never changed, it does not.
|
||||
#
|
||||
# (1..5).each_with_object(1) { |value, memo| memo *= value } # => 1
|
||||
#
|
||||
def each_with_object(memo)
|
||||
return to_enum :each_with_object, memo unless block_given?
|
||||
each do |element|
|
||||
yield element, memo
|
||||
end
|
||||
memo
|
||||
end unless [].respond_to?(:each_with_object)
|
||||
|
||||
# Convert an enumerable to a hash. Examples:
|
||||
#
|
||||
# people.index_by(&:login)
|
||||
# => { "nextangle" => <Person ...>, "chade-" => <Person ...>, ...}
|
||||
# people.index_by { |person| "#{person.first_name} #{person.last_name}" }
|
||||
# => { "Chade- Fowlersburg-e" => <Person ...>, "David Heinemeier Hansson" => <Person ...>, ...}
|
||||
#
|
||||
#
|
||||
def index_by
|
||||
inject({}) do |accum, elem|
|
||||
accum[yield(elem)] = elem
|
||||
accum
|
||||
end
|
||||
end
|
||||
|
||||
# Returns true if the collection has more than 1 element. Functionally equivalent to collection.size > 1.
|
||||
# Works with a block too ala any?, so people.many? { |p| p.age > 26 } # => returns true if more than 1 person is over 26.
|
||||
def many?(&block)
|
||||
size = block_given? ? select(&block).size : self.size
|
||||
size > 1
|
||||
return to_enum :index_by unless block_given?
|
||||
Hash[map { |elem| [yield(elem), elem] }]
|
||||
end
|
||||
|
||||
|
||||
# The negative of the Enumerable#include?. Returns true if the collection does not include the object.
|
||||
# Returns true if the enumerable has more than 1 element. Functionally equivalent to enum.to_a.size > 1.
|
||||
# Can be called with a block too, much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns true if more than one person is over 26.
|
||||
def many?
|
||||
cnt = 0
|
||||
if block_given?
|
||||
any? do |element|
|
||||
cnt += 1 if yield element
|
||||
cnt > 1
|
||||
end
|
||||
else
|
||||
any?{ (cnt += 1) > 1 }
|
||||
end
|
||||
end
|
||||
|
||||
# The negative of the <tt>Enumerable#include?</tt>. Returns true if the collection does not include the object.
|
||||
def exclude?(object)
|
||||
!include?(object)
|
||||
end
|
||||
end
|
||||
|
||||
class Range #:nodoc:
|
||||
# Optimize range sum to use arithmetic progression if a block is not given and
|
||||
# we have a range of numeric values.
|
||||
def sum(identity = 0)
|
||||
return super if block_given? || !(first.instance_of?(Integer) && last.instance_of?(Integer))
|
||||
actual_last = exclude_end? ? (last - 1) : last
|
||||
(actual_last - first + 1) * (actual_last + first) / 2
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
module ActiveSupport
|
||||
if RUBY_VERSION >= '1.9'
|
||||
FrozenObjectError = RuntimeError
|
||||
else
|
||||
FrozenObjectError = TypeError
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Turn all this into using the BacktraceCleaner.
|
||||
class Exception # :nodoc:
|
||||
def clean_message
|
||||
Pathname.clean_within message
|
||||
end
|
||||
|
||||
TraceSubstitutions = []
|
||||
FrameworkStart = /action_controller\/dispatcher\.rb/.freeze
|
||||
FrameworkRegexp = /generated|vendor|dispatch|ruby|script\/\w+/.freeze
|
||||
|
||||
def clean_backtrace
|
||||
backtrace.collect do |line|
|
||||
Pathname.clean_within(TraceSubstitutions.inject(line) do |result, (regexp, sub)|
|
||||
result.gsub regexp, sub
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def application_backtrace
|
||||
before_framework_frame = nil
|
||||
before_application_frame = true
|
||||
|
||||
trace = clean_backtrace.reject do |line|
|
||||
before_framework_frame ||= (line =~ FrameworkStart)
|
||||
non_app_frame = (line =~ FrameworkRegexp)
|
||||
before_application_frame = false unless non_app_frame
|
||||
before_framework_frame || (non_app_frame && !before_application_frame)
|
||||
end
|
||||
|
||||
# If we didn't find any application frames, return an empty app trace.
|
||||
before_application_frame ? [] : trace
|
||||
end
|
||||
|
||||
def framework_backtrace
|
||||
clean_backtrace.grep FrameworkRegexp
|
||||
end
|
||||
FrozenObjectError = RUBY_VERSION < '1.9' ? TypeError : RuntimeError
|
||||
end
|
||||
|
||||
@@ -1,5 +1,2 @@
|
||||
require 'active_support/core_ext/file/atomic'
|
||||
|
||||
class File #:nodoc:
|
||||
extend ActiveSupport::CoreExtensions::File::Atomic
|
||||
end
|
||||
require 'active_support/core_ext/file/path'
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module File #:nodoc:
|
||||
module Atomic
|
||||
# Write to a file atomically. Useful for situations where you don't
|
||||
# want other processes or threads to see half-written files.
|
||||
#
|
||||
# File.atomic_write("important.file") do |file|
|
||||
# file.write("hello")
|
||||
# end
|
||||
#
|
||||
# If your temp directory is not on the same filesystem as the file you're
|
||||
# trying to write, you can provide a different temporary directory.
|
||||
#
|
||||
# File.atomic_write("/data/something.important", "/data/tmp") do |f|
|
||||
# file.write("hello")
|
||||
# end
|
||||
def atomic_write(file_name, temp_dir = Dir.tmpdir)
|
||||
require 'tempfile' unless defined?(Tempfile)
|
||||
require 'fileutils' unless defined?(FileUtils)
|
||||
class File
|
||||
# Write to a file atomically. Useful for situations where you don't
|
||||
# want other processes or threads to see half-written files.
|
||||
#
|
||||
# File.atomic_write("important.file") do |file|
|
||||
# file.write("hello")
|
||||
# end
|
||||
#
|
||||
# If your temp directory is not on the same filesystem as the file you're
|
||||
# trying to write, you can provide a different temporary directory.
|
||||
#
|
||||
# File.atomic_write("/data/something.important", "/data/tmp") do |file|
|
||||
# file.write("hello")
|
||||
# end
|
||||
def self.atomic_write(file_name, temp_dir = Dir.tmpdir)
|
||||
require 'tempfile' unless defined?(Tempfile)
|
||||
require 'fileutils' unless defined?(FileUtils)
|
||||
|
||||
temp_file = Tempfile.new(basename(file_name), temp_dir)
|
||||
yield temp_file
|
||||
temp_file.close
|
||||
temp_file = Tempfile.new(basename(file_name), temp_dir)
|
||||
temp_file.binmode
|
||||
yield temp_file
|
||||
temp_file.close
|
||||
|
||||
begin
|
||||
# Get original file permissions
|
||||
old_stat = stat(file_name)
|
||||
rescue Errno::ENOENT
|
||||
# No old permissions, write a temp file to determine the defaults
|
||||
check_name = join(dirname(file_name), ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}")
|
||||
open(check_name, "w") { }
|
||||
old_stat = stat(check_name)
|
||||
unlink(check_name)
|
||||
end
|
||||
begin
|
||||
# Get original file permissions
|
||||
old_stat = stat(file_name)
|
||||
rescue Errno::ENOENT
|
||||
# No old permissions, write a temp file to determine the defaults
|
||||
check_name = join(dirname(file_name), ".permissions_check.#{Thread.current.object_id}.#{Process.pid}.#{rand(1000000)}")
|
||||
open(check_name, "w") { }
|
||||
old_stat = stat(check_name)
|
||||
unlink(check_name)
|
||||
end
|
||||
|
||||
# Overwrite original file with temp file
|
||||
FileUtils.mv(temp_file.path, file_name)
|
||||
# Overwrite original file with temp file
|
||||
FileUtils.mv(temp_file.path, file_name)
|
||||
|
||||
# Set correct permissions on new file
|
||||
chown(old_stat.uid, old_stat.gid, file_name)
|
||||
chmod(old_stat.mode, file_name)
|
||||
end
|
||||
end
|
||||
# Set correct permissions on new file
|
||||
begin
|
||||
chown(old_stat.uid, old_stat.gid, file_name)
|
||||
# This operation will affect filesystem ACL's
|
||||
chmod(old_stat.mode, file_name)
|
||||
rescue Errno::EPERM
|
||||
# Changing file ownership failed, moving on.
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
5
activesupport/lib/active_support/core_ext/file/path.rb
Normal file
5
activesupport/lib/active_support/core_ext/file/path.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class File
|
||||
unless File.allocate.respond_to?(:to_path)
|
||||
alias to_path path
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1 @@
|
||||
require 'active_support/core_ext/float/rounding'
|
||||
require 'active_support/core_ext/float/time'
|
||||
|
||||
class Float #:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Float::Rounding
|
||||
include ActiveSupport::CoreExtensions::Float::Time
|
||||
end
|
||||
|
||||
@@ -1,24 +1,19 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Float #:nodoc:
|
||||
module Rounding
|
||||
def self.included(base) #:nodoc:
|
||||
base.class_eval do
|
||||
alias_method :round_without_precision, :round
|
||||
alias_method :round, :round_with_precision
|
||||
end
|
||||
end
|
||||
class Float
|
||||
alias precisionless_round round
|
||||
private :precisionless_round
|
||||
|
||||
# Rounds the float with the specified precision.
|
||||
#
|
||||
# x = 1.337
|
||||
# x.round # => 1
|
||||
# x.round(1) # => 1.3
|
||||
# x.round(2) # => 1.34
|
||||
def round_with_precision(precision = nil)
|
||||
precision.nil? ? round_without_precision : (self * (10 ** precision)).round / (10 ** precision).to_f
|
||||
end
|
||||
end
|
||||
# Rounds the float with the specified precision.
|
||||
#
|
||||
# x = 1.337
|
||||
# x.round # => 1
|
||||
# x.round(1) # => 1.3
|
||||
# x.round(2) # => 1.34
|
||||
def round(precision = nil)
|
||||
if precision
|
||||
magnitude = 10.0 ** precision
|
||||
(self * magnitude).round / magnitude
|
||||
else
|
||||
precisionless_round
|
||||
end
|
||||
end
|
||||
end
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Float #:nodoc:
|
||||
module Time
|
||||
# Deprication helper methods not available as core_ext is loaded first.
|
||||
def years
|
||||
::ActiveSupport::Deprecation.warn(self.class.deprecated_method_warning(:years, "Fractional years are not respected. Convert value to integer before calling #years."), caller)
|
||||
years_without_deprecation
|
||||
end
|
||||
def months
|
||||
::ActiveSupport::Deprecation.warn(self.class.deprecated_method_warning(:months, "Fractional months are not respected. Convert value to integer before calling #months."), caller)
|
||||
months_without_deprecation
|
||||
end
|
||||
|
||||
def months_without_deprecation
|
||||
ActiveSupport::Duration.new(self * 30.days, [[:months, self]])
|
||||
end
|
||||
alias :month :months
|
||||
|
||||
def years_without_deprecation
|
||||
ActiveSupport::Duration.new(self * 365.25.days, [[:years, self]])
|
||||
end
|
||||
alias :year :years
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +1,9 @@
|
||||
%w(keys indifferent_access deep_merge reverse_merge conversions diff slice except).each do |ext|
|
||||
require "active_support/core_ext/hash/#{ext}"
|
||||
end
|
||||
|
||||
class Hash #:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Hash::Keys
|
||||
include ActiveSupport::CoreExtensions::Hash::IndifferentAccess
|
||||
include ActiveSupport::CoreExtensions::Hash::DeepMerge
|
||||
include ActiveSupport::CoreExtensions::Hash::ReverseMerge
|
||||
include ActiveSupport::CoreExtensions::Hash::Conversions
|
||||
include ActiveSupport::CoreExtensions::Hash::Diff
|
||||
include ActiveSupport::CoreExtensions::Hash::Slice
|
||||
include ActiveSupport::CoreExtensions::Hash::Except
|
||||
end
|
||||
require 'active_support/core_ext/hash/conversions'
|
||||
require 'active_support/core_ext/hash/deep_merge'
|
||||
require 'active_support/core_ext/hash/deep_dup'
|
||||
require 'active_support/core_ext/hash/diff'
|
||||
require 'active_support/core_ext/hash/except'
|
||||
require 'active_support/core_ext/hash/indifferent_access'
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
require 'active_support/core_ext/hash/reverse_merge'
|
||||
require 'active_support/core_ext/hash/slice'
|
||||
|
||||
@@ -1,247 +1,157 @@
|
||||
require 'date'
|
||||
require 'active_support/core_ext/module/attribute_accessors'
|
||||
require 'active_support/xml_mini'
|
||||
require 'active_support/time'
|
||||
require 'active_support/core_ext/array/wrap'
|
||||
require 'active_support/core_ext/hash/reverse_merge'
|
||||
require 'active_support/core_ext/object/blank'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
# these accessors are here because people using ActiveResource and REST to integrate with other systems
|
||||
# have to be able to control the default behavior of rename_key. dasherize_xml is set to true to emulate
|
||||
# existing behavior. In a future version it should be set to false by default.
|
||||
mattr_accessor :dasherize_xml
|
||||
mattr_accessor :camelize_xml
|
||||
self.dasherize_xml = true
|
||||
self.camelize_xml = false
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
module Conversions
|
||||
# This module exists to decorate files deserialized using Hash.from_xml with
|
||||
# the <tt>original_filename</tt> and <tt>content_type</tt> methods.
|
||||
module FileLike #:nodoc:
|
||||
attr_writer :original_filename, :content_type
|
||||
class Hash
|
||||
# Returns a string containing an XML representation of its receiver:
|
||||
#
|
||||
# {"foo" => 1, "bar" => 2}.to_xml
|
||||
# # =>
|
||||
# # <?xml version="1.0" encoding="UTF-8"?>
|
||||
# # <hash>
|
||||
# # <foo type="integer">1</foo>
|
||||
# # <bar type="integer">2</bar>
|
||||
# # </hash>
|
||||
#
|
||||
# To do so, the method loops over the pairs and builds nodes that depend on
|
||||
# the _values_. Given a pair +key+, +value+:
|
||||
#
|
||||
# * If +value+ is a hash there's a recursive call with +key+ as <tt>:root</tt>.
|
||||
#
|
||||
# * If +value+ is an array there's a recursive call with +key+ as <tt>:root</tt>,
|
||||
# and +key+ singularized as <tt>:children</tt>.
|
||||
#
|
||||
# * If +value+ is a callable object it must expect one or two arguments. Depending
|
||||
# on the arity, the callable is invoked with the +options+ hash as first argument
|
||||
# with +key+ as <tt>:root</tt>, and +key+ singularized as second argument. The
|
||||
# callable can add nodes by using <tt>options[:builder]</tt>.
|
||||
#
|
||||
# "foo".to_xml(lambda { |options, key| options[:builder].b(key) })
|
||||
# # => "<b>foo</b>"
|
||||
#
|
||||
# * If +value+ responds to +to_xml+ the method is invoked with +key+ as <tt>:root</tt>.
|
||||
#
|
||||
# class Foo
|
||||
# def to_xml(options)
|
||||
# options[:builder].bar "fooing!"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# {:foo => Foo.new}.to_xml(:skip_instruct => true)
|
||||
# # => "<hash><bar>fooing!</bar></hash>"
|
||||
#
|
||||
# * Otherwise, a node with +key+ as tag is created with a string representation of
|
||||
# +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
|
||||
# Unless the option <tt>:skip_types</tt> exists and is true, an attribute "type" is
|
||||
# added as well according to the following mapping:
|
||||
#
|
||||
# XML_TYPE_NAMES = {
|
||||
# "Symbol" => "symbol",
|
||||
# "Fixnum" => "integer",
|
||||
# "Bignum" => "integer",
|
||||
# "BigDecimal" => "decimal",
|
||||
# "Float" => "float",
|
||||
# "TrueClass" => "boolean",
|
||||
# "FalseClass" => "boolean",
|
||||
# "Date" => "date",
|
||||
# "DateTime" => "datetime",
|
||||
# "Time" => "datetime"
|
||||
# }
|
||||
#
|
||||
# By default the root node is "hash", but that's configurable via the <tt>:root</tt> option.
|
||||
#
|
||||
# The default XML builder is a fresh instance of <tt>Builder::XmlMarkup</tt>. You can
|
||||
# configure your own builder with the <tt>:builder</tt> option. The method also accepts
|
||||
# options like <tt>:dasherize</tt> and friends, they are forwarded to the builder.
|
||||
def to_xml(options = {})
|
||||
require 'active_support/builder' unless defined?(Builder)
|
||||
|
||||
def original_filename
|
||||
@original_filename || 'untitled'
|
||||
end
|
||||
options = options.dup
|
||||
options[:indent] ||= 2
|
||||
options[:root] ||= "hash"
|
||||
options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
|
||||
|
||||
def content_type
|
||||
@content_type || 'application/octet-stream'
|
||||
end
|
||||
end
|
||||
builder = options[:builder]
|
||||
builder.instruct! unless options.delete(:skip_instruct)
|
||||
|
||||
XML_TYPE_NAMES = {
|
||||
"Symbol" => "symbol",
|
||||
"Fixnum" => "integer",
|
||||
"Bignum" => "integer",
|
||||
"BigDecimal" => "decimal",
|
||||
"Float" => "float",
|
||||
"TrueClass" => "boolean",
|
||||
"FalseClass" => "boolean",
|
||||
"Date" => "date",
|
||||
"DateTime" => "datetime",
|
||||
"Time" => "datetime",
|
||||
"ActiveSupport::TimeWithZone" => "datetime"
|
||||
} unless defined?(XML_TYPE_NAMES)
|
||||
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
|
||||
|
||||
XML_FORMATTING = {
|
||||
"symbol" => Proc.new { |symbol| symbol.to_s },
|
||||
"date" => Proc.new { |date| date.to_s(:db) },
|
||||
"datetime" => Proc.new { |time| time.xmlschema },
|
||||
"binary" => Proc.new { |binary| ActiveSupport::Base64.encode64(binary) },
|
||||
"yaml" => Proc.new { |yaml| yaml.to_yaml }
|
||||
} unless defined?(XML_FORMATTING)
|
||||
|
||||
# TODO: use Time.xmlschema instead of Time.parse;
|
||||
# use regexp instead of Date.parse
|
||||
unless defined?(XML_PARSING)
|
||||
XML_PARSING = {
|
||||
"symbol" => Proc.new { |symbol| symbol.to_sym },
|
||||
"date" => Proc.new { |date| ::Date.parse(date) },
|
||||
"datetime" => Proc.new { |time| ::Time.parse(time).utc rescue ::DateTime.parse(time).utc },
|
||||
"integer" => Proc.new { |integer| integer.to_i },
|
||||
"float" => Proc.new { |float| float.to_f },
|
||||
"decimal" => Proc.new { |number| BigDecimal(number) },
|
||||
"boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
|
||||
"string" => Proc.new { |string| string.to_s },
|
||||
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
|
||||
"base64Binary" => Proc.new { |bin| ActiveSupport::Base64.decode64(bin) },
|
||||
"file" => Proc.new do |file, entity|
|
||||
f = StringIO.new(ActiveSupport::Base64.decode64(file))
|
||||
f.extend(FileLike)
|
||||
f.original_filename = entity['name']
|
||||
f.content_type = entity['content_type']
|
||||
f
|
||||
end
|
||||
}
|
||||
|
||||
XML_PARSING.update(
|
||||
"double" => XML_PARSING["float"],
|
||||
"dateTime" => XML_PARSING["datetime"]
|
||||
)
|
||||
end
|
||||
|
||||
def self.included(klass)
|
||||
klass.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Converts a hash into a string suitable for use as a URL query string. An optional <tt>namespace</tt> can be
|
||||
# passed to enclose the param names (see example below).
|
||||
#
|
||||
# ==== Examples
|
||||
# { :name => 'David', :nationality => 'Danish' }.to_query # => "name=David&nationality=Danish"
|
||||
#
|
||||
# { :name => 'David', :nationality => 'Danish' }.to_query('user') # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
|
||||
def to_query(namespace = nil)
|
||||
collect do |key, value|
|
||||
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
|
||||
end.sort * '&'
|
||||
end
|
||||
|
||||
alias_method :to_param, :to_query
|
||||
|
||||
def to_xml(options = {})
|
||||
require 'builder' unless defined?(Builder)
|
||||
|
||||
options = options.dup
|
||||
options[:indent] ||= 2
|
||||
options.reverse_merge!({ :builder => Builder::XmlMarkup.new(:indent => options[:indent]),
|
||||
:root => "hash" })
|
||||
options[:builder].instruct! unless options.delete(:skip_instruct)
|
||||
root = rename_key(options[:root].to_s, options)
|
||||
|
||||
options[:builder].__send__(:method_missing, root) do
|
||||
each do |key, value|
|
||||
case value
|
||||
when ::Hash
|
||||
value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
|
||||
when ::Array
|
||||
value.to_xml(options.merge({ :root => key, :children => key.to_s.singularize, :skip_instruct => true}))
|
||||
when ::Method, ::Proc
|
||||
# If the Method or Proc takes two arguments, then
|
||||
# pass the suggested child element name. This is
|
||||
# used if the Method or Proc will be operating over
|
||||
# multiple records and needs to create an containing
|
||||
# element that will contain the objects being
|
||||
# serialized.
|
||||
if 1 == value.arity
|
||||
value.call(options.merge({ :root => key, :skip_instruct => true }))
|
||||
else
|
||||
value.call(options.merge({ :root => key, :skip_instruct => true }), key.to_s.singularize)
|
||||
end
|
||||
else
|
||||
if value.respond_to?(:to_xml)
|
||||
value.to_xml(options.merge({ :root => key, :skip_instruct => true }))
|
||||
else
|
||||
type_name = XML_TYPE_NAMES[value.class.name]
|
||||
|
||||
key = rename_key(key.to_s, options)
|
||||
|
||||
attributes = options[:skip_types] || value.nil? || type_name.nil? ? { } : { :type => type_name }
|
||||
if value.nil?
|
||||
attributes[:nil] = true
|
||||
end
|
||||
|
||||
options[:builder].tag!(key,
|
||||
XML_FORMATTING[type_name] ? XML_FORMATTING[type_name].call(value) : value,
|
||||
attributes
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
yield options[:builder] if block_given?
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def rename_key(key, options = {})
|
||||
camelize = options.has_key?(:camelize) ? options[:camelize] : ActiveSupport.camelize_xml
|
||||
dasherize = options.has_key?(:dasherize) ? options[:dasherize] : ActiveSupport.dasherize_xml
|
||||
key = key.camelize if camelize
|
||||
key = key.dasherize if dasherize
|
||||
key
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def from_xml(xml)
|
||||
typecast_xml_value(unrename_keys(XmlMini.parse(xml)))
|
||||
end
|
||||
|
||||
private
|
||||
def typecast_xml_value(value)
|
||||
case value.class.to_s
|
||||
when 'Hash'
|
||||
if value['type'] == 'array'
|
||||
child_key, entries = value.detect { |k,v| k != 'type' } # child_key is throwaway
|
||||
if entries.nil? || (c = value['__content__'] && c.blank?)
|
||||
[]
|
||||
else
|
||||
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
|
||||
when "Array"
|
||||
entries.collect { |v| typecast_xml_value(v) }
|
||||
when "Hash"
|
||||
[typecast_xml_value(entries)]
|
||||
else
|
||||
raise "can't typecast #{entries.inspect}"
|
||||
end
|
||||
end
|
||||
elsif value.has_key?("__content__")
|
||||
content = value["__content__"]
|
||||
if parser = XML_PARSING[value["type"]]
|
||||
if parser.arity == 2
|
||||
XML_PARSING[value["type"]].call(content, value)
|
||||
else
|
||||
XML_PARSING[value["type"]].call(content)
|
||||
end
|
||||
else
|
||||
content
|
||||
end
|
||||
elsif value['type'] == 'string' && value['nil'] != 'true'
|
||||
""
|
||||
# blank or nil parsed values are represented by nil
|
||||
elsif value.blank? || value['nil'] == 'true'
|
||||
nil
|
||||
# If the type is the only element which makes it then
|
||||
# this still makes the value nil, except if type is
|
||||
# a XML node(where type['value'] is a Hash)
|
||||
elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash)
|
||||
nil
|
||||
else
|
||||
xml_value = value.inject({}) do |h,(k,v)|
|
||||
h[k] = typecast_xml_value(v)
|
||||
h
|
||||
end
|
||||
|
||||
# Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
|
||||
# how multipart uploaded files from HTML appear
|
||||
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
|
||||
end
|
||||
when 'Array'
|
||||
value.map! { |i| typecast_xml_value(i) }
|
||||
case value.length
|
||||
when 0 then nil
|
||||
when 1 then value.first
|
||||
else value
|
||||
end
|
||||
when 'String'
|
||||
value
|
||||
else
|
||||
raise "can't typecast #{value.class.name} - #{value.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def unrename_keys(params)
|
||||
case params.class.to_s
|
||||
when "Hash"
|
||||
params.inject({}) do |h,(k,v)|
|
||||
h[k.to_s.tr("-", "_")] = unrename_keys(v)
|
||||
h
|
||||
end
|
||||
when "Array"
|
||||
params.map { |v| unrename_keys(v) }
|
||||
else
|
||||
params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
builder.__send__(:method_missing, root) do
|
||||
each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
|
||||
yield builder if block_given?
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def from_xml(xml)
|
||||
typecast_xml_value(unrename_keys(ActiveSupport::XmlMini.parse(xml)))
|
||||
end
|
||||
|
||||
private
|
||||
def typecast_xml_value(value)
|
||||
case value.class.to_s
|
||||
when 'Hash'
|
||||
if value['type'] == 'array'
|
||||
_, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) })
|
||||
if entries.nil? || (c = value['__content__'] && c.blank?)
|
||||
[]
|
||||
else
|
||||
case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
|
||||
when "Array"
|
||||
entries.collect { |v| typecast_xml_value(v) }
|
||||
when "Hash"
|
||||
[typecast_xml_value(entries)]
|
||||
else
|
||||
raise "can't typecast #{entries.inspect}"
|
||||
end
|
||||
end
|
||||
elsif value['type'] == 'file' ||
|
||||
(value["__content__"] && (value.keys.size == 1 || value["__content__"].present?))
|
||||
content = value["__content__"]
|
||||
if parser = ActiveSupport::XmlMini::PARSING[value["type"]]
|
||||
parser.arity == 1 ? parser.call(content) : parser.call(content, value)
|
||||
else
|
||||
content
|
||||
end
|
||||
elsif value['type'] == 'string' && value['nil'] != 'true'
|
||||
""
|
||||
# blank or nil parsed values are represented by nil
|
||||
elsif value.blank? || value['nil'] == 'true'
|
||||
nil
|
||||
# If the type is the only element which makes it then
|
||||
# this still makes the value nil, except if type is
|
||||
# a XML node(where type['value'] is a Hash)
|
||||
elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash)
|
||||
nil
|
||||
else
|
||||
xml_value = Hash[value.map { |k,v| [k, typecast_xml_value(v)] }]
|
||||
|
||||
# Turn { :files => { :file => #<StringIO> } into { :files => #<StringIO> } so it is compatible with
|
||||
# how multipart uploaded files from HTML appear
|
||||
xml_value["file"].is_a?(StringIO) ? xml_value["file"] : xml_value
|
||||
end
|
||||
when 'Array'
|
||||
value.map! { |i| typecast_xml_value(i) }
|
||||
value.length > 1 ? value : value.first
|
||||
when 'String'
|
||||
value
|
||||
else
|
||||
raise "can't typecast #{value.class.name} - #{value.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def unrename_keys(params)
|
||||
case params.class.to_s
|
||||
when "Hash"
|
||||
Hash[params.map { |k,v| [k.to_s.tr("-", "_"), unrename_keys(v)] } ]
|
||||
when "Array"
|
||||
params.map { |v| unrename_keys(v) }
|
||||
else
|
||||
params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
18
activesupport/lib/active_support/core_ext/hash/deep_dup.rb
Normal file
18
activesupport/lib/active_support/core_ext/hash/deep_dup.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
class Hash
|
||||
# Returns a deep copy of hash.
|
||||
#
|
||||
# hash = { :a => { :b => 'b' } }
|
||||
# dup = hash.deep_dup
|
||||
# dup[:a][:c] = 'c'
|
||||
#
|
||||
# hash[:a][:c] #=> nil
|
||||
# dup[:a][:c] #=> "c"
|
||||
def deep_dup
|
||||
duplicate = self.dup
|
||||
duplicate.each_pair do |k,v|
|
||||
tv = duplicate[k]
|
||||
duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v
|
||||
end
|
||||
duplicate
|
||||
end
|
||||
end
|
||||
@@ -1,23 +1,21 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
# Allows for deep merging
|
||||
module DeepMerge
|
||||
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
||||
def deep_merge(other_hash)
|
||||
self.merge(other_hash) do |key, oldval, newval|
|
||||
oldval = oldval.to_hash if oldval.respond_to?(:to_hash)
|
||||
newval = newval.to_hash if newval.respond_to?(:to_hash)
|
||||
oldval.class.to_s == 'Hash' && newval.class.to_s == 'Hash' ? oldval.deep_merge(newval) : newval
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
||||
#
|
||||
# h1 = {:x => {:y => [4,5,6]}, :z => [7,8,9]}
|
||||
# h2 = {:x => {:y => [7,8,9]}, :z => "xyz"}
|
||||
#
|
||||
# h1.deep_merge(h2) #=> { :x => {:y => [7, 8, 9]}, :z => "xyz" }
|
||||
# h2.deep_merge(h1) #=> { :x => {:y => [4, 5, 6]}, :z => [7, 8, 9] }
|
||||
def deep_merge(other_hash)
|
||||
dup.deep_merge!(other_hash)
|
||||
end
|
||||
|
||||
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
||||
# Modifies the receiver in place.
|
||||
def deep_merge!(other_hash)
|
||||
replace(deep_merge(other_hash))
|
||||
end
|
||||
end
|
||||
# Same as +deep_merge+, but modifies +self+.
|
||||
def deep_merge!(other_hash)
|
||||
other_hash.each_pair do |k,v|
|
||||
tv = self[k]
|
||||
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
|
||||
end
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
module Diff
|
||||
# Returns a hash that represents the difference between two hashes.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# {1 => 2}.diff(1 => 2) # => {}
|
||||
# {1 => 2}.diff(1 => 3) # => {1 => 2}
|
||||
# {}.diff(1 => 2) # => {1 => 2}
|
||||
# {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
|
||||
def diff(h2)
|
||||
self.dup.delete_if { |k, v| h2[k] == v }.merge(h2.dup.delete_if { |k, v| self.has_key?(k) })
|
||||
end
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
# Returns a hash that represents the difference between two hashes.
|
||||
#
|
||||
# Examples:
|
||||
#
|
||||
# {1 => 2}.diff(1 => 2) # => {}
|
||||
# {1 => 2}.diff(1 => 3) # => {1 => 2}
|
||||
# {}.diff(1 => 2) # => {1 => 2}
|
||||
# {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
|
||||
def diff(h2)
|
||||
dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
require 'set'
|
||||
class Hash
|
||||
# Return a hash that includes everything but the given keys. This is useful for
|
||||
# limiting a set of parameters to everything but a few known toggles:
|
||||
#
|
||||
# @person.update_attributes(params[:person].except(:admin))
|
||||
#
|
||||
# If the receiver responds to +convert_key+, the method is called on each of the
|
||||
# arguments. This allows +except+ to play nice with hashes with indifferent access
|
||||
# for instance:
|
||||
#
|
||||
# {:a => 1}.with_indifferent_access.except(:a) # => {}
|
||||
# {:a => 1}.with_indifferent_access.except("a") # => {}
|
||||
#
|
||||
def except(*keys)
|
||||
dup.except!(*keys)
|
||||
end
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
# Return a hash that includes everything but the given keys. This is useful for
|
||||
# limiting a set of parameters to everything but a few known toggles:
|
||||
#
|
||||
# @person.update_attributes(params[:person].except(:admin))
|
||||
module Except
|
||||
# Returns a new hash without the given keys.
|
||||
def except(*keys)
|
||||
dup.except!(*keys)
|
||||
end
|
||||
|
||||
# Replaces the hash without the given keys.
|
||||
def except!(*keys)
|
||||
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
end
|
||||
end
|
||||
end
|
||||
# Replaces the hash without the given keys.
|
||||
def except!(*keys)
|
||||
keys.each { |key| delete(key) }
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,143 +1,24 @@
|
||||
# This class has dubious semantics and we only have it so that
|
||||
# people can write params[:key] instead of params['key']
|
||||
# and they get the same value for both keys.
|
||||
require 'active_support/hash_with_indifferent_access'
|
||||
|
||||
class HashWithIndifferentAccess < Hash
|
||||
def initialize(constructor = {})
|
||||
if constructor.is_a?(Hash)
|
||||
super()
|
||||
update(constructor)
|
||||
else
|
||||
super(constructor)
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
|
||||
def default(key = nil)
|
||||
if key.is_a?(Symbol) && include?(key = key.to_s)
|
||||
self[key]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
||||
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
||||
|
||||
# Assigns a new value to the hash:
|
||||
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
|
||||
#
|
||||
# hash = HashWithIndifferentAccess.new
|
||||
# hash[:key] = "value"
|
||||
# {:a => 1}.with_indifferent_access["a"] # => 1
|
||||
#
|
||||
def []=(key, value)
|
||||
regular_writer(convert_key(key), convert_value(value))
|
||||
def with_indifferent_access
|
||||
ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self)
|
||||
end
|
||||
|
||||
# Updates the instantized hash with values from the second:
|
||||
#
|
||||
# hash_1 = HashWithIndifferentAccess.new
|
||||
# hash_1[:key] = "value"
|
||||
#
|
||||
# hash_2 = HashWithIndifferentAccess.new
|
||||
# hash_2[:key] = "New Value!"
|
||||
#
|
||||
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
|
||||
#
|
||||
def update(other_hash)
|
||||
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
|
||||
self
|
||||
end
|
||||
|
||||
alias_method :merge!, :update
|
||||
|
||||
# Checks the hash for a key matching the argument passed in:
|
||||
# Called when object is nested under an object that receives
|
||||
# #with_indifferent_access. This method will be called on the current object
|
||||
# by the enclosing object and is aliased to #with_indifferent_access by
|
||||
# default. Subclasses of Hash may overwrite this method to return +self+ if
|
||||
# converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
|
||||
# desirable.
|
||||
#
|
||||
# hash = HashWithIndifferentAccess.new
|
||||
# hash["key"] = "value"
|
||||
# hash.key? :key # => true
|
||||
# hash.key? "key" # => true
|
||||
# b = {:b => 1}
|
||||
# {:a => b}.with_indifferent_access["a"] # calls b.nested_under_indifferent_access
|
||||
#
|
||||
def key?(key)
|
||||
super(convert_key(key))
|
||||
end
|
||||
|
||||
alias_method :include?, :key?
|
||||
alias_method :has_key?, :key?
|
||||
alias_method :member?, :key?
|
||||
|
||||
# Fetches the value for the specified key, same as doing hash[key]
|
||||
def fetch(key, *extras)
|
||||
super(convert_key(key), *extras)
|
||||
end
|
||||
|
||||
# Returns an array of the values at the specified indices:
|
||||
#
|
||||
# hash = HashWithIndifferentAccess.new
|
||||
# hash[:a] = "x"
|
||||
# hash[:b] = "y"
|
||||
# hash.values_at("a", "b") # => ["x", "y"]
|
||||
#
|
||||
def values_at(*indices)
|
||||
indices.collect {|key| self[convert_key(key)]}
|
||||
end
|
||||
|
||||
# Returns an exact copy of the hash.
|
||||
def dup
|
||||
HashWithIndifferentAccess.new(self)
|
||||
end
|
||||
|
||||
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
|
||||
# Does not overwrite the existing hash.
|
||||
def merge(hash)
|
||||
self.dup.update(hash)
|
||||
end
|
||||
|
||||
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
|
||||
# This overloaded definition prevents returning a regular hash, if reverse_merge is called on a HashWithDifferentAccess.
|
||||
def reverse_merge(other_hash)
|
||||
super other_hash.with_indifferent_access
|
||||
end
|
||||
|
||||
# Removes a specified key from the hash.
|
||||
def delete(key)
|
||||
super(convert_key(key))
|
||||
end
|
||||
|
||||
def stringify_keys!; self end
|
||||
def symbolize_keys!; self end
|
||||
def to_options!; self end
|
||||
|
||||
# Convert to a Hash with String keys.
|
||||
def to_hash
|
||||
Hash.new(default).merge(self)
|
||||
end
|
||||
|
||||
protected
|
||||
def convert_key(key)
|
||||
key.kind_of?(Symbol) ? key.to_s : key
|
||||
end
|
||||
|
||||
def convert_value(value)
|
||||
case value
|
||||
when Hash
|
||||
value.with_indifferent_access
|
||||
when Array
|
||||
value.collect { |e| e.is_a?(Hash) ? e.with_indifferent_access : e }
|
||||
else
|
||||
value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
module IndifferentAccess #:nodoc:
|
||||
def with_indifferent_access
|
||||
hash = HashWithIndifferentAccess.new(self)
|
||||
hash.default = self.default
|
||||
hash
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
alias nested_under_indifferent_access with_indifferent_access
|
||||
end
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
module Keys
|
||||
# Return a new hash with all keys converted to strings.
|
||||
def stringify_keys
|
||||
inject({}) do |options, (key, value)|
|
||||
options[key.to_s] = value
|
||||
options
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
# Return a new hash with all keys converted to strings.
|
||||
#
|
||||
# { :name => 'Rob', :years => '28' }.stringify_keys
|
||||
# #=> { "name" => "Rob", "years" => "28" }
|
||||
def stringify_keys
|
||||
dup.stringify_keys!
|
||||
end
|
||||
|
||||
# Destructively convert all keys to strings.
|
||||
def stringify_keys!
|
||||
keys.each do |key|
|
||||
self[key.to_s] = delete(key)
|
||||
end
|
||||
self
|
||||
end
|
||||
# Destructively convert all keys to strings. Same as
|
||||
# +stringify_keys+, but modifies +self+.
|
||||
def stringify_keys!
|
||||
keys.each do |key|
|
||||
self[key.to_s] = delete(key)
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
# Return a new hash with all keys converted to symbols.
|
||||
def symbolize_keys
|
||||
inject({}) do |options, (key, value)|
|
||||
options[(key.to_sym rescue key) || key] = value
|
||||
options
|
||||
end
|
||||
end
|
||||
# Return a new hash with all keys converted to symbols, as long as
|
||||
# they respond to +to_sym+.
|
||||
#
|
||||
# { 'name' => 'Rob', 'years' => '28' }.symbolize_keys
|
||||
# #=> { :name => "Rob", :years => "28" }
|
||||
def symbolize_keys
|
||||
dup.symbolize_keys!
|
||||
end
|
||||
|
||||
# Destructively convert all keys to symbols.
|
||||
def symbolize_keys!
|
||||
self.replace(self.symbolize_keys)
|
||||
end
|
||||
# Destructively convert all keys to symbols, as long as they respond
|
||||
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
|
||||
def symbolize_keys!
|
||||
keys.each do |key|
|
||||
self[(key.to_sym rescue key) || key] = delete(key)
|
||||
end
|
||||
self
|
||||
end
|
||||
|
||||
alias_method :to_options, :symbolize_keys
|
||||
alias_method :to_options!, :symbolize_keys!
|
||||
alias_method :to_options, :symbolize_keys
|
||||
alias_method :to_options!, :symbolize_keys!
|
||||
|
||||
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
||||
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
||||
# as keys, this will fail.
|
||||
#
|
||||
# ==== Examples
|
||||
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
|
||||
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
|
||||
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
||||
def assert_valid_keys(*valid_keys)
|
||||
unknown_keys = keys - [valid_keys].flatten
|
||||
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
||||
end
|
||||
end
|
||||
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
||||
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
||||
# as keys, this will fail.
|
||||
#
|
||||
# ==== Examples
|
||||
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years"
|
||||
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key: name"
|
||||
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
||||
def assert_valid_keys(*valid_keys)
|
||||
valid_keys.flatten!
|
||||
each_key do |k|
|
||||
raise(ArgumentError, "Unknown key: #{k}") unless valid_keys.include?(k)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,35 +1,23 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
# Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
|
||||
# in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
|
||||
#
|
||||
# def setup(options = {})
|
||||
# options.reverse_merge! :size => 25, :velocity => 10
|
||||
# end
|
||||
#
|
||||
# Using <tt>merge</tt>, the above example would look as follows:
|
||||
#
|
||||
# def setup(options = {})
|
||||
# { :size => 25, :velocity => 10 }.merge(options)
|
||||
# end
|
||||
#
|
||||
# The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
|
||||
# have the respective key.
|
||||
module ReverseMerge
|
||||
# Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
|
||||
def reverse_merge(other_hash)
|
||||
other_hash.merge(self)
|
||||
end
|
||||
|
||||
# Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
|
||||
# Modifies the receiver in place.
|
||||
def reverse_merge!(other_hash)
|
||||
replace(reverse_merge(other_hash))
|
||||
end
|
||||
|
||||
alias_method :reverse_update, :reverse_merge!
|
||||
end
|
||||
end
|
||||
class Hash
|
||||
# Merges the caller into +other_hash+. For example,
|
||||
#
|
||||
# options = options.reverse_merge(:size => 25, :velocity => 10)
|
||||
#
|
||||
# is equivalent to
|
||||
#
|
||||
# options = {:size => 25, :velocity => 10}.merge(options)
|
||||
#
|
||||
# This is particularly useful for initializing an options hash
|
||||
# with default values.
|
||||
def reverse_merge(other_hash)
|
||||
other_hash.merge(self)
|
||||
end
|
||||
|
||||
# Destructive +reverse_merge+.
|
||||
def reverse_merge!(other_hash)
|
||||
# right wins if there is no left
|
||||
merge!( other_hash ){|key,left,right| left }
|
||||
end
|
||||
|
||||
alias_method :reverse_update, :reverse_merge!
|
||||
end
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Hash #:nodoc:
|
||||
# Slice a hash to include only the given keys. This is useful for
|
||||
# limiting an options hash to valid keys before passing to a method:
|
||||
#
|
||||
# def search(criteria = {})
|
||||
# assert_valid_keys(:mass, :velocity, :time)
|
||||
# end
|
||||
#
|
||||
# search(options.slice(:mass, :velocity, :time))
|
||||
#
|
||||
# If you have an array of keys you want to limit to, you should splat them:
|
||||
#
|
||||
# valid_keys = [:mass, :velocity, :time]
|
||||
# search(options.slice(*valid_keys))
|
||||
module Slice
|
||||
# Returns a new hash with only the given keys.
|
||||
def slice(*keys)
|
||||
keys = keys.map { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
||||
hash = self.class.new
|
||||
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
||||
hash
|
||||
end
|
||||
class Hash
|
||||
# Slice a hash to include only the given keys. This is useful for
|
||||
# limiting an options hash to valid keys before passing to a method:
|
||||
#
|
||||
# def search(criteria = {})
|
||||
# assert_valid_keys(:mass, :velocity, :time)
|
||||
# end
|
||||
#
|
||||
# search(options.slice(:mass, :velocity, :time))
|
||||
#
|
||||
# If you have an array of keys you want to limit to, you should splat them:
|
||||
#
|
||||
# valid_keys = [:mass, :velocity, :time]
|
||||
# search(options.slice(*valid_keys))
|
||||
def slice(*keys)
|
||||
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
||||
hash = self.class.new
|
||||
keys.each { |k| hash[k] = self[k] if has_key?(k) }
|
||||
hash
|
||||
end
|
||||
|
||||
# Replaces the hash with only the given keys.
|
||||
# Returns a hash contained the removed key/value pairs
|
||||
# {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d =>4}
|
||||
def slice!(*keys)
|
||||
keys = keys.map { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
||||
omit = slice(*(self.keys - keys))
|
||||
hash = slice(*keys)
|
||||
replace(hash)
|
||||
omit
|
||||
end
|
||||
end
|
||||
end
|
||||
# Replaces the hash with only the given keys.
|
||||
# Returns a hash contained the removed key/value pairs
|
||||
# {:a => 1, :b => 2, :c => 3, :d => 4}.slice!(:a, :b) # => {:c => 3, :d => 4}
|
||||
def slice!(*keys)
|
||||
keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
|
||||
omit = slice(*self.keys - keys)
|
||||
hash = slice(*keys)
|
||||
replace(hash)
|
||||
omit
|
||||
end
|
||||
|
||||
# Removes and returns the key/value pairs matching the given keys.
|
||||
# {:a => 1, :b => 2, :c => 3, :d => 4}.extract!(:a, :b) # => {:a => 1, :b => 2}
|
||||
def extract!(*keys)
|
||||
result = {}
|
||||
keys.each {|key| result[key] = delete(key) }
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
require 'active_support/core_ext/integer/even_odd'
|
||||
require 'active_support/core_ext/integer/multiple'
|
||||
require 'active_support/core_ext/integer/inflections'
|
||||
require 'active_support/core_ext/integer/time'
|
||||
|
||||
class Integer #:nodoc:
|
||||
include ActiveSupport::CoreExtensions::Integer::EvenOdd
|
||||
include ActiveSupport::CoreExtensions::Integer::Inflections
|
||||
include ActiveSupport::CoreExtensions::Integer::Time
|
||||
end
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Integer #:nodoc:
|
||||
# For checking if a fixnum is even or odd.
|
||||
#
|
||||
# 2.even? # => true
|
||||
# 2.odd? # => false
|
||||
# 1.even? # => false
|
||||
# 1.odd? # => true
|
||||
# 0.even? # => true
|
||||
# 0.odd? # => false
|
||||
# -1.even? # => false
|
||||
# -1.odd? # => true
|
||||
module EvenOdd
|
||||
def multiple_of?(number)
|
||||
self % number == 0
|
||||
end
|
||||
|
||||
def even?
|
||||
multiple_of? 2
|
||||
end if RUBY_VERSION < '1.9'
|
||||
|
||||
def odd?
|
||||
!even?
|
||||
end if RUBY_VERSION < '1.9'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +1,17 @@
|
||||
require 'active_support/inflector'
|
||||
|
||||
module ActiveSupport #:nodoc:
|
||||
module CoreExtensions #:nodoc:
|
||||
module Integer #:nodoc:
|
||||
module Inflections
|
||||
# Ordinalize turns a number into an ordinal string used to denote the
|
||||
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
|
||||
#
|
||||
# 1.ordinalize # => "1st"
|
||||
# 2.ordinalize # => "2nd"
|
||||
# 1002.ordinalize # => "1002nd"
|
||||
# 1003.ordinalize # => "1003rd"
|
||||
def ordinalize
|
||||
Inflector.ordinalize(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
class Integer
|
||||
# Ordinalize turns a number into an ordinal string used to denote the
|
||||
# position in an ordered sequence such as 1st, 2nd, 3rd, 4th.
|
||||
#
|
||||
# 1.ordinalize # => "1st"
|
||||
# 2.ordinalize # => "2nd"
|
||||
# 1002.ordinalize # => "1002nd"
|
||||
# 1003.ordinalize # => "1003rd"
|
||||
# -11.ordinalize # => "-11th"
|
||||
# -1001.ordinalize # => "-1001st"
|
||||
#
|
||||
def ordinalize
|
||||
ActiveSupport::Inflector.ordinalize(self)
|
||||
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