aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support
diff options
context:
space:
mode:
Diffstat (limited to 'activesupport/lib/active_support')
-rw-r--r--activesupport/lib/active_support/all.rb1
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb10
-rw-r--r--activesupport/lib/active_support/cache.rb111
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb55
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb14
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb3
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb41
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb44
-rw-r--r--activesupport/lib/active_support/callbacks.rb330
-rw-r--r--activesupport/lib/active_support/concern.rb14
-rw-r--r--activesupport/lib/active_support/configurable.rb3
-rw-r--r--activesupport/lib/active_support/core_ext.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/array/prepend_and_append.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/conversions.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/class.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb174
-rw-r--r--activesupport/lib/active_support/core_ext/class/delegating_attributes.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb91
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb26
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/digest/uuid.rb51
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/compact.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb106
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/integer/multiple.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/kernel.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/concern.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/debugger.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb69
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/module.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/module/aliasing.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/module/attr_internal.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb174
-rw-r--r--activesupport/lib/active_support/core_ext/module/concerning.rb135
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb55
-rw-r--r--activesupport/lib/active_support/core_ext/module/deprecation.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/module/method_transplanting.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/module/remove_method.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/name_error.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/bytes.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb38
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb60
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/object/instance_variables.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb197
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_json.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb59
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_query.rb73
-rw-r--r--activesupport/lib/active_support/core_ext/object/try.rb56
-rw-r--r--activesupport/lib/active_support/core_ext/object/with_options.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/range.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/range/each.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/securerandom.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/string/access.rb70
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/string/exclude.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb51
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb25
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb131
-rw-r--r--activesupport/lib/active_support/core_ext/struct.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/thread.rb74
-rw-r--r--activesupport/lib/active_support/core_ext/time.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb57
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/marshal.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb18
-rw-r--r--activesupport/lib/active_support/dependencies.rb100
-rw-r--r--activesupport/lib/active_support/dependencies/autoload.rb2
-rw-r--r--activesupport/lib/active_support/deprecation.rb2
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb2
-rw-r--r--activesupport/lib/active_support/duration.rb39
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb2
-rw-r--r--activesupport/lib/active_support/file_watcher.rb36
-rw-r--r--activesupport/lib/active_support/gem_version.rb15
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb38
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb19
-rw-r--r--activesupport/lib/active_support/inflections.rb6
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb38
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb193
-rw-r--r--activesupport/lib/active_support/inflector/transliterate.rb14
-rw-r--r--activesupport/lib/active_support/json/decoding.rb16
-rw-r--r--activesupport/lib/active_support/json/encoding.rb373
-rw-r--r--activesupport/lib/active_support/key_generator.rb16
-rw-r--r--activesupport/lib/active_support/logger.rb2
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb11
-rw-r--r--activesupport/lib/active_support/message_verifier.rb90
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb3
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb84
-rw-r--r--activesupport/lib/active_support/notifications.rb13
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb28
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb15
-rw-r--r--activesupport/lib/active_support/number_helper.rb364
-rw-r--r--activesupport/lib/active_support/number_helper/number_converter.rb182
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_currency_converter.rb46
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb23
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_converter.rb66
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb58
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb12
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_phone_converter.rb49
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb90
-rw-r--r--activesupport/lib/active_support/option_merger.rb2
-rw-r--r--activesupport/lib/active_support/ordered_hash.rb8
-rw-r--r--activesupport/lib/active_support/ordered_options.rb2
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb17
-rw-r--r--activesupport/lib/active_support/railtie.rb7
-rw-r--r--activesupport/lib/active_support/rescuable.rb4
-rw-r--r--activesupport/lib/active_support/security_utils.rb20
-rw-r--r--activesupport/lib/active_support/subscriber.rb38
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb5
-rw-r--r--activesupport/lib/active_support/test_case.rb68
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb4
-rw-r--r--activesupport/lib/active_support/testing/constant_lookup.rb6
-rw-r--r--activesupport/lib/active_support/testing/declarative.rb19
-rw-r--r--activesupport/lib/active_support/testing/deprecation.rb21
-rw-r--r--activesupport/lib/active_support/testing/file_fixtures.rb34
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb37
-rw-r--r--activesupport/lib/active_support/testing/stream.rb42
-rw-r--r--activesupport/lib/active_support/testing/tagged_logging.rb2
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb132
-rw-r--r--activesupport/lib/active_support/time.rb2
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb130
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb201
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin904483 -> 1001806 bytes
-rw-r--r--activesupport/lib/active_support/version.rb11
-rw-r--r--activesupport/lib/active_support/xml_mini.rb6
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/libxml.rb4
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogiri.rb4
148 files changed, 3569 insertions, 2314 deletions
diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb
index 151008bbaa..f537818300 100644
--- a/activesupport/lib/active_support/all.rb
+++ b/activesupport/lib/active_support/all.rb
@@ -1,4 +1,3 @@
require 'active_support'
-require 'active_support/deprecation'
require 'active_support/time'
require 'active_support/core_ext'
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index c88ae3e661..d06f22ad5c 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -13,7 +13,7 @@ module ActiveSupport
# can focus on the rest.
#
# bc = BacktraceCleaner.new
- # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix
+ # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
# bc.clean(exception.backtrace) # perform the cleanup
#
@@ -22,7 +22,7 @@ module ActiveSupport
# <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 <tt>BacktraceCleaner#remove_filters!<tt>
+ # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt>
# These two methods will give you a completely untouched backtrace.
#
# Inspired by the Quiet Backtrace gem by Thoughtbot.
@@ -65,14 +65,14 @@ module ActiveSupport
@silencers << block
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 one of
+ # Removes all silencers, but leaves in the filters. Useful if your
+ # context of debugging suddenly expands as you suspect a bug in one of
# the libraries you use.
def remove_silencers!
@silencers = []
end
- # Removes all filters, but leaves in silencers. Useful if you suddenly
+ # Removes all filters, but leaves in the silencers. Useful if you suddenly
# need to see entire filepaths in the backtrace that you had already
# filtered out.
def remove_filters!
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 308a1b2cd9..3a1e1ac3a1 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -3,7 +3,7 @@ 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/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/numeric/bytes'
require 'active_support/core_ext/numeric/time'
require 'active_support/core_ext/object/to_param'
@@ -12,10 +12,10 @@ 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 :FileStore, 'active_support/cache/file_store'
+ autoload :MemoryStore, 'active_support/cache/memory_store'
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
- autoload :NullStore, 'active_support/cache/null_store'
+ autoload :NullStore, 'active_support/cache/null_store'
# These options mean something to all cache implementations. Individual cache
# implementations may support additional options.
@@ -88,25 +88,24 @@ module ActiveSupport
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
+ when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
+ else key.to_param
+ end.to_s
+ end
- 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
- when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
- else key.to_param
- end.to_s
- end
-
- # Obtains the specified cache store class, given the name of the +store+.
- # Raises an error when the store class cannot be found.
- def retrieve_store_class(store)
- require "active_support/cache/#{store}"
- rescue LoadError => e
- raise "Could not find cache store adapter for #{store} (#{e})"
- else
- ActiveSupport::Cache.const_get(store.to_s.camelize)
- end
+ # Obtains the specified cache store class, given the name of the +store+.
+ # Raises an error when the store class cannot be found.
+ def retrieve_store_class(store)
+ require "active_support/cache/#{store}"
+ rescue LoadError => e
+ raise "Could not find cache store adapter for #{store} (#{e})"
+ else
+ ActiveSupport::Cache.const_get(store.to_s.camelize)
+ end
end
# An abstract cache store class. There are multiple cache store
@@ -153,7 +152,6 @@ module ActiveSupport
# 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, :instance_writer => true
attr_reader :silence, :options
@@ -180,16 +178,6 @@ 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.
#
@@ -234,9 +222,9 @@ module ActiveSupport
# 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
+ # will continue to use slightly stale data for a just a bit 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.
+ # new value. After that all the processes will start getting the 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
@@ -359,20 +347,19 @@ module ActiveSupport
#
# Options are passed to the underlying cache implementation.
#
- # Returns an array with the data for each of the names. For example:
+ # Returns a hash with the data for each of the names. For example:
#
# cache.write("bim", "bam")
- # cache.fetch_multi("bim", "boom") {|key| key * 2 }
- # #=> ["bam", "boomboom"]
+ # cache.fetch_multi("bim", "boom") { |key| key * 2 }
+ # # => { "bam" => "bam", "boom" => "boomboom" }
#
def fetch_multi(*names)
options = names.extract_options!
options = merged_options(options)
-
results = read_multi(*names, options)
- names.map do |name|
- results.fetch(name) do
+ names.each_with_object({}) do |name, memo|
+ memo[name] = results.fetch(name) do
value = yield name
write(name, value, options)
value
@@ -385,6 +372,7 @@ module ActiveSupport
# Options are passed to the underlying cache implementation.
def write(name, value, options = nil)
options = merged_options(options)
+
instrument(:write, name, options) do
entry = Entry.new(value, options)
write_entry(namespaced_key(name, options), entry, options)
@@ -396,16 +384,18 @@ module ActiveSupport
# Options are passed to the underlying cache implementation.
def delete(name, options = nil)
options = merged_options(options)
+
instrument(:delete, name) do
delete_entry(namespaced_key(name, options), options)
end
end
- # Return +true+ if the cache contains an entry for the given key.
+ # Returns +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
entry = read_entry(namespaced_key(name, options), options)
(entry && !entry.expired?) || false
@@ -451,7 +441,7 @@ module ActiveSupport
# 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.
+ # The options hash is passed to the underlying cache implementation.
#
# All implementations may not support this method.
def clear(options = nil)
@@ -539,13 +529,9 @@ module ActiveSupport
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
+ payload = { :key => key }
+ payload.merge!(options) if options.is_a?(Hash)
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
end
def log(operation, key, options = nil)
@@ -585,6 +571,7 @@ module ActiveSupport
result = instrument(:generate, name, options) do |payload|
yield(name)
end
+
write(name, result, options)
result
end
@@ -608,20 +595,19 @@ module ActiveSupport
else
@value = value
end
+
@created_at = Time.now.to_f
@expires_in = options[:expires_in]
@expires_in = @expires_in.to_f if @expires_in
end
def value
- convert_version_4beta1_entry! if defined?(@v)
compressed? ? uncompress(@value) : @value
end
# Check if the entry is expired. The +expires_in+ parameter can override
# the value set when the entry was created.
def expired?
- convert_version_4beta1_entry! if defined?(@value)
@expires_in && @created_at + @expires_in <= Time.now.to_f
end
@@ -657,7 +643,6 @@ module ActiveSupport
# Duplicate the value in a class. This is used by cache implementations that don't natively
# serialize entries to protect against accidental cache modifications.
def dup_value!
- convert_version_4beta1_entry! if defined?(@v)
if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
if @value.is_a?(String)
@value = @value.dup
@@ -672,8 +657,10 @@ module ActiveSupport
if value && options[:compress]
compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize
+
return true if serialized_value_size >= compress_threshold
end
+
false
end
@@ -688,24 +675,6 @@ module ActiveSupport
def uncompress(value)
Marshal.load(Zlib::Inflate.inflate(value))
end
-
- # The internals of this method changed between Rails 3.x and 4.0. This method provides the glue
- # to ensure that cache entries created under the old version still work with the new class definition.
- def convert_version_4beta1_entry!
- if defined?(@v)
- @value = @v
- remove_instance_variable(:@v)
- end
- if defined?(@c)
- @compressed = @c
- remove_instance_variable(:@c)
- end
- if defined?(@x) && @x
- @created_at ||= Time.now.to_f
- @expires_in = @x - @created_at
- remove_instance_variable(:@x)
- end
- end
end
end
end
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 472f23c1c5..d08ecd2f7d 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -14,6 +14,7 @@ module ActiveSupport
DIR_FORMATTER = "%03X"
FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
+ FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
EXCLUDED_DIRS = ['.', '..'].freeze
def initialize(cache_path, options = nil)
@@ -22,18 +23,19 @@ module ActiveSupport
extend Strategy::LocalCache
end
- # Deletes all items from the cache. In this case it deletes all the entries in the specified
- # file store directory except for .gitkeep. Be careful which directory is specified in your
+ # Deletes all items from the cache. In this case it deletes all the entries in the specified
+ # file store directory except for .gitkeep. Be careful which directory is specified in your
# config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)}
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
end
- # Premptively iterates through all stored keys and removes the ones which have expired.
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
- each_key(options) do |key|
+ search_dir(cache_path) do |fname|
+ key = file_path_key(fname)
entry = read_entry(key, options)
delete_entry(key, options) if entry && entry.expired?
end
@@ -42,33 +44,13 @@ module ActiveSupport
# Increments an already existing integer value that is stored in the cache.
# If the key is not found nothing is done.
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
+ modify_value(name, amount, options)
end
# Decrements an already existing integer value that is stored in the cache.
# If the key is not found nothing is done.
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
+ modify_value(name, -amount, options)
end
def delete_matched(matcher, options = nil)
@@ -96,6 +78,7 @@ module ActiveSupport
def write_entry(key, entry, options)
file_name = key_file_path(key)
+ return false if options[:unless_exist] && File.exist?(file_name)
ensure_cache_path(File.dirname(file_name))
File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
true
@@ -135,6 +118,10 @@ module ActiveSupport
# Translate a key into a file path.
def key_file_path(key)
+ if key.size > FILEPATH_MAX_SIZE
+ key = Digest::MD5.hexdigest(key)
+ end
+
fname = URI.encode_www_form_component(key)
hash = Zlib.adler32(fname)
hash, dir_1 = hash.divmod(0x1000)
@@ -182,6 +169,22 @@ module ActiveSupport
end
end
end
+
+ # Modifies the amount of an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
+ def modify_value(name, amount, options)
+ 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
+ end
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 512296554f..61b4f0b8b0 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -41,17 +41,15 @@ module ActiveSupport
#
# If no addresses are specified, then MemCacheStore will connect to
# localhost port 11211 (the default memcached port).
- #
- # Instead of addresses one can pass in a MemCache-like object. For example:
- #
- # 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)
+ unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
+ raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance."
+ end
+ if addresses.first.is_a?(Dalli::Client)
@data = addresses.first
else
mem_cache_options = options.dup
@@ -87,7 +85,7 @@ module ActiveSupport
instrument(:increment, name, :amount => amount) do
@data.incr(escape_key(namespaced_key(name, options)), amount)
end
- rescue Dalli::DalliError
+ rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
nil
end
@@ -101,7 +99,7 @@ module ActiveSupport
instrument(:decrement, name, :amount => amount) do
@data.decr(escape_key(namespaced_key(name, options)), amount)
end
- rescue Dalli::DalliError
+ rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
nil
end
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index b979521c99..8a0523d0e2 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -36,7 +36,7 @@ module ActiveSupport
end
end
- # Premptively iterates through all stored keys and removes the ones which have expired.
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
instrument(:cleanup, :size => @data.size) do
@@ -124,7 +124,6 @@ module ActiveSupport
protected
- # See https://gist.github.com/ssimeonov/6047200
PER_ENTRY_OVERHEAD = 240
def cached_size(key, entry)
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index fb42c4a41e..73c6b3cb88 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/string/inflections'
+require 'active_support/per_thread_registry'
module ActiveSupport
module Cache
@@ -8,6 +9,8 @@ module ActiveSupport
# duration of a block. Repeated calls to the cache for the same key will hit the
# in-memory cache for faster access.
module LocalCache
+ autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware'
+
# Class for storing and registering the local caches.
class LocalCacheRegistry # :nodoc:
extend ActiveSupport::PerThreadRegistry
@@ -23,6 +26,9 @@ module ActiveSupport
def set_cache_for(local_cache_key, value)
@registry[local_cache_key] = value
end
+
+ def self.set_cache_for(l, v); instance.set_cache_for l, v; end
+ def self.cache_for(l); instance.cache_for l; end
end
# Simple memory backed cache. This cache is not thread safe and is intended only
@@ -60,32 +66,6 @@ module ActiveSupport
def with_local_cache
use_temporary_local_cache(LocalStore.new) { yield }
end
-
- #--
- # This class wraps up local storage for middlewares. Only the middleware method should
- # construct them.
- class Middleware # :nodoc:
- attr_reader :name, :local_cache_key
-
- def initialize(name, local_cache_key)
- @name = name
- @local_cache_key = local_cache_key
- @app = nil
- end
-
- def new(app)
- @app = app
- self
- end
-
- def call(env)
- LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
- @app.call(env)
- ensure
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
- end
- end
-
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
def middleware
@@ -106,13 +86,13 @@ module ActiveSupport
def increment(name, amount = 1, options = nil) # :nodoc:
value = bypass_local_cache{super}
- increment_or_decrement(value, name, amount, options)
+ set_cache_value(value, name, amount, options)
value
end
def decrement(name, amount = 1, options = nil) # :nodoc:
value = bypass_local_cache{super}
- increment_or_decrement(value, name, amount, options)
+ set_cache_value(value, name, amount, options)
value
end
@@ -140,8 +120,7 @@ module ActiveSupport
super
end
- private
- def increment_or_decrement(value, name, amount, options)
+ def set_cache_value(value, name, amount, options)
if local_cache
local_cache.mute do
if value
@@ -153,6 +132,8 @@ module ActiveSupport
end
end
+ private
+
def local_cache_key
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
new file mode 100644
index 0000000000..a6f24b1a3c
--- /dev/null
+++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
@@ -0,0 +1,44 @@
+require 'rack/body_proxy'
+require 'rack/utils'
+
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+
+ #--
+ # This class wraps up local storage for middlewares. Only the middleware method should
+ # construct them.
+ class Middleware # :nodoc:
+ attr_reader :name, :local_cache_key
+
+ def initialize(name, local_cache_key)
+ @name = name
+ @local_cache_key = local_cache_key
+ @app = nil
+ end
+
+ def new(app)
+ @app = app
+ self
+ end
+
+ def call(env)
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) do
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ end
+ response
+ rescue Rack::Utils::InvalidParameterError
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ [400, {}, []]
+ rescue Exception
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ raise
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 5c738572a8..0d5035f637 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -4,17 +4,18 @@ require 'active_support/core_ext/array/extract_options'
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/string/filters'
require 'thread'
module ActiveSupport
- # Callbacks are code hooks that are run at key points in an object's lifecycle.
+ # Callbacks are code hooks that are run at key points in an object's life cycle.
# 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 the events in the object's
- # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+),
+ # life cycle 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+).
@@ -71,25 +72,29 @@ module ActiveSupport
# order.
#
# If the callback chain was halted, returns +false+. Otherwise returns the
- # result of the block, or +true+ if no block is given.
+ # result of the block, +nil+ if no callbacks have been set, or +true+
+ # if callbacks have been set but no block is given.
#
# run_callbacks :save do
# save
# end
def run_callbacks(kind, &block)
- cbs = send("_#{kind}_callbacks")
- if cbs.empty?
- yield if block_given?
+ send "_run_#{kind}_callbacks", &block
+ end
+
+ private
+
+ def _run_callbacks(callbacks, &block)
+ if callbacks.empty?
+ block.call if block
else
- runner = cbs.compile
+ runner = callbacks.compile
e = Filters::Environment.new(self, false, nil, block)
runner.call(e).value
end
end
- private
-
- # A hook invoked everytime a before callback is halted.
+ # A hook invoked every time a before callback is halted.
# This can be overridden in AS::Callback implementors in order
# to provide better debugging/logging.
def halted_callback_hook(filter)
@@ -117,102 +122,106 @@ module ActiveSupport
ENDING = End.new
class Before
- def self.build(next_callback, user_callback, user_conditions, chain_config, filter)
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter)
halted_lambda = chain_config[:terminator]
if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
+ halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
elsif chain_config.key? :terminator
- halting(next_callback, user_callback, halted_lambda, filter)
+ halting(callback_sequence, user_callback, halted_lambda, filter)
elsif user_conditions.any?
- conditional(next_callback, user_callback, user_conditions)
+ conditional(callback_sequence, user_callback, user_conditions)
else
- simple next_callback, user_callback
+ simple callback_sequence, user_callback
end
end
- private
-
- def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
- lambda { |env|
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter)
+ callback_sequence.before do |env|
target = env.target
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
- result = user_callback.call target, value
- env.halted = halted_lambda.call(target, result)
+ result_lambda = -> { user_callback.call target, value }
+ env.halted = halted_lambda.call(target, result_lambda)
if env.halted
target.send :halted_callback_hook, filter
end
end
- next_callback.call env
- }
+
+ env
+ end
end
+ private_class_method :halting_and_conditional
- def self.halting(next_callback, user_callback, halted_lambda, filter)
- lambda { |env|
+ def self.halting(callback_sequence, user_callback, halted_lambda, filter)
+ callback_sequence.before do |env|
target = env.target
value = env.value
halted = env.halted
unless halted
- result = user_callback.call target, value
- env.halted = halted_lambda.call(target, result)
+ result_lambda = -> { user_callback.call target, value }
+ env.halted = halted_lambda.call(target, result_lambda)
+
if env.halted
target.send :halted_callback_hook, filter
end
end
- next_callback.call env
- }
+
+ env
+ end
end
+ private_class_method :halting
- def self.conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.before do |env|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
- next_callback.call env
- }
+
+ env
+ end
end
+ private_class_method :conditional
- def self.simple(next_callback, user_callback)
- lambda { |env|
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.before do |env|
user_callback.call env.target, env.value
- next_callback.call env
- }
+
+ env
+ end
end
+ private_class_method :simple
end
class After
- def self.build(next_callback, user_callback, user_conditions, chain_config)
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
if chain_config[:skip_after_callbacks_if_terminated]
if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(next_callback, user_callback, user_conditions)
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
elsif chain_config.key?(:terminator)
- halting(next_callback, user_callback)
+ halting(callback_sequence, user_callback)
elsif user_conditions.any?
- conditional next_callback, user_callback, user_conditions
+ conditional callback_sequence, user_callback, user_conditions
else
- simple next_callback, user_callback
+ simple callback_sequence, user_callback
end
else
if user_conditions.any?
- conditional next_callback, user_callback, user_conditions
+ conditional callback_sequence, user_callback, user_conditions
else
- simple next_callback, user_callback
+ simple callback_sequence, user_callback
end
end
end
- private
-
- def self.halting_and_conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
- env = next_callback.call env
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.after do |env|
target = env.target
value = env.value
halted = env.halted
@@ -220,118 +229,126 @@ module ActiveSupport
if !halted && user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
+
env
- }
+ end
end
+ private_class_method :halting_and_conditional
- def self.halting(next_callback, user_callback)
- lambda { |env|
- env = next_callback.call env
+ def self.halting(callback_sequence, user_callback)
+ callback_sequence.after do |env|
unless env.halted
user_callback.call env.target, env.value
end
+
env
- }
+ end
end
+ private_class_method :halting
- def self.conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
- env = next_callback.call env
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.after do |env|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call target, value
end
+
env
- }
+ end
end
+ private_class_method :conditional
- def self.simple(next_callback, user_callback)
- lambda { |env|
- env = next_callback.call env
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.after do |env|
user_callback.call env.target, env.value
+
env
- }
+ end
end
+ private_class_method :simple
end
class Around
- def self.build(next_callback, user_callback, user_conditions, chain_config)
+ def self.build(callback_sequence, user_callback, user_conditions, chain_config)
if chain_config.key?(:terminator) && user_conditions.any?
- halting_and_conditional(next_callback, user_callback, user_conditions)
+ halting_and_conditional(callback_sequence, user_callback, user_conditions)
elsif chain_config.key? :terminator
- halting(next_callback, user_callback)
+ halting(callback_sequence, user_callback)
elsif user_conditions.any?
- conditional(next_callback, user_callback, user_conditions)
+ conditional(callback_sequence, user_callback, user_conditions)
else
- simple(next_callback, user_callback)
+ simple(callback_sequence, user_callback)
end
end
- private
-
- def self.halting_and_conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
+ def self.halting_and_conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.around do |env, &run|
target = env.target
value = env.value
halted = env.halted
if !halted && user_conditions.all? { |c| c.call(target, value) }
user_callback.call(target, value) {
- env = next_callback.call env
+ env = run.call env
env.value
}
+
env
else
- next_callback.call env
+ run.call env
end
- }
+ end
end
+ private_class_method :halting_and_conditional
- def self.halting(next_callback, user_callback)
- lambda { |env|
+ def self.halting(callback_sequence, user_callback)
+ callback_sequence.around do |env, &run|
target = env.target
value = env.value
- unless env.halted
+ if env.halted
+ run.call env
+ else
user_callback.call(target, value) {
- env = next_callback.call env
+ env = run.call env
env.value
}
env
- else
- next_callback.call env
end
- }
+ end
end
+ private_class_method :halting
- def self.conditional(next_callback, user_callback, user_conditions)
- lambda { |env|
+ def self.conditional(callback_sequence, user_callback, user_conditions)
+ callback_sequence.around do |env, &run|
target = env.target
value = env.value
if user_conditions.all? { |c| c.call(target, value) }
user_callback.call(target, value) {
- env = next_callback.call env
+ env = run.call env
env.value
}
env
else
- next_callback.call env
+ run.call env
end
- }
+ end
end
+ private_class_method :conditional
- def self.simple(next_callback, user_callback)
- lambda { |env|
+ def self.simple(callback_sequence, user_callback)
+ callback_sequence.around do |env, &run|
user_callback.call(env.target, env.value) {
- env = next_callback.call env
+ env = run.call env
env.value
}
env
- }
+ end
end
+ private_class_method :simple
end
end
@@ -356,14 +373,14 @@ module ActiveSupport
def filter; @key; end
def raw_filter; @filter; end
- def merge(chain, new_options)
+ def merge_conditional_options(chain, if_option:, unless_option:)
options = {
:if => @if.dup,
:unless => @unless.dup
}
- options[:if].concat Array(new_options.fetch(:unless, []))
- options[:unless].concat Array(new_options.fetch(:if, []))
+ options[:if].concat Array(unless_option)
+ options[:unless].concat Array(if_option)
self.class.build chain, @filter, @kind, options
end
@@ -382,17 +399,17 @@ module ActiveSupport
end
# Wraps code with filter
- def apply(next_callback)
+ def apply(callback_sequence)
user_conditions = conditions_lambdas
user_callback = make_lambda @filter
case kind
when :before
- Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter)
+ Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter)
when :after
- Filters::After.build(next_callback, user_callback, user_conditions, chain_config)
+ Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config)
when :around
- Filters::Around.build(next_callback, user_callback, user_conditions, chain_config)
+ Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config)
end
end
@@ -409,15 +426,8 @@ module ActiveSupport
# Procs:: A proc to call with the object.
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
#
- # All of these objects are compiled into methods and handled
- # the same after this point:
- #
- # Symbols:: Already methods.
- # Strings:: class_eval'd into methods.
- # Procs:: using define_method compiled into methods.
- # Objects::
- # a method is created that calls the before_foo method
- # on the object.
+ # All of these objects are converted into a lambda and handled
+ # the same after this point.
def make_lambda(filter)
case filter
when Symbol
@@ -464,16 +474,59 @@ module ActiveSupport
end
end
+ # Execute before and after filters in a sequence instead of
+ # chaining them with nested lambda calls, see:
+ # https://github.com/rails/rails/issues/18011
+ class CallbackSequence
+ def initialize(&call)
+ @call = call
+ @before = []
+ @after = []
+ end
+
+ def before(&before)
+ @before.unshift(before)
+ self
+ end
+
+ def after(&after)
+ @after.push(after)
+ self
+ end
+
+ def around(&around)
+ CallbackSequence.new do |*args|
+ around.call(*args) {
+ self.call(*args)
+ }
+ end
+ end
+
+ def call(*args)
+ @before.each { |b| b.call(*args) }
+ value = @call.call(*args)
+ @after.each { |a| a.call(*args) }
+ value
+ end
+ end
+
# An Array with a compile method.
class CallbackChain #:nodoc:#
include Enumerable
attr_reader :name, :config
+ # If true, any callback returning +false+ will halt the entire callback
+ # chain and display a deprecation message. If false, callback chains will
+ # only be halted by calling +throw :abort+. Defaults to +true+.
+ class_attribute :halt_and_display_warning_on_return_false
+ self.halt_and_display_warning_on_return_false = true
+
def initialize(name, config)
@name = name
@config = {
- :scope => [ :kind ]
+ scope: [:kind],
+ terminator: default_terminator
}.merge!(config)
@chain = []
@callbacks = nil
@@ -508,8 +561,9 @@ module ActiveSupport
def compile
@callbacks || @mutex.synchronize do
- @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback|
- callback.apply chain
+ final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) }
+ @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback|
+ callback.apply callback_sequence
end
end
end
@@ -543,6 +597,28 @@ module ActiveSupport
@callbacks = nil
@chain.delete_if { |c| callback.duplicates?(c) }
end
+
+ def default_terminator
+ Proc.new do |target, result_lambda|
+ terminate = true
+ catch(:abort) do
+ result = result_lambda.call if result_lambda.is_a?(Proc)
+ if halt_and_display_warning_on_return_false && result == false
+ display_deprecation_warning_for_false_terminator
+ else
+ terminate = false
+ end
+ end
+ terminate
+ end
+ end
+
+ def display_deprecation_warning_for_false_terminator
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Returning `false` in a callback will not implicitly halt a callback chain in the next release of Rails.
+ To explicitly halt a callback chain, please use `throw :abort` instead.
+ MSG
+ end
end
module ClassMethods
@@ -556,7 +632,7 @@ module ActiveSupport
# This is used internally to append, prepend and skip callbacks to the
# CallbackChain.
def __update_callbacks(name) #:nodoc:
- ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
+ ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target|
chain = target.get_callbacks name
yield target, chain.dup
end
@@ -566,7 +642,7 @@ module ActiveSupport
#
# set_callback :save, :before, :before_meth
# set_callback :save, :after, :after_meth, if: :condition
- # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+ # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
#
# The second arguments indicates whether the callback is to be run +:before+,
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
@@ -574,10 +650,10 @@ module ActiveSupport
#
# set_callback :save, :before_meth
#
- # The callback can specified as a symbol naming an instance method; as a
+ # The callback can be 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+.
+ # argument to +define_callbacks+.
#
# 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
@@ -625,7 +701,7 @@ module ActiveSupport
filter = chain.find {|c| c.matches?(type, filter) }
if filter && options.any?
- new_filter = filter.merge(chain, options)
+ new_filter = filter.merge_conditional_options(chain, if_option: options[:if], unless_option: options[:unless])
chain.insert(chain.index(filter), new_filter)
end
@@ -648,7 +724,7 @@ module ActiveSupport
self.set_callbacks name, callbacks.dup.clear
end
- # Define sets of events in the object lifecycle that support callbacks.
+ # Define sets of events in the object life cycle that support callbacks.
#
# define_callbacks :validate
# define_callbacks :initialize, :save, :destroy
@@ -656,16 +732,17 @@ module ActiveSupport
# ===== 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 should be a lambda to be executed.
+ # callback chain, preventing following before and around callbacks from
+ # being called and the event from being triggered.
+ # This should be a lambda to be executed.
# The current object and the return result of the callback will be called
# with the lambda.
#
# define_callbacks :validate, terminator: ->(target, result) { 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.
+ # any successive before and around callback is not executed.
+ # Defaults to +false+, meaning no value halts the chain.
#
# * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after
# callbacks should be terminated by the <tt>:terminator</tt> option. By
@@ -716,18 +793,21 @@ module ActiveSupport
# define_callbacks :save, scope: [:name]
#
# would call <tt>Audit#save</tt>.
+ #
+ # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
+ # `!`, `?` or `=`.
def define_callbacks(*names)
options = names.extract_options!
- if options.key?(:terminator) && String === options[:terminator]
- ActiveSupport::Deprecation.warn "String based terminators are deprecated, please use a lambda"
- value = options[:terminator]
- line = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__
- options[:terminator] = lambda { |target, result| target.instance_exec(result, &line) }
- end
names.each do |name|
class_attribute "_#{name}_callbacks"
set_callbacks name, CallbackChain.new(name, options)
+
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _run_#{name}_callbacks(&block)
+ _run_callbacks(_#{name}_callbacks, &block)
+ end
+ RUBY
end
end
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index b796d01dfd..4082d2d464 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -26,7 +26,7 @@ module ActiveSupport
# scope :disabled, -> { where(disabled: true) }
# end
#
- # module ClassMethods
+ # class_methods do
# ...
# end
# end
@@ -95,7 +95,7 @@ module ActiveSupport
# end
#
# class Host
- # include Bar # works, Bar takes care now of its dependencies
+ # include Bar # It works, now Bar takes care of its dependencies
# end
module Concern
class MultipleIncludedBlocks < StandardError #:nodoc:
@@ -114,7 +114,7 @@ module ActiveSupport
return false
else
return false if base < self
- @_dependencies.each { |dep| base.send(:include, dep) }
+ @_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
@@ -130,5 +130,13 @@ module ActiveSupport
super
end
end
+
+ def class_methods(&class_methods_module_definition)
+ mod = const_defined?(:ClassMethods) ?
+ const_get(:ClassMethods) :
+ const_set(:ClassMethods, Module.new)
+
+ mod.module_eval(&class_methods_module_definition)
+ end
end
end
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index e0d39d509f..8256c325af 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -107,7 +107,7 @@ module ActiveSupport
options = names.extract_options!
names.each do |name|
- raise NameError.new('invalid config attribute name') unless name =~ /^[_A-Za-z]\w*$/
+ raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
@@ -122,6 +122,7 @@ module ActiveSupport
send("#{name}=", yield) if block_given?
end
end
+ private :config_accessor
end
# Reads and writes attributes from a configuration <tt>OrderedHash</tt>.
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index 199aa91020..52706c3d7a 100644
--- a/activesupport/lib/active_support/core_ext.rb
+++ b/activesupport/lib/active_support/core_ext.rb
@@ -1,3 +1,4 @@
-Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path|
+DEPRECATED_FILES = ["#{File.dirname(__FILE__)}/core_ext/struct.rb"]
+(Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"] - DEPRECATED_FILES).each do |path|
require path
end
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index 4f1e432b61..ca66d806ef 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -5,6 +5,8 @@ class Array
# %w( a b c d ).from(2) # => ["c", "d"]
# %w( a b c d ).from(10) # => []
# %w().from(0) # => []
+ # %w( a b c d ).from(-2) # => ["c", "d"]
+ # %w( a b c ).from(-10) # => []
def from(position)
self[position, length] || []
end
@@ -15,8 +17,14 @@ class Array
# %w( a b c d ).to(2) # => ["a", "b", "c"]
# %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
# %w().to(0) # => []
+ # %w( a b c d ).to(-2) # => ["a", "b", "c"]
+ # %w( a b c ).to(-10) # => []
def to(position)
- first position + 1
+ if position >= 0
+ take position + 1
+ else
+ self[0..position]
+ end
end
# Equal to <tt>self[1]</tt>.
@@ -48,6 +56,8 @@ class Array
end
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
+ #
+ # (1..42).to_a.forty_two # => 42
def forty_two
self[41]
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 76ffd23ed1..080e3b5ef7 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -92,7 +92,7 @@ class Array
if empty?
'null'
else
- collect { |element| element.id }.join(',')
+ collect(&:id).join(',')
end
else
to_default_s
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index 37f007c751..87ae052eb0 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -18,6 +18,11 @@ class Array
# ["3", "4"]
# ["5"]
def in_groups_of(number, fill_with = nil)
+ if number.to_i <= 0
+ raise ArgumentError,
+ "Group size must be a positive integer, was #{number.inspect}"
+ end
+
if fill_with == false
collection = self
else
@@ -53,7 +58,7 @@ class Array
# ["4", "5"]
# ["6", "7"]
def in_groups(number, fill_with = nil)
- # size / number gives minor group size;
+ # size.div 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.div number
@@ -83,10 +88,10 @@ class Array
#
# [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, &block)
- if block
+ def split(value = nil)
+ if block_given?
inject([[]]) do |results, element|
- if block.call(element)
+ if yield(element)
results << []
else
results.last << element
@@ -95,9 +100,9 @@ class Array
results
end
else
- results, arr = [[]], self
+ results, arr = [[]], self.dup
until arr.empty?
- if (idx = index(value))
+ if (idx = arr.index(value))
results.last.concat(arr.shift(idx))
arr.shift
results << []
diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
index 27718f19d4..f8d48b69df 100644
--- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
+++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
@@ -1,7 +1,7 @@
class Array
- # The human way of thinking about adding stuff to the end of a list is with append
+ # 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
+ # The human way of thinking about adding stuff to the beginning of a list is with prepend.
alias_method :prepend, :unshift
end \ No newline at end of file
diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
index 39b8cea807..234283e792 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
@@ -1,31 +1,15 @@
require 'bigdecimal'
require 'bigdecimal/util'
-require 'yaml'
class BigDecimal
- YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
-
- 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(*args)
- if args[0].is_a?(Symbol)
- super
+ alias_method :to_default_s, :to_s
+
+ def to_s(format = nil, options = nil)
+ if format.is_a?(Symbol)
+ to_formatted_s(format, options || {})
else
- format = args[0] || DEFAULT_STRING_FORMAT
- _original_to_s(format)
+ to_default_s(format || DEFAULT_STRING_FORMAT)
end
end
- alias_method :_original_to_s, :to_s
- alias_method :to_s, :to_formatted_s
end
diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb
index 86b752c2f3..c750a10bb2 100644
--- a/activesupport/lib/active_support/core_ext/class.rb
+++ b/activesupport/lib/active_support/core_ext/class.rb
@@ -1,4 +1,3 @@
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/subclasses'
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 83038f9da5..f2b7bb3ef1 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -116,9 +116,4 @@ class Class
attr_writer name if instance_writer
end
end
-
- private
- def singleton_class?
- ancestors.first != self
- end
end
diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
index 34859617c9..84d5e95e7a 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
@@ -1,170 +1,4 @@
-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 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|
- raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- unless defined? @@#{sym}
- @@#{sym} = nil
- end
-
- def self.#{sym}
- @@#{sym}
- end
- EOS
-
- unless options[:instance_reader] == false || options[:instance_accessor] == false
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{sym}
- @@#{sym}
- end
- EOS
- end
- 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|
- raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- unless defined? @@#{sym}
- @@#{sym} = nil
- end
-
- def self.#{sym}=(obj)
- @@#{sym} = obj
- end
- EOS
-
- unless options[:instance_writer] == false || options[:instance_accessor] == false
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{sym}=(obj)
- @@#{sym} = obj
- end
- EOS
- end
- send("#{sym}=", yield) if block_given?
- end
- end
-
- # 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, &blk)
- end
-end
+# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
+# but we keep this around for libraries that directly require it knowing they
+# want cattr_*. No need to deprecate.
+require 'active_support/core_ext/module/attribute_accessors'
diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
index ff870f5fd1..1c305c5970 100644
--- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
+++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
@@ -1,25 +1,30 @@
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/module/deprecation'
+
class Class
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}")
+ # methods are overridden.
+ _superclass_delegating_accessor("_#{name}", options)
- # Generate the public methods name, name=, and name?
+ # Generate the public methods name, name=, and name?.
# These methods dispatch to the private _name, and _name= methods, making them
- # overridable
+ # 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
+ # If an instance_reader is needed, generate public instance methods name and name?.
+ if options[:instance_reader] != false
+ define_method(name) { send("_#{name}") }
+ define_method("#{name}?") { !!send("#{name}") }
+ end
end
+ deprecate superclass_delegating_accessor: :class_attribute
+
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
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index af048d0c85..c60e833441 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -129,7 +129,7 @@ class Date
options.fetch(:day, day)
)
end
-
+
# Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there.
def compare_with_coercion(other)
if other.is_a?(Time)
diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb
index 6bc8f12176..df419a6e63 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -1,6 +1,7 @@
require 'date'
require 'active_support/inflector/methods'
require 'active_support/core_ext/date/zones'
+require 'active_support/core_ext/module/remove_method'
class Date
DATE_FORMATS = {
@@ -19,8 +20,10 @@ class Date
# Ruby 1.9 has Date#to_time which converts to localtime only.
remove_method :to_time
- # Ruby 1.9 has Date#xmlschema which converts to a string without the time component.
- remove_method :xmlschema
+ # Ruby 1.9 has Date#xmlschema which converts to a string without the time
+ # component. This removal may generate an issue on FreeBSD, that's why we
+ # need to use remove_possible_method here
+ remove_possible_method :xmlschema
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
#
@@ -37,12 +40,12 @@ class Date
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
# date.to_formatted_s(:iso8601) # => "2007-11-10"
#
- # == Adding your own time formats to to_formatted_s
+ # == Adding your own date 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
+ # # config/initializers/date_formats.rb
# Date::DATE_FORMATS[:month_and_year] = '%B %Y'
# Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") }
def to_formatted_s(format = :default)
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index c869a0e210..9525c10112 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -9,15 +9,26 @@ module DateAndTime
:saturday => 5,
:sunday => 6
}
+ WEEKEND_DAYS = [ 6, 0 ]
# Returns a new date/time representing yesterday.
def yesterday
- advance(:days => -1)
+ advance(days: -1)
+ end
+
+ # Returns a new date/time representing the previous day.
+ def prev_day
+ advance(days: -1)
end
# Returns a new date/time representing tomorrow.
def tomorrow
- advance(:days => 1)
+ advance(days: 1)
+ end
+
+ # Returns a new date/time representing the next day.
+ def next_day
+ advance(days: 1)
end
# Returns true if the date/time is today.
@@ -35,6 +46,11 @@ module DateAndTime
self > self.class.current
end
+ # Returns true if the date/time falls on a Saturday or Sunday.
+ def on_weekend?
+ WEEKEND_DAYS.include?(wday)
+ end
+
# Returns a new date/time the specified number of days ago.
def days_ago(days)
advance(:days => -days)
@@ -111,9 +127,19 @@ module DateAndTime
# Returns a new date/time representing the given day in the next week.
# The +given_day_in_next_week+ defaults to the beginning of the week
# which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+
- # when set. +DateTime+ objects have their time set to 0:00.
- def next_week(given_day_in_next_week = Date.beginning_of_week)
- first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
+ # when set. +DateTime+ objects have their time set to 0:00 unless +same_time+ is true.
+ def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false)
+ result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
+ same_time ? copy_time_to(result) : result
+ end
+
+ # Returns a new date/time representing the next weekday.
+ def next_weekday
+ if next_day.on_weekend?
+ next_week(:monday, same_time: true)
+ else
+ next_day
+ end
end
# Short-hand for months_since(1).
@@ -134,12 +160,23 @@ module DateAndTime
# Returns a new date/time representing the given day in the previous week.
# Week is assumed to start on +start_day+, default is
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
- # DateTime objects have their time set to 0:00.
- def prev_week(start_day = Date.beginning_of_week)
- first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
+ # DateTime objects have their time set to 0:00 unless +same_time+ is true.
+ def prev_week(start_day = Date.beginning_of_week, same_time: false)
+ result = first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
+ same_time ? copy_time_to(result) : result
end
alias_method :last_week, :prev_week
+ # Returns a new date/time representing the previous weekday.
+ def prev_weekday
+ if prev_day.on_weekend?
+ copy_time_to(beginning_of_week(:friday))
+ else
+ prev_day
+ end
+ end
+ alias_method :last_weekday, :prev_weekday
+
# Short-hand for months_ago(1).
def prev_month
months_ago(1)
@@ -213,18 +250,42 @@ module DateAndTime
end
alias :at_end_of_year :end_of_year
- private
+ # Returns a Range representing the whole week of the current date/time.
+ # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
+ def all_week(start_day = Date.beginning_of_week)
+ beginning_of_week(start_day)..end_of_week(start_day)
+ end
- def first_hour(date_or_time)
- date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
+ # Returns a Range representing the whole month of the current date/time.
+ def all_month
+ beginning_of_month..end_of_month
end
- def last_hour(date_or_time)
- date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
+ # Returns a Range representing the whole quarter of the current date/time.
+ def all_quarter
+ beginning_of_quarter..end_of_quarter
end
- def days_span(day)
- (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7
+ # Returns a Range representing the whole year of the current date/time.
+ def all_year
+ beginning_of_year..end_of_year
end
+
+ private
+ def first_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
+ end
+
+ def last_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
+ end
+
+ def days_span(day)
+ (DAYS_INTO_WEEK[day] - DAYS_INTO_WEEK[Date.beginning_of_week]) % 7
+ end
+
+ def copy_time_to(other)
+ other.change(hour: hour, min: min, sec: sec, usec: try(:usec))
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 8e5d723074..55ad384f4f 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -10,7 +10,11 @@ class DateTime
end
end
- # Seconds since midnight: DateTime.now.seconds_since_midnight.
+ # Returns the number of seconds since 00:00:00.
+ #
+ # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0
+ # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296
+ # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399
def seconds_since_midnight
sec + (min * 60) + (hour * 3600)
end
@@ -53,6 +57,16 @@ class DateTime
# <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
# <tt>:minutes</tt>, <tt>:seconds</tt>.
def advance(options)
+ unless options[:weeks].nil?
+ options[:weeks], partial_weeks = options[:weeks].divmod(1)
+ options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
+ end
+
+ unless options[:days].nil?
+ options[:days], partial_days = options[:days].divmod(1)
+ options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
+ end
+
d = to_date.advance(options)
datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
seconds_to_advance = \
@@ -63,7 +77,7 @@ class DateTime
if seconds_to_advance.zero?
datetime_advanced_by_date
else
- datetime_advanced_by_date.since seconds_to_advance
+ datetime_advanced_by_date.since(seconds_to_advance)
end
end
@@ -151,7 +165,13 @@ class DateTime
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
def <=>(other)
- super other.to_datetime
+ if other.kind_of?(Infinity)
+ super
+ elsif other.respond_to? :to_datetime
+ super other.to_datetime
+ else
+ nil
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
index 6ddfb72a0d..2a9c09fc29 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -71,9 +71,9 @@ class DateTime
civil(year, month, day, hour, min, sec, offset)
end
- # Converts +self+ to a floating-point number of seconds since the Unix epoch.
+ # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch.
def to_f
- seconds_since_unix_epoch.to_f
+ seconds_since_unix_epoch.to_f + sec_fraction
end
# Converts +self+ to an integer number of seconds since the Unix epoch.
diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb
new file mode 100644
index 0000000000..593c51bba2
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb
@@ -0,0 +1,51 @@
+require 'securerandom'
+
+module Digest
+ module UUID
+ DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+
+ # Generates a v5 non-random UUID (Universally Unique IDentifier).
+ #
+ # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
+ # uuid_from_hash always generates the same UUID for a given name and namespace combination.
+ #
+ # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt
+ def self.uuid_from_hash(hash_class, uuid_namespace, name)
+ if hash_class == Digest::MD5
+ version = 3
+ elsif hash_class == Digest::SHA1
+ version = 5
+ else
+ raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
+ end
+
+ hash = hash_class.new
+ hash.update(uuid_namespace)
+ hash.update(name)
+
+ ary = hash.digest.unpack('NnnnnN')
+ ary[2] = (ary[2] & 0x0FFF) | (version << 12)
+ ary[3] = (ary[3] & 0x3FFF) | 0x8000
+
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
+ end
+
+ # Convenience method for uuid_from_hash using Digest::MD5.
+ def self.uuid_v3(uuid_namespace, name)
+ self.uuid_from_hash(Digest::MD5, uuid_namespace, name)
+ end
+
+ # Convenience method for uuid_from_hash using Digest::SHA1.
+ def self.uuid_v5(uuid_namespace, name)
+ self.uuid_from_hash(Digest::SHA1, uuid_namespace, name)
+ end
+
+ # Convenience method for SecureRandom.uuid.
+ def self.uuid_v4
+ SecureRandom.uuid
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 4501b7ff58..1343beb87a 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -35,7 +35,7 @@ module Enumerable
if block_given?
Hash[map { |elem| [yield(elem), elem] }]
else
- to_enum :index_by
+ to_enum(:index_by) { size if respond_to?(:size) }
end
end
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
index c3e6124a57..fad6fa8d9d 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -20,10 +20,10 @@ class File
temp_file = Tempfile.new(basename(file_name), temp_dir)
temp_file.binmode
- yield temp_file
+ return_val = yield temp_file
temp_file.close
- if File.exists?(file_name)
+ if File.exist?(file_name)
# Get original file permissions
old_stat = stat(file_name)
else
@@ -40,7 +40,10 @@ class File
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
+
+ # Make sure we return the result of the yielded block
+ return_val
+ rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index 686f12c6da..af4d1da0eb 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/hash/compact'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/except'
@@ -5,3 +6,4 @@ 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'
+require 'active_support/core_ext/hash/transform_values'
diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb
new file mode 100644
index 0000000000..5dc9a05ec7
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/compact.rb
@@ -0,0 +1,20 @@
+class Hash
+ # Returns a hash with non +nil+ values.
+ #
+ # hash = { a: true, b: false, c: nil}
+ # hash.compact # => { a: true, b: false}
+ # hash # => { a: true, b: false, c: nil}
+ # { c: nil }.compact # => {}
+ def compact
+ self.select { |_, value| !value.nil? }
+ end
+
+ # Replaces current hash with non +nil+ values.
+ #
+ # hash = { a: true, b: false, c: nil}
+ # hash.compact! # => { a: true, b: false}
+ # hash # => { a: true, b: false}
+ def compact!
+ self.reject! { |_, value| value.nil? }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index fbf2877117..2149d4439d 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections'
class Hash
# Returns a string containing an XML representation of its receiver:
#
- # {'foo' => 1, 'bar' => 2}.to_xml
+ # { foo: 1, bar: 2 }.to_xml
# # =>
# # <?xml version="1.0" encoding="UTF-8"?>
# # <hash>
@@ -43,7 +43,10 @@ class Hash
# end
#
# { foo: Foo.new }.to_xml(skip_instruct: true)
- # # => "<hash><bar>fooing!</bar></hash>"
+ # # =>
+ # # <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.
@@ -102,7 +105,7 @@ class Hash
# hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
#
- # DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or
+ # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
# <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML.
def from_xml(xml, disallowed_types = nil)
ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
@@ -201,7 +204,7 @@ module ActiveSupport
end
def become_empty_string?(value)
- # {"string" => true}
+ # { "string" => true }
# No tests fail when the second term is removed.
value['type'] == 'string' && value['nil'] != 'true'
end
@@ -218,7 +221,7 @@ module ActiveSupport
def garbage?(value)
# 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)
+ # an XML node(where type['value'] is a Hash)
value['type'] && !value['type'].is_a?(::Hash) && value.size == 1
end
@@ -235,6 +238,6 @@ module ActiveSupport
value.map! { |i| deep_to_h(i) }
value.length > 1 ? value : value.first
end
+
end
end
-
diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
index e07db50b77..9c9faf67ea 100644
--- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -1,27 +1,38 @@
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 = { a: true, b: { c: [1, 2, 3] } }
+ # h2 = { a: false, b: { x: [3, 4, 5] } }
#
- # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"}
- # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
- # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) }
- # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
+ # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
+ #
+ # Like with Hash#merge in the standard library, a block can be provided
+ # to merge values:
+ #
+ # h1 = { a: 100, b: 200, c: { c1: 100 } }
+ # h2 = { b: 250, c: { c1: 200 } }
+ # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
+ # # => { a: 100, b: 450, c: { c1: 300 } }
def deep_merge(other_hash, &block)
dup.deep_merge!(other_hash, &block)
end
# Same as +deep_merge+, but modifies +self+.
def deep_merge!(other_hash, &block)
- other_hash.each_pair do |k,v|
- tv = self[k]
- if tv.is_a?(Hash) && v.is_a?(Hash)
- self[k] = tv.deep_merge(v, &block)
+ other_hash.each_pair do |current_key, other_value|
+ this_value = self[current_key]
+
+ self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
+ this_value.deep_merge(other_value, &block)
else
- self[k] = block && tv ? block.call(k, tv, v) : v
+ if block_given? && key?(current_key)
+ block.call(current_key, this_value, other_value)
+ else
+ other_value
+ end
end
end
+
self
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index d90e996ad4..6e397abf51 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -1,13 +1,19 @@
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:
+ # Returns a hash that includes everything but the given keys.
+ # hash = { a: true, b: false, c: nil}
+ # hash.except(:c) # => { a: true, b: false}
+ # hash # => { a: true, b: false, c: nil}
#
+ # This is useful for limiting a set of parameters to everything but a few known toggles:
# @person.update(params[:person].except(:admin))
def except(*keys)
dup.except!(*keys)
end
# Replaces the hash without the given keys.
+ # hash = { a: true, b: false, c: nil}
+ # hash.except!(:c) # => { a: true, b: false}
+ # hash # => { a: true, b: false }
def except!(*keys)
keys.each { |key| delete(key) }
self
diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
index 981e8436bf..28cb3e2a3b 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -18,5 +18,6 @@ class Hash
#
# b = { b: 1 }
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
+ # # => {"b"=>1}
alias nested_under_indifferent_access with_indifferent_access
end
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index b4c451ace4..9297a59c46 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,12 +1,13 @@
class Hash
- # Return a new hash with all keys converted using the block operation.
+ # Returns a new hash with all keys converted using the block operation.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.transform_keys{ |key| key.to_s.upcase }
- # # => { "NAME" => "Rob", "AGE" => "28" }
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
def transform_keys
- result = {}
+ return enum_for(:transform_keys) unless block_given?
+ result = self.class.new
each_key do |key|
result[yield(key)] = self[key]
end
@@ -16,35 +17,36 @@ class Hash
# Destructively convert all keys using the block operations.
# Same as transform_keys but modifies +self+.
def transform_keys!
+ return enum_for(:transform_keys!) unless block_given?
keys.each do |key|
self[yield(key)] = delete(key)
end
self
end
- # Return a new hash with all keys converted to strings.
+ # Returns a new hash with all keys converted to strings.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.stringify_keys
- # #=> { "name" => "Rob", "age" => "28" }
+ # # => {"name"=>"Rob", "age"=>"28"}
def stringify_keys
- transform_keys{ |key| key.to_s }
+ transform_keys(&:to_s)
end
# Destructively convert all keys to strings. Same as
# +stringify_keys+, but modifies +self+.
def stringify_keys!
- transform_keys!{ |key| key.to_s }
+ transform_keys!(&:to_s)
end
- # Return a new hash with all keys converted to symbols, as long as
+ # Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+.
#
# hash = { 'name' => 'Rob', 'age' => '28' }
#
# hash.symbolize_keys
- # #=> { name: "Rob", age: "28" }
+ # # => {:name=>"Rob", :age=>"28"}
def symbolize_keys
transform_keys{ |key| key.to_sym rescue key }
end
@@ -57,82 +59,108 @@ class Hash
end
alias_method :to_options!, :symbolize_keys!
- # Validate all keys in a hash match <tt>*valid_keys</tt>, 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.
+ # Validate all keys in a hash match <tt>*valid_keys</tt>, raising
+ # ArgumentError on a mismatch.
#
- # { 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"
+ # Note that keys are treated differently than HashWithIndifferentAccess,
+ # meaning that string and symbol keys will not match.
+ #
+ # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
+ # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
# { 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.new("Unknown key: #{k}") unless valid_keys.include?(k)
+ unless valid_keys.include?(k)
+ raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
+ end
end
end
- # Return a new hash with all keys converted by the block operation.
+ # Returns a new hash with all keys converted by the block operation.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_keys{ |key| key.to_s.upcase }
- # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } }
+ # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
def deep_transform_keys(&block)
- result = {}
- each do |key, value|
- result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
- end
- result
+ _deep_transform_keys_in_object(self, &block)
end
# Destructively convert all keys by using the block operation.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_transform_keys!(&block)
- keys.each do |key|
- value = delete(key)
- self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
- end
- self
+ _deep_transform_keys_in_object!(self, &block)
end
- # Return a new hash with all keys converted to strings.
+ # Returns a new hash with all keys converted to strings.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_stringify_keys
- # # => { "person" => { "name" => "Rob", "age" => "28" } }
+ # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
def deep_stringify_keys
- deep_transform_keys{ |key| key.to_s }
+ deep_transform_keys(&:to_s)
end
# Destructively convert all keys to strings.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_stringify_keys!
- deep_transform_keys!{ |key| key.to_s }
+ deep_transform_keys!(&:to_s)
end
- # Return a new hash with all keys converted to symbols, as long as
+ # Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+. This includes the keys from the root hash
- # and from all nested hashes.
+ # and from all nested hashes and arrays.
#
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
#
# hash.deep_symbolize_keys
- # # => { person: { name: "Rob", age: "28" } }
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
def deep_symbolize_keys
deep_transform_keys{ |key| key.to_sym rescue key }
end
# Destructively convert all keys to symbols, as long as they respond
# to +to_sym+. This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_symbolize_keys!
deep_transform_keys!{ |key| key.to_sym rescue key }
end
+
+ private
+ # support methods for deep transforming nested hashes and arrays
+ def _deep_transform_keys_in_object(object, &block)
+ case object
+ when Hash
+ object.each_with_object({}) do |(key, value), result|
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
+ end
+ when Array
+ object.map {|e| _deep_transform_keys_in_object(e, &block) }
+ else
+ object
+ end
+ end
+
+ def _deep_transform_keys_in_object!(object, &block)
+ case object
+ when Hash
+ object.keys.each do |key|
+ value = object.delete(key)
+ object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
+ end
+ object
+ when Array
+ object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
+ else
+ object
+ end
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index 9fa9b3dac4..41b2279013 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -1,6 +1,12 @@
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:
+ # Slice a hash to include only the given keys. Returns a hash containing
+ # the given keys.
+ #
+ # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
+ # # => {:a=>1, :b=>2}
+ #
+ # This is useful for limiting an options hash to valid keys before
+ # passing to a method:
#
# def search(criteria = {})
# criteria.assert_valid_keys(:mass, :velocity, :time)
@@ -26,6 +32,8 @@ class Hash
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
omit = slice(*self.keys - keys)
hash = slice(*keys)
+ hash.default = default
+ hash.default_proc = default_proc if default_proc
replace(hash)
omit
end
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
new file mode 100644
index 0000000000..e9bcce761f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -0,0 +1,23 @@
+class Hash
+ # Returns a new hash with the results of running +block+ once for every value.
+ # The keys are unchanged.
+ #
+ # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
+ # # => { a: 2, b: 4, c: 6 }
+ def transform_values
+ return enum_for(:transform_values) unless block_given?
+ result = self.class.new
+ each do |key, value|
+ result[key] = yield(value)
+ end
+ result
+ end
+
+ # Destructive +transform_values+
+ def transform_values!
+ return enum_for(:transform_values!) unless block_given?
+ each do |key, value|
+ self[key] = yield(value)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb
index 7c6c2f1ca7..c668c7c2eb 100644
--- a/activesupport/lib/active_support/core_ext/integer/multiple.rb
+++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb
@@ -1,9 +1,9 @@
class Integer
# Check whether the integer is evenly divisible by the argument.
#
- # 0.multiple_of?(0) #=> true
- # 6.multiple_of?(5) #=> false
- # 10.multiple_of?(2) #=> true
+ # 0.multiple_of?(0) # => true
+ # 6.multiple_of?(5) # => false
+ # 10.multiple_of?(2) # => true
def multiple_of?(number)
number != 0 ? self % number == 0 : zero?
end
diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 0275f4c037..364ed9d65f 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/agnostics'
-require 'active_support/core_ext/kernel/debugger'
+require 'active_support/core_ext/kernel/concern'
+require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
new file mode 100644
index 0000000000..bf72caa058
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -0,0 +1,10 @@
+require 'active_support/core_ext/module/concerning'
+
+module Kernel
+ # A shortcut to define a toplevel concern, not within a module.
+ #
+ # See Module::Concerning for more.
+ def concern(topic, &module_definition)
+ Object.concern topic, &module_definition
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb
index 2073cac98d..1fde3db070 100644
--- a/activesupport/lib/active_support/core_ext/kernel/debugger.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/debugger.rb
@@ -1,10 +1,3 @@
-module Kernel
- unless respond_to?(:debugger)
- # Starts a debugging session if the +debugger+ gem has been loaded (call rails server --debugger to do load it).
- def debugger
- message = "\n***** Debugger requested, but was not available (ensure the debugger gem is listed in Gemfile/installed as gem): Start server with --debugger to enable *****\n"
- defined?(Rails) ? Rails.logger.info(message) : $stderr.puts(message)
- end
- alias breakpoint debugger unless respond_to?(:breakpoint)
- end
-end
+require 'active_support/deprecation'
+
+ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.")
diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
index 36ab836457..9189e6d977 100644
--- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
@@ -1,4 +1,3 @@
-require 'rbconfig'
require 'tempfile'
module Kernel
@@ -29,27 +28,6 @@ module Kernel
$VERBOSE = old_verbose
end
- # For compatibility
- def silence_stderr #:nodoc:
- silence_stream(STDERR) { yield }
- end
-
- # Silences any stream for the duration of the block.
- #
- # silence_stream(STDOUT) do
- # puts 'This will never be seen'
- # end
- #
- # puts 'But this will'
- def silence_stream(stream)
- old_stream = stream.dup
- stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
- stream.sync = true
- yield
- ensure
- stream.reopen(old_stream)
- end
-
# Blocks and ignores any exception passed as argument if raised within the block.
#
# suppress(ZeroDivisionError) do
@@ -60,51 +38,6 @@ module Kernel
# puts 'This code gets executed and nothing related to ZeroDivisionError was seen'
def suppress(*exception_classes)
yield
- rescue Exception => e
- raise unless exception_classes.any? { |cls| e.kind_of?(cls) }
- end
-
- # Captures the given stream and returns it:
- #
- # stream = capture(:stdout) { puts 'notice' }
- # stream # => "notice\n"
- #
- # stream = capture(:stderr) { warn 'error' }
- # stream # => "error\n"
- #
- # even for subprocesses:
- #
- # stream = capture(:stdout) { system('echo notice') }
- # stream # => "notice\n"
- #
- # stream = capture(:stderr) { system('echo error 1>&2') }
- # stream # => "error\n"
- def capture(stream)
- stream = stream.to_s
- captured_stream = Tempfile.new(stream)
- stream_io = eval("$#{stream}")
- origin_stream = stream_io.dup
- stream_io.reopen(captured_stream)
-
- yield
-
- stream_io.rewind
- return captured_stream.read
- ensure
- captured_stream.close
- captured_stream.unlink
- stream_io.reopen(origin_stream)
- end
- alias :silence :capture
-
- # Silences both STDOUT and STDERR, even for subprocesses.
- #
- # quietly { system 'bundle install' }
- def quietly
- silence_stream(STDOUT) do
- silence_stream(STDERR) do
- yield
- end
- end
+ rescue *exception_classes
end
end
diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb
index fe24f3716d..d9fb392752 100644
--- a/activesupport/lib/active_support/core_ext/load_error.rb
+++ b/activesupport/lib/active_support/core_ext/load_error.rb
@@ -1,3 +1,5 @@
+require 'active_support/deprecation/proxy_wrappers'
+
class LoadError
REGEXPS = [
/^no such file to load -- (.+)$/i,
@@ -7,6 +9,7 @@ class LoadError
]
unless method_defined?(:path)
+ # Returns the path which was unable to be loaded.
def path
@path ||= begin
REGEXPS.find do |regex|
@@ -17,9 +20,11 @@ class LoadError
end
end
+ # Returns true if the given path name (except perhaps for the ".rb"
+ # extension) is the missing file which caused the exception to be raised.
def is_missing?(location)
location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '')
end
end
-MissingSourceFile = LoadError \ No newline at end of file
+MissingSourceFile = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('MissingSourceFile', 'LoadError')
diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb
index f2d4887df6..b4efff8b24 100644
--- a/activesupport/lib/active_support/core_ext/module.rb
+++ b/activesupport/lib/active_support/core_ext/module.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/reachable'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/module/attr_internal'
+require 'active_support/core_ext/module/concerning'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/module/remove_method'
diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb
index 580cb80413..0a6fadf928 100644
--- a/activesupport/lib/active_support/core_ext/module/aliasing.rb
+++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb
@@ -19,9 +19,9 @@ class Module
# alias_method :foo_without_feature?, :foo?
# alias_method :foo?, :foo_with_feature?
#
- # so you can safely chain foo, foo?, and foo! with the same feature.
+ # so you can safely chain foo, foo?, foo! and/or foo= with the same feature.
def alias_method_chain(target, feature)
- # Strip out punctuation on predicates or bang methods since
+ # Strip out punctuation on predicates, bang or writer methods since
# e.g. target?_without_feature is not a valid method name.
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
yield(aliased_target, punctuation) if block_given?
diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
index db07d549b0..93fb598650 100644
--- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb
+++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
@@ -27,10 +27,8 @@ class Module
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '')
- class_eval do # class_eval is necessary on 1.9 or else the methods a made private
- # use native attr_* methods as they are faster on some Ruby implementations
- send("attr_#{type}", internal_name)
- end
+ # use native attr_* methods as they are faster on some Ruby implementations
+ send("attr_#{type}", internal_name)
attr_name, internal_name = "#{attr_name}=", "#{internal_name}=" if type == :writer
alias_method attr_name, internal_name
remove_method internal_name
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
index 672cc0256f..d4e6b5a1ac 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -1,10 +1,59 @@
require 'active_support/core_ext/array/extract_options'
+# Extends the module object with class/module and instance accessors for
+# class/module attributes, just like the native attr* accessors for instance
+# attributes.
class Module
+ # Defines a class attribute and creates a class and instance reader methods.
+ # The underlying the class variable is set to +nil+, if it is not previously
+ # defined.
+ #
+ # module HairColors
+ # mattr_reader :hair_colors
+ # end
+ #
+ # HairColors.hair_colors # => nil
+ # HairColors.class_variable_set("@@hair_colors", [:brown, :black])
+ # HairColors.hair_colors # => [:brown, :black]
+ #
+ # The attribute name must be a valid method name in Ruby.
+ #
+ # module Foo
+ # mattr_reader :"1_Badname "
+ # end
+ # # => NameError: invalid attribute name
+ #
+ # If you want to opt out the creation on the instance reader method, pass
+ # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors, instance_reader: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors # => NoMethodError
+ #
+ #
+ # Also, you can pass a block to set up the attribute with a default value.
+ #
+ # module HairColors
+ # cattr_reader :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.hair_colors # => [:brown, :black, :blonde, :red]
def mattr_reader(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
@@ -20,14 +69,60 @@ class Module
end
EOS
end
+ class_variable_set("@@#{sym}", yield) if block_given?
end
end
+ alias :cattr_reader :mattr_reader
+ # Defines a class attribute and creates a class and instance writer methods to
+ # allow assignment to the attribute.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # HairColors.hair_colors = [:brown, :black]
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black]
+ # Person.new.hair_colors = [:blonde, :red]
+ # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
+ #
+ # If you want to opt out the instance writer method, pass
+ # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors, instance_writer: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:blonde, :red] # => NoMethodError
+ #
+ # Also, you can pass a block to set up the attribute with a default value.
+ #
+ # class HairColors
+ # mattr_writer :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_writer(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ @@#{sym} = nil unless defined? @@#{sym}
+
def self.#{sym}=(obj)
@@#{sym} = obj
end
@@ -40,27 +135,78 @@ class Module
end
EOS
end
+ send("#{sym}=", yield) if block_given?
end
end
+ alias :cattr_writer :mattr_writer
- # Extends the module object with module and instance accessors for class attributes,
- # just like the native attr* accessors for instance attributes.
+ # Defines both class and instance accessors for class attributes.
#
- # module AppConfiguration
- # mattr_accessor :google_api_key
+ # module HairColors
+ # mattr_accessor :hair_colors
+ # end
#
- # self.google_api_key = "123456789"
+ # class Person
+ # include HairColors
# end
#
- # AppConfiguration.google_api_key # => "123456789"
- # AppConfiguration.google_api_key = "overriding the api key!"
- # AppConfiguration.google_api_key # => "overriding the api key!"
+ # 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>.
- # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
- def mattr_accessor(*syms)
- mattr_reader(*syms)
- mattr_writer(*syms)
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # 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.
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors, instance_accessor: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # 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.
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
+ def mattr_accessor(*syms, &blk)
+ mattr_reader(*syms, &blk)
+ mattr_writer(*syms, &blk)
end
+ alias :cattr_accessor :mattr_accessor
end
diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb
new file mode 100644
index 0000000000..07a392404e
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/concerning.rb
@@ -0,0 +1,135 @@
+require 'active_support/concern'
+
+class Module
+ # = Bite-sized separation of concerns
+ #
+ # We often find ourselves with a medium-sized chunk of behavior that we'd
+ # like to extract, but only mix in to a single class.
+ #
+ # Extracting a plain old Ruby object to encapsulate it and collaborate or
+ # delegate to the original object is often a good choice, but when there's
+ # no additional state to encapsulate or we're making DSL-style declarations
+ # about the parent class, introducing new collaborators can obfuscate rather
+ # than simplify.
+ #
+ # The typical route is to just dump everything in a monolithic class, perhaps
+ # with a comment, as a least-bad alternative. Using modules in separate files
+ # means tedious sifting to get a big-picture view.
+ #
+ # = Dissatisfying ways to separate small concerns
+ #
+ # == Using comments:
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # ## Event tracking
+ # has_many :events
+ #
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ #
+ # == With an inline module:
+ #
+ # Noisy syntax.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # module EventTracking
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # has_many :events
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ # end
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ # include EventTracking
+ # end
+ #
+ # == Mix-in noise exiled to its own file:
+ #
+ # Once our chunk of behavior starts pushing the scroll-to-understand it's
+ # boundary, we give in and move it to a separate file. At this size, the
+ # overhead feels in good proportion to the size of our extraction, despite
+ # diluting our at-a-glance sense of how things really work.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # include TodoEventTracking
+ # end
+ #
+ # = Introducing Module#concerning
+ #
+ # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
+ # separate bite-sized concerns.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # concerning :EventTracking do
+ # included do
+ # has_many :events
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ # end
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ # end
+ #
+ # Todo.ancestors
+ # # => Todo, Todo::EventTracking, Object
+ #
+ # This small step has some wonderful ripple effects. We can
+ # * grok the behavior of our class in one glance,
+ # * clean up monolithic junk-drawer classes by separating their concerns, and
+ # * stop leaning on protected/private for crude "this is internal stuff" modularity.
+ module Concerning
+ # Define a new concern and mix it in.
+ def concerning(topic, &block)
+ include concern(topic, &block)
+ end
+
+ # A low-cruft shortcut to define a concern.
+ #
+ # concern :EventTracking do
+ # ...
+ # end
+ #
+ # is equivalent to
+ #
+ # module EventTracking
+ # extend ActiveSupport::Concern
+ #
+ # ...
+ # end
+ def concern(topic, &module_definition)
+ const_set topic, Module.new {
+ extend ::ActiveSupport::Concern
+ module_eval(&module_definition)
+ }
+ end
+ end
+ include Concerning
+end
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index bca3800344..24df83800b 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,15 +1,23 @@
+require 'set'
+
class Module
# Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
# option is not used.
class DelegationError < NoMethodError; end
+ RUBY_RESERVED_WORDS = Set.new(
+ %w(alias and BEGIN begin break case class def defined? do else elsif END
+ end ensure false for if in module next nil not or redo rescue retry
+ return self super then true undef unless until when while yield)
+ ).freeze
+
# Provides a +delegate+ class method to easily expose contained objects'
# public methods as your own.
#
# ==== Options
# * <tt>:to</tt> - Specifies the target object
# * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
- # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ to be raised
+ # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised
#
# The macro receives one or more method names (specified as symbols or
# strings) and the name of the target object via the <tt>:to</tt> option
@@ -138,6 +146,8 @@ class Module
#
# Foo.new("Bar").name # raises NoMethodError: undefined method `name'
#
+ # The target method must be public, otherwise it will raise +NoMethodError+.
+ #
def delegate(*methods)
options = methods.pop
unless options.is_a?(Hash) && to = options[:to]
@@ -161,46 +171,35 @@ class Module
line = line.to_i
to = to.to_s
- to = 'self.class' if to == 'class'
+ to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to)
methods.each do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
- # The following generated methods call the target exactly once, storing
+ # The following generated method calls the target exactly once, storing
# the returned value in a dummy variable.
#
# Reason is twofold: On one hand doing less calls is in general better.
# On the other hand it could be that the target has side-effects,
# whereas conceptually, from the user point of view, the delegator should
# be doing one call.
- if allow_nil
- module_eval(<<-EOS, file, line - 3)
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
- _ = #{to} # _ = client
- if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name)
- _.#{method}(#{definition}) # _.name(*args, &block)
- end # end
- end # end
- EOS
- else
- exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
- module_eval(<<-EOS, file, line - 2)
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
- _ = #{to} # _ = client
- _.#{method}(#{definition}) # _.name(*args, &block)
- rescue NoMethodError => e # rescue NoMethodError => e
- location = "%s:%d:in `%s'" % [__FILE__, __LINE__ - 2, '#{method_prefix}#{method}'] # location = "%s:%d:in `%s'" % [__FILE__, __LINE__ - 2, 'customer_name']
- if _.nil? && e.backtrace.first == location # if _.nil? && e.backtrace.first == location
- #{exception} # # add helpful message to the exception
- else # else
- raise # raise
- end # end
- end # end
- EOS
- end
+ exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
+
+ method_def = [
+ "def #{method_prefix}#{method}(#{definition})",
+ " _ = #{to}",
+ " if !_.nil? || nil.respond_to?(:#{method})",
+ " _.#{method}(#{definition})",
+ " else",
+ " #{exception unless allow_nil}",
+ " end",
+ "end"
+ ].join ';'
+
+ module_eval(method_def, file, line)
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb
index d873de197f..56d670fbe8 100644
--- a/activesupport/lib/active_support/core_ext/module/deprecation.rb
+++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation/method_wrappers'
-
class Module
# deprecate :foo
# deprecate bar: 'message'
diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
index b1097cc83b..1fde3db070 100644
--- a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
+++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
@@ -1,11 +1,3 @@
-class Module
- ###
- # TODO: remove this after 1.9 support is dropped
- def methods_transplantable? # :nodoc:
- x = Module.new { def foo; end }
- Module.new { define_method :bar, x.instance_method(:foo) }
- true
- rescue TypeError
- false
- end
-end
+require 'active_support/deprecation'
+
+ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.")
diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb
index 719071d1c2..8a2569a7d0 100644
--- a/activesupport/lib/active_support/core_ext/module/remove_method.rb
+++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb
@@ -1,10 +1,13 @@
class Module
+ # Remove the named method, if it exists.
def remove_possible_method(method)
if method_defined?(method) || private_method_defined?(method)
undef_method(method)
end
end
+ # Replace the existing method definition, if there is one, with the contents
+ # of the block.
def redefine_method(method, &block)
remove_possible_method(method)
define_method(method, &block)
diff --git a/activesupport/lib/active_support/core_ext/name_error.rb b/activesupport/lib/active_support/core_ext/name_error.rb
index e1ebd4f91c..b82148e4e5 100644
--- a/activesupport/lib/active_support/core_ext/name_error.rb
+++ b/activesupport/lib/active_support/core_ext/name_error.rb
@@ -1,5 +1,12 @@
class NameError
# Extract the name of the missing constant from the exception message.
+ #
+ # begin
+ # HelloWorld
+ # rescue NameError => e
+ # e.missing_name
+ # end
+ # # => "HelloWorld"
def missing_name
if /undefined local variable or method/ !~ message
$1 if /((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/ =~ message
@@ -7,6 +14,13 @@ class NameError
end
# Was this exception raised because the given name was missing?
+ #
+ # begin
+ # HelloWorld
+ # rescue NameError => e
+ # e.missing_name?("HelloWorld")
+ # end
+ # # => true
def missing_name?(name)
if name.is_a? Symbol
last_name = (missing_name || '').split('::').last
diff --git a/activesupport/lib/active_support/core_ext/numeric/bytes.rb b/activesupport/lib/active_support/core_ext/numeric/bytes.rb
index deea8e9358..dfbca32474 100644
--- a/activesupport/lib/active_support/core_ext/numeric/bytes.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/bytes.rb
@@ -7,36 +7,56 @@ class Numeric
EXABYTE = PETABYTE * 1024
# Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes
+ #
+ # 2.bytes # => 2
def bytes
self
end
alias :byte :bytes
+ # Returns the number of bytes equivalent to the kilobytes provided.
+ #
+ # 2.kilobytes # => 2048
def kilobytes
self * KILOBYTE
end
alias :kilobyte :kilobytes
+ # Returns the number of bytes equivalent to the megabytes provided.
+ #
+ # 2.megabytes # => 2_097_152
def megabytes
self * MEGABYTE
end
alias :megabyte :megabytes
+ # Returns the number of bytes equivalent to the gigabytes provided.
+ #
+ # 2.gigabytes # => 2_147_483_648
def gigabytes
self * GIGABYTE
end
alias :gigabyte :gigabytes
+ # Returns the number of bytes equivalent to the terabytes provided.
+ #
+ # 2.terabytes # => 2_199_023_255_552
def terabytes
self * TERABYTE
end
alias :terabyte :terabytes
+ # Returns the number of bytes equivalent to the petabytes provided.
+ #
+ # 2.petabytes # => 2_251_799_813_685_248
def petabytes
self * PETABYTE
end
alias :petabyte :petabytes
+ # Returns the number of bytes equivalent to the exabytes provided.
+ #
+ # 2.exabytes # => 2_305_843_009_213_693_952
def exabytes
self * EXABYTE
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 6d3635c69a..0c8ff79237 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -118,18 +118,28 @@ class Numeric
end
end
- [Float, Fixnum, Bignum, BigDecimal].each do |klass|
- klass.send(:alias_method, :to_default_s, :to_s)
-
- klass.send(:define_method, :to_s) do |*args|
- if args[0].is_a?(Symbol)
- format = args[0]
- options = args[1] || {}
+ [Fixnum, Bignum].each do |klass|
+ klass.class_eval do
+ alias_method :to_default_s, :to_s
+ def to_s(base_or_format = 10, options = nil)
+ if base_or_format.is_a?(Symbol)
+ to_formatted_s(base_or_format, options || {})
+ else
+ to_default_s(base_or_format)
+ end
+ end
+ end
+ end
- self.to_formatted_s(format, options)
+ Float.class_eval do
+ alias_method :to_default_s, :to_s
+ def to_s(*args)
+ if args.empty?
+ to_default_s
else
- to_default_s(*args)
+ to_formatted_s(*args)
end
end
end
+
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 87b9a23aef..98716383f4 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -1,6 +1,8 @@
require 'active_support/duration'
require 'active_support/core_ext/time/calculations'
require 'active_support/core_ext/time/acts_like'
+require 'active_support/core_ext/date/calculations'
+require 'active_support/core_ext/date/acts_like'
class Numeric
# Enables the use of time calculations and declarations, like 45.minutes + 2.hours + 4.years.
@@ -36,44 +38,52 @@ class Numeric
end
alias :second :seconds
+ # Returns a Duration instance matching the number of minutes provided.
+ #
+ # 2.minutes # => 120 seconds
def minutes
ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]])
end
alias :minute :minutes
+ # Returns a Duration instance matching the number of hours provided.
+ #
+ # 2.hours # => 7_200 seconds
def hours
ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]])
end
alias :hour :hours
+ # Returns a Duration instance matching the number of days provided.
+ #
+ # 2.days # => 2 days
def days
ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
end
alias :day :days
+ # Returns a Duration instance matching the number of weeks provided.
+ #
+ # 2.weeks # => 14 days
def weeks
ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
end
alias :week :weeks
+ # Returns a Duration instance matching the number of fortnights provided.
+ #
+ # 2.fortnights # => 28 days
def fortnights
ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]])
end
alias :fortnight :fortnights
- # Reads best without arguments: 10.minutes.ago
- def ago(time = ::Time.current)
- time - self
- end
-
- # Reads best with argument: 10.minutes.until(time)
- alias :until :ago
-
- # Reads best with argument: 10.minutes.since(time)
- def since(time = ::Time.current)
- time + self
+ # Returns the number of milliseconds equivalent to the seconds provided.
+ # Used with the standard time durations, like 1.hour.in_milliseconds --
+ # so we can feed them to JavaScript functions like getTime().
+ #
+ # 2.in_milliseconds # => 2_000
+ def in_milliseconds
+ self * 1000
end
-
- # Reads best without arguments: 10.minutes.from_now
- alias :from_now :since
end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index ec2157221f..f4f9152d6a 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -8,7 +8,7 @@ require 'active_support/core_ext/object/inclusion'
require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/instance_variables'
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/object/with_options'
diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index 8a5eb4bc93..38e43478df 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -4,36 +4,42 @@ class Object
# An object is blank if it's false, empty, or a whitespace string.
# For example, '', ' ', +nil+, [], and {} are all blank.
#
- # This simplifies:
+ # This simplifies
#
- # if address.nil? || address.empty?
+ # address.nil? || address.empty?
#
- # ...to:
+ # to
#
- # if address.blank?
+ # address.blank?
+ #
+ # @return [true, false]
def blank?
- respond_to?(:empty?) ? empty? : !self
+ respond_to?(:empty?) ? !!empty? : !self
end
- # An object is present if it's not <tt>blank?</tt>.
+ # An object is present if it's not blank.
+ #
+ # @return [true, false]
def present?
!blank?
end
- # Returns object if it's <tt>present?</tt> otherwise returns +nil+.
- # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>.
+ # Returns the receiver if it's present otherwise returns +nil+.
+ # <tt>object.presence</tt> is equivalent to
#
- # This is handy for any representation of objects where blank is the same
- # as not present at all. For example, this simplifies a common check for
- # HTTP POST/query parameters:
+ # object.present? ? object : nil
+ #
+ # For example, something like
#
# state = params[:state] if params[:state].present?
# country = params[:country] if params[:country].present?
# region = state || country || 'US'
#
- # ...becomes:
+ # becomes
#
# region = params[:state].presence || params[:country].presence || 'US'
+ #
+ # @return [Object]
def presence
self if present?
end
@@ -43,6 +49,8 @@ class NilClass
# +nil+ is blank:
#
# nil.blank? # => true
+ #
+ # @return [true]
def blank?
true
end
@@ -52,6 +60,8 @@ class FalseClass
# +false+ is blank:
#
# false.blank? # => true
+ #
+ # @return [true]
def blank?
true
end
@@ -61,6 +71,8 @@ class TrueClass
# +true+ is not blank:
#
# true.blank? # => false
+ #
+ # @return [false]
def blank?
false
end
@@ -71,6 +83,8 @@ class Array
#
# [].blank? # => true
# [1,2,3].blank? # => false
+ #
+ # @return [true, false]
alias_method :blank?, :empty?
end
@@ -79,18 +93,28 @@ class Hash
#
# {}.blank? # => true
# { key: 'value' }.blank? # => false
+ #
+ # @return [true, false]
alias_method :blank?, :empty?
end
class String
+ BLANK_RE = /\A[[:space:]]*\z/
+
# A string is blank if it's empty or contains whitespaces only:
#
- # ''.blank? # => true
- # ' '.blank? # => true
- # ' '.blank? # => true
- # ' something here '.blank? # => false
+ # ''.blank? # => true
+ # ' '.blank? # => true
+ # "\t\n\r".blank? # => true
+ # ' blah '.blank? # => false
+ #
+ # Unicode whitespace is supported:
+ #
+ # "\u00a0".blank? # => true
+ #
+ # @return [true, false]
def blank?
- self !~ /[^[:space:]]/
+ BLANK_RE === self
end
end
@@ -99,6 +123,8 @@ class Numeric #:nodoc:
#
# 1.blank? # => false
# 0.blank? # => false
+ #
+ # @return [false]
def blank?
false
end
diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
index 1d639f3af6..0191d2e973 100644
--- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb
+++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
@@ -8,8 +8,8 @@ class Object
# dup = object.deep_dup
# dup.instance_variable_set(:@a, 1)
#
- # object.instance_variable_defined?(:@a) #=> false
- # dup.instance_variable_defined?(:@a) #=> true
+ # object.instance_variable_defined?(:@a) # => false
+ # dup.instance_variable_defined?(:@a) # => true
def deep_dup
duplicable? ? dup : self
end
@@ -22,10 +22,10 @@ class Array
# dup = array.deep_dup
# dup[1][2] = 4
#
- # array[1][2] #=> nil
- # dup[1][2] #=> 4
+ # array[1][2] # => nil
+ # dup[1][2] # => 4
def deep_dup
- map { |it| it.deep_dup }
+ map(&:deep_dup)
end
end
@@ -36,8 +36,8 @@ class Hash
# dup = hash.deep_dup
# dup[:a][:c] = 'c'
#
- # hash[:a][:c] #=> nil
- # dup[:a][:c] #=> "c"
+ # hash[:a][:c] # => nil
+ # dup[:a][:c] # => "c"
def deep_dup
each_with_object(dup) do |(key, value), hash|
hash[key.deep_dup] = value.deep_dup
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 9cd7485e2e..620f7b6561 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -19,7 +19,7 @@
class Object
# Can you safely dup this object?
#
- # False for +nil+, +false+, +true+, symbol, and number objects;
+ # False for +nil+, +false+, +true+, symbol, number objects;
# true otherwise.
def duplicable?
true
@@ -78,13 +78,17 @@ end
require 'bigdecimal'
class BigDecimal
- begin
- BigDecimal.new('4.56').dup
+ def duplicable?
+ true
+ end
+end
- def duplicable?
- true
- end
- rescue TypeError
- # can't dup, so use superclass implementation
+class Method
+ # Methods are not duplicable:
+ #
+ # method(:puts).duplicable? # => false
+ # method(:puts).dup # => TypeError: allocator undefined for Method
+ def duplicable?
+ false
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb
index b5671f66d0..55f281b213 100644
--- a/activesupport/lib/active_support/core_ext/object/inclusion.rb
+++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb
@@ -12,4 +12,16 @@ class Object
rescue NoMethodError
raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
end
+
+ # Returns the receiver if it's included in the argument otherwise returns +nil+.
+ # Argument must be any object which responds to +#include?+. Usage:
+ #
+ # params[:bucket_type].presence_in %w( project calendar )
+ #
+ # This will throw an ArgumentError if the argument doesn't respond to +#include?+.
+ #
+ # @return [Object]
+ def presence_in(another_object)
+ self.in?(another_object) ? self : nil
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
index 755e1c6b16..593a7a4940 100644
--- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb
+++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
@@ -23,6 +23,6 @@ class Object
#
# C.new(0, 1).instance_variable_names # => ["@y", "@x"]
def instance_variable_names
- instance_variables.map { |var| var.to_s }
+ instance_variables.map(&:to_s)
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
new file mode 100644
index 0000000000..698b2d1920
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -0,0 +1,197 @@
+# Hack to load json gem first so we can overwrite its to_json.
+require 'json'
+require 'bigdecimal'
+require 'active_support/core_ext/big_decimal/conversions' # for #to_s
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/object/instance_variables'
+require 'time'
+require 'active_support/core_ext/time/conversions'
+require 'active_support/core_ext/date_time/conversions'
+require 'active_support/core_ext/date/conversions'
+require 'active_support/core_ext/module/aliasing'
+
+# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
+# their default behavior. That said, we need to define the basic to_json method in all of them,
+# otherwise they will always use to_json gem implementation, which is backwards incompatible in
+# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
+# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
+#
+# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
+# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
+# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
+# calls to the original to_json method.
+#
+# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is
+# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
+# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
+# should give exactly the same results with or without active support.
+[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass|
+ klass.class_eval do
+ def to_json_with_active_support_encoder(options = nil)
+ if options.is_a?(::JSON::State)
+ # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
+ self.to_json_without_active_support_encoder(options)
+ else
+ # to_json is being invoked directly, use ActiveSupport's encoder
+ ActiveSupport::JSON.encode(self, options)
+ end
+ end
+
+ alias_method_chain :to_json, :active_support_encoder
+ end
+end
+
+class Object
+ def as_json(options = nil) #:nodoc:
+ if respond_to?(:to_hash)
+ to_hash.as_json(options)
+ else
+ instance_values.as_json(options)
+ end
+ end
+end
+
+class Struct #:nodoc:
+ def as_json(options = nil)
+ Hash[members.zip(values)].as_json(options)
+ end
+end
+
+class TrueClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class FalseClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class NilClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class String
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class Symbol
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Numeric
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class Float
+ # Encoding Infinity or NaN to JSON should return "null". The default returns
+ # "Infinity" or "NaN" which are not valid JSON.
+ def as_json(options = nil) #:nodoc:
+ finite? ? self : nil
+ end
+end
+
+class BigDecimal
+ # A BigDecimal would be naturally represented as a JSON number. Most libraries,
+ # however, parse non-integer JSON numbers directly as floats. Clients using
+ # those libraries would get in general a wrong number and no way to recover
+ # other than manually inspecting the string with the JSON code itself.
+ #
+ # That's why a JSON string is returned. The JSON literal is not numeric, but
+ # if the other end knows by contract that the data is supposed to be a
+ # BigDecimal, it still has the chance to post-process the string and get the
+ # real value.
+ def as_json(options = nil) #:nodoc:
+ finite? ? to_s : nil
+ end
+end
+
+class Regexp
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+module Enumerable
+ def as_json(options = nil) #:nodoc:
+ to_a.as_json(options)
+ end
+end
+
+class Range
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Array
+ def as_json(options = nil) #:nodoc:
+ map { |v| options ? v.as_json(options.dup) : v.as_json }
+ end
+end
+
+class Hash
+ def as_json(options = nil) #:nodoc:
+ # create a subset of the hash by applying :only or :except
+ subset = if options
+ if attrs = options[:only]
+ slice(*Array(attrs))
+ elsif attrs = options[:except]
+ except(*Array(attrs))
+ else
+ self
+ end
+ else
+ self
+ end
+
+ Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
+ end
+end
+
+class Time
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
+ else
+ %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
+ end
+ end
+end
+
+class Date
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
+ strftime("%Y-%m-%d")
+ else
+ strftime("%Y/%m/%d")
+ end
+ end
+end
+
+class DateTime
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
+ else
+ strftime('%Y/%m/%d %H:%M:%S %z')
+ end
+ end
+end
+
+class Process::Status #:nodoc:
+ def as_json(options = nil)
+ { :exitstatus => exitstatus, :pid => pid }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb
deleted file mode 100644
index 83cc8066e7..0000000000
--- a/activesupport/lib/active_support/core_ext/object/to_json.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# Hack to load json gem first so we can overwrite its to_json.
-begin
- require 'json'
-rescue LoadError
-end
-
-# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
-# their default behavior. That said, we need to define the basic to_json method in all of them,
-# otherwise they will always use to_json gem implementation, which is backwards incompatible in
-# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
-# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
-[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
- klass.class_eval do
- # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
- def to_json(options = nil)
- ActiveSupport::JSON.encode(self, options)
- end
- end
-end
-
-module Process
- class Status
- def as_json(options = nil)
- { :exitstatus => exitstatus, :pid => pid }
- end
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb
index 3b137ce6ae..684d4ef57e 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -1,58 +1 @@
-class Object
- # Alias of <tt>to_s</tt>.
- def to_param
- to_s
- end
-end
-
-class NilClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class TrueClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class FalseClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class Array
- # Calls <tt>to_param</tt> on all its elements and joins the result with
- # slashes. This is used by <tt>url_for</tt> in Action Pack.
- def to_param
- collect { |e| e.to_param }.join '/'
- end
-end
-
-class Hash
- # Returns a string representation of the receiver suitable for use as a URL
- # query string:
- #
- # {name: 'David', nationality: 'Danish'}.to_param
- # # => "name=David&nationality=Danish"
- #
- # An optional namespace can be passed to enclose the param names:
- #
- # {name: 'David', nationality: 'Danish'}.to_param('user')
- # # => "user[name]=David&user[nationality]=Danish"
- #
- # The string pairs "key=value" that conform the query string
- # are sorted lexicographically in ascending order.
- #
- # This method is also aliased as +to_query+.
- def to_param(namespace = nil)
- collect do |key, value|
- value.to_query(namespace ? "#{namespace}[#{key}]" : key)
- end.sort! * '&'
- end
-end
+require 'active_support/core_ext/object/to_query'
diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb
index 5d5fcf00e0..ec5ace4e16 100644
--- a/activesupport/lib/active_support/core_ext/object/to_query.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_query.rb
@@ -1,27 +1,84 @@
-require 'active_support/core_ext/object/to_param'
+require 'cgi'
class Object
- # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the
- # param name.
- #
- # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work.
+ # Alias of <tt>to_s</tt>.
+ def to_param
+ to_s
+ end
+
+ # Converts an object into a string suitable for use as a URL query string,
+ # using the given <tt>key</tt> as the param name.
def to_query(key)
- require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
"#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
end
end
+class NilClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
+class TrueClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
+class FalseClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
class Array
+ # Calls <tt>to_param</tt> on all its elements and joins the result with
+ # slashes. This is used by <tt>url_for</tt> in Action Pack.
+ def to_param
+ collect(&: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 '&'
+
+ if empty?
+ nil.to_query(prefix)
+ else
+ collect { |value| value.to_query(prefix) }.join '&'
+ end
end
end
class Hash
- alias_method :to_query, :to_param
+ # Returns a string representation of the receiver suitable for use as a URL
+ # query string:
+ #
+ # {name: 'David', nationality: 'Danish'}.to_query
+ # # => "name=David&nationality=Danish"
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # {name: 'David', nationality: 'Danish'}.to_query('user')
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
+ #
+ # The string pairs "key=value" that conform the query string
+ # are sorted lexicographically in ascending order.
+ #
+ # This method is also aliased as +to_param+.
+ def to_query(namespace = nil)
+ collect do |key, value|
+ unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
+ end
+ end.compact.sort! * '&'
+ end
+
+ alias_method :to_param, :to_query
end
diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb
index 48190e1e66..e0f70b9caa 100644
--- a/activesupport/lib/active_support/core_ext/object/try.rb
+++ b/activesupport/lib/active_support/core_ext/object/try.rb
@@ -9,7 +9,23 @@ class Object
#
# instead of
#
- # @person ? @person.name : nil
+ # @person.name if @person
+ #
+ # +try+ calls can be chained:
+ #
+ # @person.try(:spouse).try(:name)
+ #
+ # instead of
+ #
+ # @person.spouse.name if @person && @person.spouse
+ #
+ # +try+ will also return +nil+ if the receiver does not respond to the method:
+ #
+ # @person.try(:non_existing_method) # => nil
+ #
+ # instead of
+ #
+ # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
#
# +try+ returns +nil+ when called on +nil+ regardless of whether it responds
# to the method:
@@ -24,7 +40,7 @@ class Object
#
# The number of arguments in the signature must match. If the object responds
# to the method the call is attempted and +ArgumentError+ is still raised
- # otherwise.
+ # in case of argument mismatch.
#
# If +try+ is called without arguments it yields the receiver to a given
# block unless it is +nil+:
@@ -33,24 +49,33 @@ class Object
# ...
# end
#
- # Please also note that +try+ is defined on +Object+, therefore it won't work
+ # You can also call try with a block without accepting an argument, and the block
+ # will be instance_eval'ed instead:
+ #
+ # @person.try { upcase.truncate(50) }
+ #
+ # Please also note that +try+ is defined on +Object+. Therefore, it won't work
# with instances of classes that do not have +Object+ among their ancestors,
# like direct subclasses of +BasicObject+. For example, using +try+ with
# +SimpleDelegator+ will delegate +try+ to the target instead of calling it on
- # delegator itself.
+ # the delegator itself.
def try(*a, &b)
- if a.empty? && block_given?
- yield self
- else
- public_send(*a, &b) if respond_to?(a.first)
- end
+ try!(*a, &b) if a.empty? || respond_to?(a.first)
end
- # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and
- # does not implement the tried method.
+ # Same as #try, but raises a NoMethodError exception if the receiver is
+ # not +nil+ and does not implement the tried method.
+ #
+ # "a".try!(:upcase) # => "A"
+ # nil.try!(:upcase) # => nil
+ # 123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum
def try!(*a, &b)
if a.empty? && block_given?
- yield self
+ if b.arity.zero?
+ instance_eval(&b)
+ else
+ yield self
+ end
else
public_send(*a, &b)
end
@@ -59,12 +84,12 @@ end
class NilClass
# Calling +try+ on +nil+ always returns +nil+.
- # It becomes specially helpful when navigating through associations that may return +nil+.
+ # It becomes especially helpful when navigating through associations that may return +nil+.
#
# nil.try(:name) # => nil
#
# Without +try+
- # @person && !@person.children.blank? && @person.children.first.name
+ # @person && @person.children.any? && @person.children.first.name
#
# With +try+
# @person.try(:children).try(:first).try(:name)
@@ -72,6 +97,9 @@ class NilClass
nil
end
+ # Calling +try!+ on +nil+ always returns +nil+.
+ #
+ # nil.try!(:name) # => nil
def try!(*args)
nil
end
diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb
index 42e388b065..7d38e1d134 100644
--- a/activesupport/lib/active_support/core_ext/object/with_options.rb
+++ b/activesupport/lib/active_support/core_ext/object/with_options.rb
@@ -34,9 +34,36 @@ class Object
# body i18n.t :body, user_name: user.name
# end
#
+ # When you don't pass an explicit receiver, it executes the whole block
+ # in merging options context:
+ #
+ # class Account < ActiveRecord::Base
+ # with_options dependent: :destroy do
+ # has_many :customers
+ # has_many :products
+ # has_many :invoices
+ # has_many :expenses
+ # end
+ # end
+ #
# <tt>with_options</tt> can also be nested since the call is forwarded to its receiver.
- # Each nesting level will merge inherited defaults in addition to their own.
- def with_options(options)
- yield ActiveSupport::OptionMerger.new(self, options)
+ #
+ # NOTE: Each nesting level will merge inherited defaults in addition to their own.
+ #
+ # class Post < ActiveRecord::Base
+ # with_options if: :persisted?, length: { minimum: 50 } do
+ # validates :content, if: -> { content.present? }
+ # end
+ # end
+ #
+ # The code is equivalent to:
+ #
+ # validates :content, length: { minimum: 50 }, if: -> { content.present? }
+ #
+ # Hence the inherited default for `if` key is ignored.
+ #
+ def with_options(options, &block)
+ option_merger = ActiveSupport::OptionMerger.new(self, options)
+ block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
end
end
diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb
index 1d8b1ede5a..9368e81235 100644
--- a/activesupport/lib/active_support/core_ext/range.rb
+++ b/activesupport/lib/active_support/core_ext/range.rb
@@ -1,3 +1,4 @@
require 'active_support/core_ext/range/conversions'
require 'active_support/core_ext/range/include_range'
require 'active_support/core_ext/range/overlaps'
+require 'active_support/core_ext/range/each'
diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb
new file mode 100644
index 0000000000..ecef78f55f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/each.rb
@@ -0,0 +1,23 @@
+require 'active_support/core_ext/module/aliasing'
+
+class Range #:nodoc:
+
+ def each_with_time_with_zone(&block)
+ ensure_iteration_allowed
+ each_without_time_with_zone(&block)
+ end
+ alias_method_chain :each, :time_with_zone
+
+ def step_with_time_with_zone(n = 1, &block)
+ ensure_iteration_allowed
+ step_without_time_with_zone(n, &block)
+ end
+ alias_method_chain :step, :time_with_zone
+
+ private
+ def ensure_iteration_allowed
+ if first.is_a?(Time)
+ raise TypeError, "can't iterate from #{first.class}"
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb
new file mode 100644
index 0000000000..6cdbea1f37
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/securerandom.rb
@@ -0,0 +1,23 @@
+require 'securerandom'
+
+module SecureRandom
+ BASE58_ALPHABET = ('0'..'9').to_a + ('A'..'Z').to_a + ('a'..'z').to_a - ['0', 'O', 'I', 'l']
+ # SecureRandom.base58 generates a random base58 string.
+ #
+ # The argument _n_ specifies the length, of the random string to be generated.
+ #
+ # If _n_ is not specified or is nil, 16 is assumed. It may be larger in the future.
+ #
+ # The result may contain alphanumeric characters except 0, O, I and l
+ #
+ # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE"
+ # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7"
+ #
+ def self.base58(n = 16)
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
+ idx = byte % 64
+ idx = SecureRandom.random_number(58) if idx >= 58
+ BASE58_ALPHABET[idx]
+ end.join
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb
index ee3b6d2b3f..ebd0dd3fc7 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -8,22 +8,22 @@ class String
# the beginning of the range is greater than the end of the string.
#
# str = "hello"
- # str.at(0) #=> "h"
- # str.at(1..3) #=> "ell"
- # str.at(-2) #=> "l"
- # str.at(-2..-1) #=> "lo"
- # str.at(5) #=> nil
- # str.at(5..-1) #=> ""
+ # str.at(0) # => "h"
+ # str.at(1..3) # => "ell"
+ # str.at(-2) # => "l"
+ # str.at(-2..-1) # => "lo"
+ # str.at(5) # => nil
+ # str.at(5..-1) # => ""
#
# If a Regexp is given, the matching portion of the string is returned.
# If a String is given, that given string is returned if it occurs in
# the string. In both cases, nil is returned if there is no match.
#
# str = "hello"
- # str.at(/lo/) #=> "lo"
- # str.at(/ol/) #=> nil
- # str.at("lo") #=> "lo"
- # str.at("ol") #=> nil
+ # str.at(/lo/) # => "lo"
+ # str.at(/ol/) # => nil
+ # str.at("lo") # => "lo"
+ # str.at("ol") # => nil
def at(position)
self[position]
end
@@ -32,15 +32,15 @@ class String
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
- # str.from(0) #=> "hello"
- # str.from(3) #=> "lo"
- # str.from(-2) #=> "lo"
+ # str.from(0) # => "hello"
+ # str.from(3) # => "lo"
+ # str.from(-2) # => "lo"
#
# You can mix it with +to+ method and do fun things like:
#
# str = "hello"
- # str.from(0).to(-1) #=> "hello"
- # str.from(1).to(-2) #=> "ell"
+ # str.from(0).to(-1) # => "hello"
+ # str.from(1).to(-2) # => "ell"
def from(position)
self[position..-1]
end
@@ -49,34 +49,34 @@ class String
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
- # str.to(0) #=> "h"
- # str.to(3) #=> "hell"
- # str.to(-2) #=> "hell"
+ # str.to(0) # => "h"
+ # str.to(3) # => "hell"
+ # str.to(-2) # => "hell"
#
# You can mix it with +from+ method and do fun things like:
#
# str = "hello"
- # str.from(0).to(-1) #=> "hello"
- # str.from(1).to(-2) #=> "ell"
+ # str.from(0).to(-1) # => "hello"
+ # str.from(1).to(-2) # => "ell"
def to(position)
- self[0, position + 1]
+ self[0..position]
end
# Returns the first character. If a limit is supplied, returns a substring
# from the beginning of the string until it reaches the limit value. If the
- # given limit is greater than or equal to the string length, returns self.
+ # given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
- # str.first #=> "h"
- # str.first(1) #=> "h"
- # str.first(2) #=> "he"
- # str.first(0) #=> ""
- # str.first(6) #=> "hello"
+ # str.first # => "h"
+ # str.first(1) # => "h"
+ # str.first(2) # => "he"
+ # str.first(0) # => ""
+ # str.first(6) # => "hello"
def first(limit = 1)
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
to(limit - 1)
end
@@ -84,19 +84,19 @@ class String
# Returns the last character of the string. If a limit is supplied, returns a substring
# from the end of the string until it reaches the limit value (counting backwards). If
- # the given limit is greater than or equal to the string length, returns self.
+ # the given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
- # str.last #=> "o"
- # str.last(1) #=> "o"
- # str.last(2) #=> "lo"
- # str.last(0) #=> ""
- # str.last(6) #=> "hello"
+ # str.last # => "o"
+ # str.last(1) # => "o"
+ # str.last(2) # => "lo"
+ # str.last(0) # => ""
+ # str.last(6) # => "hello"
def last(limit = 1)
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
from(-limit)
end
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 6691fc0995..3e0cb8a7ac 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -36,20 +36,20 @@ class String
# Converts a string to a Date value.
#
- # "1-1-2012".to_date #=> Sun, 01 Jan 2012
- # "01/01/2012".to_date #=> Sun, 01 Jan 2012
- # "2012-12-13".to_date #=> Thu, 13 Dec 2012
- # "12/13/2012".to_date #=> ArgumentError: invalid date
+ # "1-1-2012".to_date # => Sun, 01 Jan 2012
+ # "01/01/2012".to_date # => Sun, 01 Jan 2012
+ # "2012-12-13".to_date # => Thu, 13 Dec 2012
+ # "12/13/2012".to_date # => ArgumentError: invalid date
def to_date
::Date.parse(self, false) unless blank?
end
# Converts a string to a DateTime value.
#
- # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000
- # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000
- # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000
- # "12/13/2012".to_datetime #=> ArgumentError: invalid date
+ # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000
+ # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000
+ # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000
+ # "12/13/2012".to_datetime # => ArgumentError: invalid date
def to_datetime
::DateTime.parse(self, false) unless blank?
end
diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb
index 114bcb87f0..0ac684f6ee 100644
--- a/activesupport/lib/active_support/core_ext/string/exclude.rb
+++ b/activesupport/lib/active_support/core_ext/string/exclude.rb
@@ -2,9 +2,9 @@ class String
# The inverse of <tt>String#include?</tt>. Returns true if the string
# does not include the other string.
#
- # "hello".exclude? "lo" #=> false
- # "hello".exclude? "ol" #=> true
- # "hello".exclude? ?h #=> false
+ # "hello".exclude? "lo" # => false
+ # "hello".exclude? "ol" # => true
+ # "hello".exclude? ?h # => false
def exclude?(string)
!include?(string)
end
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index 2e2dd86a8b..096292dc58 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -3,7 +3,7 @@ class String
# the string, and then changing remaining consecutive whitespace
# groups into one space each.
#
- # Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E).
+ # Note that it handles both ASCII and Unicode whitespace.
#
# %{ Multi-line
# string }.squish # => "Multi-line string"
@@ -13,6 +13,9 @@ class String
end
# Performs a destructive squish. See String#squish.
+ # str = " foo bar \n \t boo"
+ # str.squish! # => "foo bar boo"
+ # str # => "foo bar boo"
def squish!
gsub!(/\A[[:space:]]+/, '')
gsub!(/[[:space:]]+\z/, '')
@@ -20,14 +23,24 @@ class String
self
end
- # Returns a new string with all occurances of the pattern removed. Short-hand for String#gsub(pattern, '').
- def remove(pattern)
- gsub pattern, ''
+ # Returns a new string with all occurrences of the patterns removed.
+ # str = "foo bar test"
+ # str.remove(" test") # => "foo bar"
+ # str # => "foo bar test"
+ def remove(*patterns)
+ dup.remove!(*patterns)
end
- # Alters the string by removing all occurances of the pattern. Short-hand for String#gsub!(pattern, '').
- def remove!(pattern)
- gsub! pattern, ''
+ # Alters the string by removing all occurrences of the patterns.
+ # str = "foo bar test"
+ # str.remove!(" test") # => "foo bar"
+ # str # => "foo bar"
+ def remove!(*patterns)
+ patterns.each do |pattern|
+ gsub! pattern, ""
+ end
+
+ self
end
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
@@ -62,4 +75,28 @@ class String
"#{self[0, stop]}#{omission}"
end
+
+ # Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
+ #
+ # 'Once upon a time in a world far far away'.truncate_words(4)
+ # # => "Once upon a time..."
+ #
+ # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
+ #
+ # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
+ # # => "Once<br>upon<br>a<br>time<br>in..."
+ #
+ # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
+ #
+ # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
+ # # => "And they found that many... (continued)"
+ def truncate_words(words_count, options = {})
+ sep = options[:separator] || /\s+/
+ sep = Regexp.escape(sep.to_s) unless Regexp === sep
+ if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
+ $1 + (options[:omission] || '...')
+ else
+ dup
+ end
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 56e8a5f98d..38d567c014 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -31,7 +31,7 @@ class String
def pluralize(count = nil, locale = :en)
locale = count if count.is_a?(Symbol)
if count == 1
- self
+ self.dup
else
ActiveSupport::Inflector.pluralize(self, locale)
end
@@ -130,6 +130,8 @@ class String
#
# 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ''
#
# See also +deconstantize+.
def demodulize
@@ -182,21 +184,24 @@ class String
#
# 'egg_and_hams'.classify # => "EggAndHam"
# 'posts'.classify # => "Post"
- #
- # Singular names are not handled correctly.
- #
- # 'business'.classify # => "Business"
def classify
ActiveSupport::Inflector.classify(self)
end
- # Capitalizes the first word, turns underscores into spaces, and strips '_id'.
+ # Capitalizes the first word, turns underscores into spaces, and strips a
+ # trailing '_id' if present.
# Like +titleize+, this is meant for creating pretty output.
#
- # 'employee_salary'.humanize # => "Employee salary"
- # 'author_id'.humanize # => "Author"
- def humanize
- ActiveSupport::Inflector.humanize(self)
+ # The capitalization of the first word can be turned off by setting the
+ # optional parameter +capitalize+ to false.
+ # By default, this parameter is true.
+ #
+ # 'employee_salary'.humanize # => "Employee salary"
+ # 'author_id'.humanize # => "Author"
+ # 'author_id'.humanize(capitalize: false) # => "author"
+ # '_id'.humanize # => "Id"
+ def humanize(options = {})
+ ActiveSupport::Inflector.humanize(self, options)
end
# Creates a foreign key name from a class name.
diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb
index a124202936..2eedd4fdb1 100644
--- a/activesupport/lib/active_support/core_ext/string/multibyte.rb
+++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb
@@ -1,4 +1,3 @@
-# encoding: utf-8
require 'active_support/multibyte'
class String
diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index dc033ed11b..ff5712ed5d 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -4,9 +4,10 @@ require 'active_support/core_ext/kernel/singleton_class'
class ERB
module Util
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
- JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
- HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
- JSON_ESCAPE_REGEXP = /[&"><]/
+ JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
+ HTML_ESCAPE_REGEXP = /[&"'><]/
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
+ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
@@ -17,12 +18,7 @@ class ERB
# puts html_escape('is a > 0 & a < 10?')
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
- s = s.to_s
- if s.html_safe?
- s
- else
- s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe
- end
+ unwrapped_html_escape(s).html_safe
end
# Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
@@ -34,6 +30,18 @@ class ERB
singleton_class.send(:remove_method, :html_escape)
module_function :html_escape
+ # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
+ # This method is not for public consumption! Seriously!
+ def unwrapped_html_escape(s) # :nodoc:
+ s = s.to_s
+ if s.html_safe?
+ s
+ else
+ s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
+ end
+ end
+ module_function :unwrapped_html_escape
+
# A utility method for escaping HTML without affecting existing escaped entities.
#
# html_escape_once('1 < 2 &amp; 3')
@@ -48,17 +56,61 @@ class ERB
module_function :html_escape_once
- # A utility method for escaping HTML entities in JSON strings
- # using \uXXXX JavaScript escape sequences for string literals:
+ # A utility method for escaping HTML entities in JSON strings. Specifically, the
+ # &, > and < characters are replaced with their equivalent unicode escaped form -
+ # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
+ # escaped as they are treated as newline characters in some JavaScript engines.
+ # These sequences have identical meaning as the original characters inside the
+ # context of a JSON string, so assuming the input is a valid and well-formed
+ # JSON value, the output will have equivalent meaning when parsed:
+ #
+ # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
+ # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
+ #
+ # json_escape(json)
+ # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
+ #
+ # JSON.parse(json) == JSON.parse(json_escape(json))
+ # # => true
+ #
+ # The intended use case for this method is to escape JSON strings before including
+ # them inside a script tag to avoid XSS vulnerability:
+ #
+ # <script>
+ # var currentUser = <%= raw json_escape(current_user.to_json) %>;
+ # </script>
#
- # json_escape('is a > 0 & a < 10?')
- # # => is a \u003E 0 \u0026 a \u003C 10?
+ # It is necessary to +raw+ the result of +json_escape+, so that quotation marks
+ # don't get converted to <tt>&quot;</tt> entities. +json_escape+ doesn't
+ # automatically flag the result as HTML safe, since the raw value is unsafe to
+ # use inside HTML attributes.
#
- # Note that after this operation is performed the output is not
- # valid JSON. In particular double quotes are removed:
+ # If your JSON is being used downstream for insertion into the DOM, be aware of
+ # whether or not it is being inserted via +html()+. Most JQuery plugins do this.
+ # If that is the case, be sure to +html_escape+ or +sanitize+ any user-generated
+ # content returned by your JSON.
#
- # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}')
- # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1}
+ # If you need to output JSON elsewhere in your HTML, you can just do something
+ # like this, as any unsafe characters (including quotation marks) will be
+ # automatically escaped for you:
+ #
+ # <div data-user-info="<%= current_user.to_json %>">...</div>
+ #
+ # WARNING: this helper only works with valid JSON. Using this on non-JSON values
+ # will open up serious XSS vulnerabilities. For example, if you replace the
+ # +current_user.to_json+ in the example above with user input instead, the browser
+ # will happily eval() that string as JavaScript.
+ #
+ # The escaping performed in this method is identical to those performed in the
+ # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
+ # set to true. Because this transformation is idempotent, this helper can be
+ # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
+ #
+ # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
+ # is enabled, or if you are unsure where your JSON string originated from, it
+ # is recommended that you always apply this helper (other libraries, such as the
+ # JSON gem, do not provide this kind of protection by default; also some gems
+ # might override +to_json+ to bypass Active Support's encoder).
def json_escape(s)
result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
s.html_safe? ? result.html_safe : result
@@ -84,7 +136,7 @@ module ActiveSupport #:nodoc:
class SafeBuffer < String
UNSAFE_STRING_METHODS = %w(
capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
- slice squeeze strip sub succ swapcase tr tr_s upcase prepend
+ slice squeeze strip sub succ swapcase tr tr_s upcase
)
alias_method :original_concat, :concat
@@ -102,7 +154,11 @@ module ActiveSupport #:nodoc:
else
if html_safe?
new_safe_buffer = super
- new_safe_buffer.instance_eval { @html_safe = true }
+
+ if new_safe_buffer
+ new_safe_buffer.instance_variable_set :@html_safe, true
+ end
+
new_safe_buffer
else
to_str[*args]
@@ -130,28 +186,27 @@ module ActiveSupport #:nodoc:
end
def concat(value)
- if !html_safe? || value.html_safe?
- super(value)
- else
- super(ERB::Util.h(value))
- end
+ super(html_escape_interpolated_argument(value))
end
alias << concat
+ def prepend(value)
+ super(html_escape_interpolated_argument(value))
+ end
+
def +(other)
dup.concat(other)
end
def %(args)
- args = Array(args).map do |arg|
- if !html_safe? || arg.html_safe?
- arg
- else
- ERB::Util.h(arg)
- end
+ case args
+ when Hash
+ escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }]
+ else
+ escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
end
- self.class.new(super(args))
+ self.class.new(super(escaped_args))
end
def html_safe?
@@ -171,7 +226,7 @@ module ActiveSupport #:nodoc:
end
UNSAFE_STRING_METHODS.each do |unsafe_method|
- if 'String'.respond_to?(unsafe_method)
+ if unsafe_method.respond_to?(unsafe_method)
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
@@ -184,10 +239,22 @@ module ActiveSupport #:nodoc:
EOT
end
end
+
+ private
+
+ def html_escape_interpolated_argument(arg)
+ (!html_safe? || arg.html_safe?) ? arg :
+ arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE)
+ end
end
end
class String
+ # Marks a string as trusted safe. It will be inserted into HTML with no
+ # additional escaping performed. It is your responsibilty to ensure that the
+ # string contains no malicious content. This method is equivalent to the
+ # `raw` helper in views. It is recommended that you use `sanitize` instead of
+ # this method. It should never be called on user input.
def html_safe
ActiveSupport::SafeBuffer.new(self)
end
diff --git a/activesupport/lib/active_support/core_ext/struct.rb b/activesupport/lib/active_support/core_ext/struct.rb
index c2c30044f2..1fde3db070 100644
--- a/activesupport/lib/active_support/core_ext/struct.rb
+++ b/activesupport/lib/active_support/core_ext/struct.rb
@@ -1,6 +1,3 @@
-# Backport of Struct#to_h from Ruby 2.0
-class Struct # :nodoc:
- def to_h
- Hash[members.zip(values)]
- end
-end unless Struct.instance_methods.include?(:to_h)
+require 'active_support/deprecation'
+
+ActiveSupport::Deprecation.warn("This file is deprecated and will be removed in Rails 5.1 with no replacement.")
diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb
deleted file mode 100644
index 5481766f10..0000000000
--- a/activesupport/lib/active_support/core_ext/thread.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-class Thread
- LOCK = Mutex.new # :nodoc:
-
- # Returns the value of a thread local variable that has been set. Note that
- # these are different than fiber local values.
- #
- # Thread local values are carried along with threads, and do not respect
- # fibers. For example:
- #
- # Thread.new {
- # Thread.current.thread_variable_set("foo", "bar") # set a thread local
- # Thread.current["foo"] = "bar" # set a fiber local
- #
- # Fiber.new {
- # Fiber.yield [
- # Thread.current.thread_variable_get("foo"), # get the thread local
- # Thread.current["foo"], # get the fiber local
- # ]
- # }.resume
- # }.join.value # => ['bar', nil]
- #
- # The value <tt>"bar"</tt> is returned for the thread local, where +nil+ is returned
- # for the fiber local. The fiber is executed in the same thread, so the
- # thread local values are available.
- def thread_variable_get(key)
- locals[key.to_sym]
- end
-
- # Sets a thread local with +key+ to +value+. Note that these are local to
- # threads, and not to fibers. Please see Thread#thread_variable_get for
- # more information.
- def thread_variable_set(key, value)
- locals[key.to_sym] = value
- end
-
- # Returns an an array of the names of the thread-local variables (as Symbols).
- #
- # thr = Thread.new do
- # Thread.current.thread_variable_set(:cat, 'meow')
- # Thread.current.thread_variable_set("dog", 'woof')
- # end
- # thr.join #=> #<Thread:0x401b3f10 dead>
- # thr.thread_variables #=> [:dog, :cat]
- #
- # Note that these are not fiber local variables. Please see Thread#thread_variable_get
- # for more details.
- def thread_variables
- locals.keys
- end
-
- # Returns <tt>true</tt> if the given string (or symbol) exists as a
- # thread-local variable.
- #
- # me = Thread.current
- # me.thread_variable_set(:oliver, "a")
- # me.thread_variable?(:oliver) #=> true
- # me.thread_variable?(:stanley) #=> false
- #
- # Note that these are not fiber local variables. Please see Thread#thread_variable_get
- # for more details.
- def thread_variable?(key)
- locals.has_key?(key.to_sym)
- end
-
- private
-
- def locals
- if defined?(@locals)
- @locals
- else
- LOCK.synchronize { @locals ||= {} }
- end
- end
-end unless Thread.instance_methods.include?(:thread_variable_set)
diff --git a/activesupport/lib/active_support/core_ext/time.rb b/activesupport/lib/active_support/core_ext/time.rb
index 32cffe237d..72c3234630 100644
--- a/activesupport/lib/active_support/core_ext/time.rb
+++ b/activesupport/lib/active_support/core_ext/time.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/time/acts_like'
require 'active_support/core_ext/time/calculations'
require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/time/marshal'
require 'active_support/core_ext/time/zones'
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index 6e0af0db4d..13610ba19f 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -64,11 +64,12 @@ class Time
# Returns a new Time where one or more of the elements have been changed according
# to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>,
- # <tt>:sec</tt>, <tt>:usec</tt>) reset cascadingly, so if only the hour is passed,
- # then minute, sec, and usec is set to 0. If the hour and minute is passed, then
- # sec and usec is set to 0. The +options+ parameter takes a hash with any of these
- # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>,
- # <tt>:sec</tt>, <tt>:usec</tt>.
+ # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only
+ # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour
+ # and minute is passed, then sec, usec and nsec is set to 0. The +options+
+ # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>,
+ # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>
+ # <tt>:nsec</tt>. Pass either <tt>:usec</tt> or <tt>:nsec</tt>, not both.
#
# Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)
# Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
@@ -80,21 +81,29 @@ class Time
new_hour = options.fetch(:hour, hour)
new_min = options.fetch(:min, options[:hour] ? 0 : min)
new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
- new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+
+ if new_nsec = options[:nsec]
+ raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
+ new_usec = Rational(new_nsec, 1000)
+ else
+ new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+ end
if utc?
::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec)
elsif zone
::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec)
else
+ raise ArgumentError, 'argument out of range' if new_usec > 999999
::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset)
end
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>.
+ # Uses Date to provide precise Time calculations for years, months, and days
+ # according to the proleptic Gregorian calendar. 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)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
@@ -107,6 +116,7 @@ class Time
end
d = to_date.advance(options)
+ d = d.gregorian if d.julian?
time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
@@ -152,7 +162,7 @@ class Time
alias :at_noon :middle_of_day
alias :at_middle_of_day :middle_of_day
- # Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9)
+ # Returns a new Time representing the end of the day, 23:59:59.999999
def end_of_day
change(
:hour => 23,
@@ -169,7 +179,7 @@ class Time
end
alias :at_beginning_of_hour :beginning_of_hour
- # Returns a new Time representing the end of the hour, x:59:59.999999 (.999999999 in ruby1.9)
+ # Returns a new Time representing the end of the hour, x:59:59.999999
def end_of_hour
change(
:min => 59,
@@ -185,7 +195,7 @@ class Time
end
alias :at_beginning_of_minute :beginning_of_minute
- # Returns a new Time representing the end of the minute, x:xx:59.999999 (.999999999 in ruby1.9)
+ # Returns a new Time representing the end of the minute, x:xx:59.999999
def end_of_minute
change(
:sec => 59,
@@ -199,27 +209,6 @@ class Time
beginning_of_day..end_of_day
end
- # Returns a Range representing the whole week of the current time.
- # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
- def all_week(start_day = Date.beginning_of_week)
- beginning_of_week(start_day)..end_of_week(start_day)
- end
-
- # Returns a Range representing the whole month of the current time.
- def all_month
- beginning_of_month..end_of_month
- end
-
- # Returns a Range representing the whole quarter of the current time.
- def all_quarter
- beginning_of_quarter..end_of_quarter
- end
-
- # Returns a Range representing the whole year of the current time.
- def all_year
- beginning_of_year..end_of_year
- end
-
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index 9fd26156c7..dbf1f2f373 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -20,7 +20,7 @@ class Time
:iso8601 => lambda { |time| time.iso8601 }
}
- # Converts to a formatted string. See DATE_FORMATS for builtin formats.
+ # Converts to a formatted string. See DATE_FORMATS for built-in formats.
#
# This method is aliased to <tt>to_s</tt>.
#
diff --git a/activesupport/lib/active_support/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb
index 497c4c3fb8..467bad1726 100644
--- a/activesupport/lib/active_support/core_ext/time/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/time/marshal.rb
@@ -1,30 +1,3 @@
-# Ruby 1.9.2 adds utc_offset and zone to Time, but marshaling only
-# preserves utc_offset. Preserve zone also, even though it may not
-# work in some edge cases.
-if Time.local(2010).zone != Marshal.load(Marshal.dump(Time.local(2010))).zone
- class Time
- class << self
- alias_method :_load_without_zone, :_load
- def _load(marshaled_time)
- time = _load_without_zone(marshaled_time)
- time.instance_eval do
- if zone = defined?(@_zone) && remove_instance_variable('@_zone')
- ary = to_a
- ary[0] += subsec if ary[0] == sec
- ary[-1] = zone
- utc? ? Time.utc(*ary) : Time.local(*ary)
- else
- self
- end
- end
- end
- end
+require 'active_support/deprecation'
- alias_method :_dump_without_zone, :_dump
- def _dump(*args)
- obj = dup
- obj.instance_variable_set('@_zone', zone)
- obj.send :_dump_without_zone, *args
- end
- end
-end
+ActiveSupport::Deprecation.warn("This is deprecated and will be removed in Rails 5.1 with no replacement.")
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index bbda04d60c..0668eadb1e 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -1,4 +1,5 @@
require 'active_support/time_with_zone'
+require 'active_support/core_ext/time/acts_like'
require 'active_support/core_ext/date_and_time/zones'
class Time
@@ -50,7 +51,16 @@ class Time
end
end
- # Returns a TimeZone instance or nil, or raises an ArgumentError for invalid timezones.
+ # Returns a TimeZone instance matching the time zone provided.
+ # Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
+ # Raises an ArgumentError for invalid time zones.
+ #
+ # Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
+ # Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...>
+ # Time.find_zone! -5.hours # => #<ActiveSupport::TimeZone @name="Bogota" ...>
+ # Time.find_zone! nil # => nil
+ # Time.find_zone! false # => false
+ # Time.find_zone! "NOT-A-TIMEZONE" # => ArgumentError: Invalid Timezone: NOT-A-TIMEZONE
def find_zone!(time_zone)
if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone)
time_zone
@@ -71,6 +81,12 @@ class Time
raise ArgumentError, "Invalid Timezone: #{time_zone}"
end
+ # Returns a TimeZone instance matching the time zone provided.
+ # Accepts the time zone in any format supported by <tt>Time.zone=</tt>.
+ # Returns +nil+ for invalid time zones.
+ #
+ # Time.find_zone "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...>
+ # Time.find_zone "NOT-A-TIMEZONE" # => nil
def find_zone(time_zone)
find_zone!(time_zone) rescue nil
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 73559bfe0e..664cc15a29 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -8,6 +8,7 @@ require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/qualified_const'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/name_error'
require 'active_support/core_ext/string/starts_ends_with'
@@ -29,6 +30,10 @@ module ActiveSupport #:nodoc:
mattr_accessor :loaded
self.loaded = Set.new
+ # Stack of files being loaded.
+ mattr_accessor :loading
+ self.loading = []
+
# Should we load files or require them?
mattr_accessor :mechanism
self.mechanism = ENV['NO_RELOAD'] ? :require : :load
@@ -175,14 +180,23 @@ module ActiveSupport #:nodoc:
end
def const_missing(const_name)
- # The interpreter does not pass nesting information, and in the
- # case of anonymous modules we cannot even make the trade-off of
- # assuming their name reflects the nesting. Resort to Object as
- # the only meaningful guess we can make.
- from_mod = anonymous? ? ::Object : self
+ from_mod = anonymous? ? guess_for_anonymous(const_name) : self
Dependencies.load_missing_constant(from_mod, const_name)
end
+ # We assume that the name of the module reflects the nesting
+ # (unless it can be proven that is not the case) and the path to the file
+ # that defines the constant. Anonymous modules cannot follow these
+ # conventions and therefore we assume that the user wants to refer to a
+ # top-level constant.
+ def guess_for_anonymous(const_name)
+ if Object.const_defined?(const_name)
+ raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name
+ else
+ Object
+ end
+ end
+
def unloadable(const_desc = self)
super(const_desc)
end
@@ -191,16 +205,29 @@ module ActiveSupport #:nodoc:
# Object includes this module.
module Loadable #:nodoc:
def self.exclude_from(base)
- base.class_eval { define_method(:load, Kernel.instance_method(:load)) }
+ base.class_eval do
+ define_method(:load, Kernel.instance_method(:load))
+ private :load
+ end
end
def require_or_load(file_name)
Dependencies.require_or_load(file_name)
end
+ # Interprets a file using <tt>mechanism</tt> and marks its defined
+ # constants as autoloaded. <tt>file_name</tt> can be either a string or
+ # respond to <tt>to_path</tt>.
+ #
+ # Use this method in code that absolutely needs a certain constant to be
+ # defined at that point. A typical use case is to make constant name
+ # resolution deterministic for constants with the same relative name in
+ # different namespaces whose evaluation would depend on load order
+ # otherwise.
def require_dependency(file_name, message = "No such file to load -- %s")
+ file_name = file_name.to_path if file_name.respond_to?(:to_path)
unless file_name.is_a?(String)
- raise ArgumentError, "the file name must be a String -- you passed #{file_name.inspect}"
+ raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
end
Dependencies.depend_on(file_name, message)
@@ -217,18 +244,6 @@ module ActiveSupport #:nodoc:
raise
end
- def load(file, wrap = false)
- result = false
- load_dependency(file) { result = super }
- result
- end
-
- def require(file)
- result = false
- load_dependency(file) { result = super }
- result
- end
-
# Mark the given constant as unloadable. Unloadable constants are removed
# each time dependencies are cleared.
#
@@ -245,6 +260,20 @@ module ActiveSupport #:nodoc:
def unloadable(const_desc)
Dependencies.mark_for_unload const_desc
end
+
+ private
+
+ def load(file, wrap = false)
+ result = false
+ load_dependency(file) { result = super }
+ result
+ end
+
+ def require(file)
+ result = false
+ load_dependency(file) { result = super }
+ result
+ end
end
# Exception file-blaming.
@@ -297,6 +326,7 @@ module ActiveSupport #:nodoc:
def clear
log_call
loaded.clear
+ loading.clear
remove_unloadable_constants!
end
@@ -309,6 +339,7 @@ module ActiveSupport #:nodoc:
# Record that we've seen this file *before* loading it to avoid an
# infinite loop with mutual dependencies.
loaded << expanded
+ loading << expanded
begin
if load?
@@ -331,6 +362,8 @@ module ActiveSupport #:nodoc:
rescue Exception
loaded.delete expanded
raise
+ ensure
+ loading.pop
end
# Record history *after* loading so first load gets warnings.
@@ -340,7 +373,7 @@ module ActiveSupport #:nodoc:
# Is the provided constant path defined?
def qualified_const_defined?(path)
- Object.qualified_const_defined?(path.sub(/^::/, ''), false)
+ Object.const_defined?(path, false)
end
# Given +path+, a filesystem path to a ruby file, return an array of
@@ -388,7 +421,8 @@ module ActiveSupport #:nodoc:
end
def load_once_path?(path)
- # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false
+ # to_s works around a ruby issue where String#starts_with?(Pathname)
+ # will raise a TypeError: no implicit conversion of Pathname into String
autoload_once_paths.any? { |base| path.starts_with? base.to_s }
end
@@ -445,8 +479,6 @@ module ActiveSupport #:nodoc:
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end
- raise NameError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name, false)
-
qualified_name = qualified_name_for from_mod, const_name
path_suffix = qualified_name.underscore
@@ -456,10 +488,10 @@ module ActiveSupport #:nodoc:
expanded = File.expand_path(file_path)
expanded.sub!(/\.rb\z/, '')
- if loaded.include?(expanded)
+ if loading.include?(expanded)
raise "Circular dependency detected while autoloading constant #{qualified_name}"
else
- require_or_load(expanded)
+ require_or_load(expanded, qualified_name)
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false)
return from_mod.const_get(const_name)
end
@@ -497,9 +529,9 @@ module ActiveSupport #:nodoc:
end
end
- raise NameError,
- "uninitialized constant #{qualified_name}",
- caller.reject { |l| l.starts_with? __FILE__ }
+ name_error = NameError.new("uninitialized constant #{qualified_name}", const_name)
+ name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ })
+ raise name_error
end
# Remove the constants that have been autoloaded, and those that have been
@@ -575,7 +607,7 @@ module ActiveSupport #:nodoc:
def autoloaded?(desc)
return false if desc.is_a?(Module) && desc.anonymous?
name = to_constant_name desc
- return false unless qualified_const_defined? name
+ return false unless qualified_const_defined?(name)
return autoloaded_constants.include?(name)
end
@@ -648,6 +680,14 @@ module ActiveSupport #:nodoc:
constants = normalized.split('::')
to_remove = constants.pop
+ # Remove the file path from the loaded list.
+ file_path = search_for_file(const.underscore)
+ if file_path
+ expanded = File.expand_path(file_path)
+ expanded.sub!(/\.rb\z/, '')
+ self.loaded.delete(expanded)
+ end
+
if constants.empty?
parent = Object
else
@@ -702,7 +742,7 @@ module ActiveSupport #:nodoc:
protected
def log_call(*args)
if log_activity?
- arg_str = args.collect { |arg| arg.inspect } * ', '
+ arg_str = args.collect(&:inspect) * ', '
/in `([a-z_\?\!]+)'/ =~ caller(1).first
selector = $1 || '<unknown>'
log "called #{selector}(#{arg_str})"
diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb
index c0dba5f7fd..13036d521d 100644
--- a/activesupport/lib/active_support/dependencies/autoload.rb
+++ b/activesupport/lib/active_support/dependencies/autoload.rb
@@ -67,7 +67,7 @@ module ActiveSupport
end
def eager_load!
- @_autoloads.values.each { |file| require file }
+ @_autoloads.each_value { |file| require file }
end
def autoloads
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 0281c9222e..46e9996d59 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -32,7 +32,7 @@ module ActiveSupport
# and the second is a library name
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- def initialize(deprecation_horizon = '4.1', gem_name = 'Rails')
+ def initialize(deprecation_horizon = '5.0', gem_name = 'Rails')
self.gem_name = gem_name
self.deprecation_horizon = deprecation_horizon
# By default, warnings are not silenced and debugging is off.
diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb
index 328b8c320a..9f9dca8453 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -20,7 +20,7 @@ module ActiveSupport
log: ->(message, callstack) {
logger =
- if defined?(Rails) && Rails.logger
+ if defined?(Rails.logger) && Rails.logger
Rails.logger
else
require 'active_support/logger'
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 00727ef73c..5a64fc52cc 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -1,4 +1,3 @@
-require 'active_support/proxy_object'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/object/acts_like'
@@ -7,7 +6,7 @@ module ActiveSupport
# Time#advance, respectively. It mainly supports the methods on Numeric.
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
- class Duration < ProxyObject
+ class Duration
attr_accessor :value, :parts
def initialize(value, parts) #:nodoc:
@@ -39,6 +38,10 @@ module ActiveSupport
end
alias :kind_of? :is_a?
+ def instance_of?(klass) # :nodoc:
+ Duration == klass || value.instance_of?(klass)
+ end
+
# Returns +true+ if +other+ is also a Duration instance with the
# same +value+, or if <tt>other == value</tt>.
def ==(other)
@@ -49,6 +52,20 @@ module ActiveSupport
end
end
+ def to_s
+ @value.to_s
+ end
+
+ # Returns +true+ if +other+ is also a Duration instance, which has the
+ # same parts as this one.
+ def eql?(other)
+ Duration === other && other.value.eql?(value)
+ end
+
+ def hash
+ @value.hash
+ end
+
def self.===(other) #:nodoc:
other.is_a?(Duration)
rescue ::NoMethodError
@@ -70,19 +87,23 @@ module ActiveSupport
alias :until :ago
def inspect #:nodoc:
- val_for = parts.inject(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }
- [:years, :months, :days, :minutes, :seconds].
- select {|unit| val_for[unit].nonzero?}.
- tap {|units| units << :seconds if units.empty?}.
- map {|unit| [unit, val_for[unit]]}.
- map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
- to_sentence(:locale => :en)
+ parts.
+ reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
+ sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
+ map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
+ to_sentence(locale: ::I18n.default_locale)
end
def as_json(options = nil) #:nodoc:
to_i
end
+ def respond_to_missing?(method, include_private=false) #:nodoc:
+ @value.respond_to?(method, include_private)
+ end
+
+ delegate :<=>, to: :value
+
protected
def sum(sign, time = ::Time.current) #:nodoc:
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index d6918bede2..78b627c286 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -92,7 +92,7 @@ module ActiveSupport
def watched
@watched || begin
- all = @files.select { |f| File.exists?(f) }
+ all = @files.select { |f| File.exist?(f) }
all.concat(Dir[@glob]) if @glob
all
end
diff --git a/activesupport/lib/active_support/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb
deleted file mode 100644
index 81e63e76a7..0000000000
--- a/activesupport/lib/active_support/file_watcher.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveSupport
- class FileWatcher
- class Backend
- def initialize(path, watcher)
- @watcher = watcher
- @path = path
- end
-
- def trigger(files)
- @watcher.trigger(files)
- end
- end
-
- def initialize
- @regex_matchers = {}
- end
-
- def watch(pattern, &block)
- @regex_matchers[pattern] = block
- end
-
- def trigger(files)
- trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
-
- files.each do |file, state|
- @regex_matchers.each do |pattern, block|
- trigger_files[block][state] << file if pattern === file
- end
- end
-
- trigger_files.each do |block, payload|
- block.call payload
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
new file mode 100644
index 0000000000..7068f09d87
--- /dev/null
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveSupport
+ # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 3da99872c0..c7477b9ff4 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/hash/reverse_merge'
module ActiveSupport
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
@@ -55,7 +56,7 @@ module ActiveSupport
end
def initialize(constructor = {})
- if constructor.is_a?(Hash)
+ if constructor.respond_to?(:to_hash)
super()
update(constructor)
else
@@ -72,8 +73,10 @@ module ActiveSupport
end
def self.new_from_hash_copying_default(hash)
+ hash = hash.to_hash
new(hash).tap do |new_hash|
new_hash.default = hash.default
+ new_hash.default_proc = hash.default_proc if hash.default_proc
end
end
@@ -125,7 +128,7 @@ module ActiveSupport
if other_hash.is_a? HashWithIndifferentAccess
super(other_hash)
else
- other_hash.each_pair do |key, value|
+ other_hash.to_hash.each_pair do |key, value|
if block_given? && key?(key)
value = yield(convert_key(key), self[key], value)
end
@@ -159,7 +162,7 @@ module ActiveSupport
#
# counters.fetch('foo') # => 1
# counters.fetch(:bar, 0) # => 0
- # counters.fetch(:bar) {|key| 0} # => 0
+ # counters.fetch(:bar) { |key| 0 } # => 0
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
def fetch(key, *extras)
super(convert_key(key), *extras)
@@ -172,10 +175,17 @@ module ActiveSupport
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
def values_at(*indices)
- indices.collect {|key| self[convert_key(key)]}
+ indices.collect { |key| self[convert_key(key)] }
end
- # Returns an exact copy of the hash.
+ # Returns a shallow copy of the hash.
+ #
+ # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } })
+ # dup = hash.dup
+ # dup[:a][:c] = 'c'
+ #
+ # hash[:a][:c] # => nil
+ # dup[:a][:c] # => "c"
def dup
self.class.new(self).tap do |new_hash|
new_hash.default = default
@@ -207,7 +217,7 @@ module ActiveSupport
# Replaces the contents of this hash with other_hash.
#
# h = { "a" => 100, "b" => 200 }
- # h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400}
+ # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
def replace(other_hash)
super(self.class.new_from_hash_copying_default(other_hash))
end
@@ -227,17 +237,21 @@ module ActiveSupport
def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
def to_options!; self end
- def select(*args, &block)
- dup.tap {|hash| hash.select!(*args, &block)}
+ def select(*)
+ super.with_indifferent_access
+ end
+
+ def reject(*)
+ super.with_indifferent_access
end
# Convert to a regular hash with string keys.
def to_hash
- _new_hash= {}
+ _new_hash = Hash.new(default)
each do |key, value|
- _new_hash[convert_key(key)] = convert_value(value, for: :to_hash)
+ _new_hash[key] = convert_value(value, for: :to_hash)
end
- Hash.new(default).merge!(_new_hash)
+ _new_hash
end
protected
@@ -253,7 +267,7 @@ module ActiveSupport
value.nested_under_indifferent_access
end
elsif value.is_a?(Array)
- unless options[:for] == :assignment
+ if options[:for] != :assignment || value.frozen?
value = value.dup
end
value.map! { |e| convert_value(e, options) }
diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 890dd9380b..95f3f6255a 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -31,6 +31,12 @@ module I18n
fallbacks = app.config.i18n.delete(:fallbacks)
+ # Avoid issues with setting the default_locale by disabling available locales
+ # check while configuring.
+ enforce_available_locales = app.config.i18n.delete(:enforce_available_locales)
+ enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
+ I18n.enforce_available_locales = false
+
app.config.i18n.each do |setting, value|
case setting
when :railties_load_path
@@ -44,16 +50,25 @@ module I18n
init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
+ # Restore available locales check so it will take place from now on.
+ I18n.enforce_available_locales = enforce_available_locales
+
reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! }
app.reloaders << reloader
- ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
+ ActionDispatch::Reloader.to_prepare do
+ reloader.execute_if_updated
+ # TODO: remove the following line as soon as the return value of
+ # callbacks is ignored, that is, returning `false` does not
+ # display a deprecation warning or halts the callback chain.
+ true
+ end
reloader.execute
@i18n_inited = true
end
def self.include_fallbacks_module
- I18n.backend.class.send(:include, I18n::Backend::Fallbacks)
+ I18n.backend.class.include(I18n::Backend::Fallbacks)
end
def self.init_fallbacks(fallbacks)
diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb
index 4ea6abfa12..2ca1124e76 100644
--- a/activesupport/lib/active_support/inflections.rb
+++ b/activesupport/lib/active_support/inflections.rb
@@ -1,5 +1,11 @@
require 'active_support/inflector/inflections'
+#--
+# Defines the standard inflection rules. These are the starting point for
+# new projects and are not considered complete. The current set of inflection
+# rules is frozen. This means, we do not change them to become more complete.
+# This is a safety measure to keep existing applications from breaking.
+#++
module ActiveSupport
Inflector.inflections(:en) do |inflect|
inflect.plural(/$/, 's')
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index c96debb93f..486838bd15 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -52,21 +52,21 @@ module ActiveSupport
# into a non-delimited single lowercase word when passed to +underscore+.
#
# acronym 'HTML'
- # titleize 'html' #=> 'HTML'
- # camelize 'html' #=> 'HTML'
- # underscore 'MyHTML' #=> 'my_html'
+ # titleize 'html' # => 'HTML'
+ # camelize 'html' # => 'HTML'
+ # underscore 'MyHTML' # => 'my_html'
#
# The acronym, however, must occur as a delimited unit and not be part of
# another word for conversions to recognize it:
#
# acronym 'HTTP'
- # camelize 'my_http_delimited' #=> 'MyHTTPDelimited'
- # camelize 'https' #=> 'Https', not 'HTTPs'
- # underscore 'HTTPS' #=> 'http_s', not 'https'
+ # camelize 'my_http_delimited' # => 'MyHTTPDelimited'
+ # camelize 'https' # => 'Https', not 'HTTPs'
+ # underscore 'HTTPS' # => 'http_s', not 'https'
#
# acronym 'HTTPS'
- # camelize 'https' #=> 'HTTPS'
- # underscore 'HTTPS' #=> 'https'
+ # camelize 'https' # => 'HTTPS'
+ # underscore 'HTTPS' # => 'https'
#
# Note: Acronyms that are passed to +pluralize+ will no longer be
# recognized, since the acronym will not occur as a delimited unit in the
@@ -74,25 +74,25 @@ module ActiveSupport
# form as an acronym as well:
#
# acronym 'API'
- # camelize(pluralize('api')) #=> 'Apis'
+ # camelize(pluralize('api')) # => 'Apis'
#
# acronym 'APIs'
- # camelize(pluralize('api')) #=> 'APIs'
+ # camelize(pluralize('api')) # => 'APIs'
#
# +acronym+ may be used to specify any word that contains an acronym or
# otherwise needs to maintain a non-standard capitalization. The only
# restriction is that the word must begin with a capital letter.
#
# acronym 'RESTful'
- # underscore 'RESTful' #=> 'restful'
- # underscore 'RESTfulController' #=> 'restful_controller'
- # titleize 'RESTfulController' #=> 'RESTful Controller'
- # camelize 'restful' #=> 'RESTful'
- # camelize 'restful_controller' #=> 'RESTfulController'
+ # underscore 'RESTful' # => 'restful'
+ # underscore 'RESTfulController' # => 'restful_controller'
+ # titleize 'RESTfulController' # => 'RESTful Controller'
+ # camelize 'restful' # => 'RESTful'
+ # camelize 'restful_controller' # => 'RESTfulController'
#
# acronym 'McDonald'
- # underscore 'McDonald' #=> 'mcdonald'
- # camelize 'mcdonald' #=> 'McDonald'
+ # underscore 'McDonald' # => 'mcdonald'
+ # camelize 'mcdonald' # => 'McDonald'
def acronym(word)
@acronyms[word.downcase] = word
@acronym_regex = /#{@acronyms.values.join("|")}/
@@ -154,13 +154,13 @@ module ActiveSupport
end
end
- # Add uncountable words that shouldn't be attempted inflected.
+ # Specifies words that are uncountable and should not be inflected.
#
# uncountable 'money'
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
- (@uncountables << words).flatten!
+ @uncountables += words.flatten.map(&:downcase)
end
# Specifies a humanized form of a string by a regular expression rule or
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index d063fcb49c..fe8a2ac9ba 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -22,49 +22,49 @@ module ActiveSupport
# pluralized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
- # 'post'.pluralize # => "posts"
- # 'octopus'.pluralize # => "octopi"
- # 'sheep'.pluralize # => "sheep"
- # 'words'.pluralize # => "words"
- # 'CamelOctopus'.pluralize # => "CamelOctopi"
- # 'ley'.pluralize(:es) # => "leyes"
+ # pluralize('post') # => "posts"
+ # pluralize('octopus') # => "octopi"
+ # pluralize('sheep') # => "sheep"
+ # pluralize('words') # => "words"
+ # pluralize('CamelOctopus') # => "CamelOctopi"
+ # pluralize('ley', :es) # => "leyes"
def pluralize(word, locale = :en)
apply_inflections(word, inflections(locale).plurals)
end
- # The reverse of +pluralize+, returns the singular form of a word in a
+ # The reverse of #pluralize, returns the singular form of a word in a
# string.
#
# If passed an optional +locale+ parameter, the word will be
- # pluralized using rules defined for that language. By default,
+ # singularized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
- # 'posts'.singularize # => "post"
- # 'octopi'.singularize # => "octopus"
- # 'sheep'.singularize # => "sheep"
- # 'word'.singularize # => "word"
- # 'CamelOctopi'.singularize # => "CamelOctopus"
- # 'leyes'.singularize(:es) # => "ley"
+ # singularize('posts') # => "post"
+ # singularize('octopi') # => "octopus"
+ # singularize('sheep') # => "sheep"
+ # singularize('word') # => "word"
+ # singularize('CamelOctopi') # => "CamelOctopus"
+ # singularize('leyes', :es) # => "ley"
def singularize(word, locale = :en)
apply_inflections(word, inflections(locale).singulars)
end
- # By default, +camelize+ converts strings to UpperCamelCase. If the argument
- # to +camelize+ is set to <tt>:lower</tt> then +camelize+ produces
+ # Converts strings to UpperCamelCase.
+ # If the +uppercase_first_letter+ parameter is set to false, then produces
# lowerCamelCase.
#
- # +camelize+ will also convert '/' to '::' which is useful for converting
+ # Also converts '/' to '::' which is useful for converting
# paths to namespaces.
#
- # 'active_model'.camelize # => "ActiveModel"
- # 'active_model'.camelize(:lower) # => "activeModel"
- # 'active_model/errors'.camelize # => "ActiveModel::Errors"
- # 'active_model/errors'.camelize(:lower) # => "activeModel::Errors"
+ # camelize('active_model') # => "ActiveModel"
+ # camelize('active_model', false) # => "activeModel"
+ # camelize('active_model/errors') # => "ActiveModel::Errors"
+ # camelize('active_model/errors', false) # => "activeModel::Errors"
#
# As a rule of thumb you can think of +camelize+ as the inverse of
- # +underscore+, though there are cases where that does not hold:
+ # #underscore, though there are cases where that does not hold:
#
- # 'SSLError'.underscore.camelize # => "SslError"
+ # camelize(underscore('SSLError')) # => "SslError"
def camelize(term, uppercase_first_letter = true)
string = term.to_s
if uppercase_first_letter
@@ -73,7 +73,7 @@ module ActiveSupport
string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase }
end
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
- string.gsub!('/', '::')
+ string.gsub!(/\//, '::')
string
end
@@ -81,16 +81,17 @@ module ActiveSupport
#
# Changes '::' to '/' to convert namespaces to paths.
#
- # 'ActiveModel'.underscore # => "active_model"
- # 'ActiveModel::Errors'.underscore # => "active_model/errors"
+ # underscore('ActiveModel') # => "active_model"
+ # underscore('ActiveModel::Errors') # => "active_model/errors"
#
# As a rule of thumb you can think of +underscore+ as the inverse of
- # +camelize+, though there are cases where that does not hold:
+ # #camelize, though there are cases where that does not hold:
#
- # 'SSLError'.underscore.camelize # => "SslError"
+ # camelize(underscore('SSLError')) # => "SslError"
def underscore(camel_cased_word)
- word = camel_cased_word.to_s.gsub('::', '/')
- word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
+ word = camel_cased_word.to_s.gsub(/::/, '/')
+ word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" }
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
word.tr!("-", "_")
@@ -98,20 +99,47 @@ module ActiveSupport
word
end
- # Capitalizes the first word and turns underscores into spaces and strips a
- # trailing "_id", if any. Like +titleize+, this is meant for creating pretty
- # output.
+ # Tweaks an attribute name for display to end users.
+ #
+ # Specifically, performs these transformations:
+ #
+ # * Applies human inflection rules to the argument.
+ # * Deletes leading underscores, if any.
+ # * Removes a "_id" suffix if present.
+ # * Replaces underscores with spaces, if any.
+ # * Downcases all words except acronyms.
+ # * Capitalizes the first word.
+ #
+ # The capitalization of the first word can be turned off by setting the
+ # +:capitalize+ option to false (default is true).
+ #
+ # humanize('employee_salary') # => "Employee salary"
+ # humanize('author_id') # => "Author"
+ # humanize('author_id', capitalize: false) # => "author"
+ # humanize('_id') # => "Id"
+ #
+ # If "SSL" was defined to be an acronym:
+ #
+ # humanize('ssl_error') # => "SSL error"
#
- # 'employee_salary'.humanize # => "Employee salary"
- # 'author_id'.humanize # => "Author"
- def humanize(lower_case_and_underscored_word)
+ def humanize(lower_case_and_underscored_word, options = {})
result = lower_case_and_underscored_word.to_s.dup
+
inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
- result.gsub!(/_id$/, "")
+
+ result.sub!(/\A_+/, '')
+ result.sub!(/_id\z/, '')
result.tr!('_', ' ')
- result.gsub(/([a-z\d]*)/i) { |match|
+
+ result.gsub!(/([a-z\d]*)/i) do |match|
"#{inflections.acronyms[match] || match.downcase}"
- }.gsub(/^\w/) { $&.upcase }
+ end
+
+ if options.fetch(:capitalize, true)
+ result.sub!(/\A\w/) { |match| match.upcase }
+ end
+
+ result
end
# Capitalizes all the words and replaces some characters in the string to
@@ -120,34 +148,34 @@ module ActiveSupport
#
# +titleize+ is also aliased as +titlecase+.
#
- # 'man from the boondocks'.titleize # => "Man From The Boondocks"
- # 'x-men: the last stand'.titleize # => "X Men: The Last Stand"
- # 'TheManWithoutAPast'.titleize # => "The Man Without A Past"
- # 'raiders_of_the_lost_ark'.titleize # => "Raiders Of The Lost Ark"
+ # titleize('man from the boondocks') # => "Man From The Boondocks"
+ # titleize('x-men: the last stand') # => "X Men: The Last Stand"
+ # titleize('TheManWithoutAPast') # => "The Man Without A Past"
+ # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
def titleize(word)
humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { $&.capitalize }
end
- # Create the name of a table like Rails does for models to table names. This
- # method uses the +pluralize+ method on the last word in the string.
+ # Creates the name of a table like Rails does for models to table names.
+ # This method uses the #pluralize method on the last word in the string.
#
- # 'RawScaledScorer'.tableize # => "raw_scaled_scorers"
- # 'egg_and_ham'.tableize # => "egg_and_hams"
- # 'fancyCategory'.tableize # => "fancy_categories"
+ # tableize('RawScaledScorer') # => "raw_scaled_scorers"
+ # tableize('egg_and_ham') # => "egg_and_hams"
+ # tableize('fancyCategory') # => "fancy_categories"
def tableize(class_name)
pluralize(underscore(class_name))
end
- # Create a class name from a plural table name like Rails does for table
+ # Creates a class name from a plural table name like Rails does for table
# names to models. Note that this returns a string and not a Class (To
- # convert to an actual class follow +classify+ with +constantize+).
+ # convert to an actual class follow +classify+ with #constantize).
#
- # 'egg_and_hams'.classify # => "EggAndHam"
- # 'posts'.classify # => "Post"
+ # classify('egg_and_hams') # => "EggAndHam"
+ # classify('posts') # => "Post"
#
# Singular names are not handled correctly:
#
- # 'business'.classify # => "Busines"
+ # classify('calculus') # => "Calculu"
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*\./, '')))
@@ -155,17 +183,19 @@ module ActiveSupport
# Replaces underscores with dashes in the string.
#
- # 'puni_puni'.dasherize # => "puni-puni"
+ # dasherize('puni_puni') # => "puni-puni"
def dasherize(underscored_word)
underscored_word.tr('_', '-')
end
# Removes the module part from the expression in the string.
#
- # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
- # 'Inflections'.demodulize # => "Inflections"
+ # demodulize('ActiveRecord::CoreExtensions::String::Inflections') # => "Inflections"
+ # demodulize('Inflections') # => "Inflections"
+ # demodulize('::Inflections') # => "Inflections"
+ # demodulize('') # => ""
#
- # See also +deconstantize+.
+ # See also #deconstantize.
def demodulize(path)
path = path.to_s
if i = path.rindex('::')
@@ -177,13 +207,13 @@ module ActiveSupport
# Removes the rightmost segment from the constant expression in the string.
#
- # 'Net::HTTP'.deconstantize # => "Net"
- # '::Net::HTTP'.deconstantize # => "::Net"
- # 'String'.deconstantize # => ""
- # '::String'.deconstantize # => ""
- # ''.deconstantize # => ""
+ # deconstantize('Net::HTTP') # => "Net"
+ # deconstantize('::Net::HTTP') # => "::Net"
+ # deconstantize('String') # => ""
+ # deconstantize('::String') # => ""
+ # deconstantize('') # => ""
#
- # See also +demodulize+.
+ # See also #demodulize.
def deconstantize(path)
path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename
end
@@ -192,9 +222,9 @@ module ActiveSupport
# +separate_class_name_and_id_with_underscore+ sets whether
# the method should put '_' between the name and 'id'.
#
- # 'Message'.foreign_key # => "message_id"
- # 'Message'.foreign_key(false) # => "messageid"
- # 'Admin::Post'.foreign_key # => "post_id"
+ # foreign_key('Message') # => "message_id"
+ # foreign_key('Message', false) # => "messageid"
+ # foreign_key('Admin::Post') # => "post_id"
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
end
@@ -220,7 +250,7 @@ module ActiveSupport
def constantize(camel_cased_word)
names = camel_cased_word.split('::')
- # Trigger a builtin NameError exception including the ill-formed constant in the message.
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?
# Remove the first blank element in case of '::ClassName' notation.
@@ -234,8 +264,8 @@ module ActiveSupport
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)
- # Go down the ancestors to check it it's owned
- # directly before we reach Object or the end of ancestors.
+ # Go down the ancestors to check if it is owned directly. The check
+ # stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
@@ -250,8 +280,8 @@ module ActiveSupport
# Tries to find a constant with the name specified in the argument string.
#
- # 'Module'.safe_constantize # => Module
- # 'Test::Unit'.safe_constantize # => Test::Unit
+ # safe_constantize('Module') # => Module
+ # safe_constantize('Test::Unit') # => Test::Unit
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
@@ -260,21 +290,21 @@ module ActiveSupport
# C = 'outside'
# module M
# C = 'inside'
- # C # => 'inside'
- # 'C'.safe_constantize # => 'outside', same as ::C
+ # C # => 'inside'
+ # safe_constantize('C') # => 'outside', same as ::C
# end
#
# +nil+ is returned when the name is not in CamelCase or the constant (or
# part of it) is unknown.
#
- # 'blargle'.safe_constantize # => nil
- # 'UnknownModule'.safe_constantize # => nil
- # 'UnknownModule::Foo::Bar'.safe_constantize # => nil
+ # safe_constantize('blargle') # => nil
+ # safe_constantize('UnknownModule') # => nil
+ # safe_constantize('UnknownModule::Foo::Bar') # => nil
def safe_constantize(camel_cased_word)
constantize(camel_cased_word)
rescue NameError => e
- raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ ||
- e.name.to_s == camel_cased_word.to_s
+ raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
+ e.name.to_s == camel_cased_word.to_s)
rescue ArgumentError => e
raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
end
@@ -318,10 +348,11 @@ module ActiveSupport
private
- # Mount a regular expression that will match part by part of the constant.
+ # Mounts a regular expression, returned as a string to ease interpolation,
+ # that will match part by part the given constant.
#
- # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/
- # const_regexp("::") # => /::/
+ # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
+ # const_regexp("::") # => "::"
def const_regexp(camel_cased_word) #:nodoc:
parts = camel_cased_word.split("::")
diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb
index 1cde417fc5..edea142e82 100644
--- a/activesupport/lib/active_support/inflector/transliterate.rb
+++ b/activesupport/lib/active_support/inflector/transliterate.rb
@@ -67,17 +67,8 @@ module ActiveSupport
# Replaces special characters in a string so that it may be used as part of
# a 'pretty' URL.
#
- # class Person
- # def to_param
- # "#{id}-#{name.parameterize}"
- # end
- # end
- #
- # @person = Person.find(1)
- # # => #<Person id: 1, name: "Donald E. Knuth">
- #
- # <%= link_to(@person.name, person_path(@person)) %>
- # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
+ # parameterize("Donald E. Knuth") # => "donald-e-knuth"
+ # parameterize("^trés|Jolie-- ") # => "tres-jolie"
def parameterize(string, sep = '-')
# replace accented chars with their ascii equivalents
parameterized_string = transliterate(string)
@@ -92,6 +83,5 @@ module ActiveSupport
end
parameterized_string.downcase
end
-
end
end
diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb
index 30833a4cb1..35548f3f56 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -7,14 +7,24 @@ module ActiveSupport
mattr_accessor :parse_json_times
module JSON
+ # matches YAML-formatted dates
+ DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
+
class << self
# Parses a JSON string (JavaScript Object Notation) into a hash.
- # See www.json.org for more info.
+ # See http://www.json.org for more info.
#
# ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
# => {"team" => "rails", "players" => "36"}
- def decode(json, proc = nil, options = {})
- data = ::JSON.load(json, proc, options)
+ def decode(json, options = {})
+ if options.present?
+ raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \
+ "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \
+ "and has been removed."
+ end
+
+ data = ::JSON.parse(json, quirks_mode: true)
+
if ActiveSupport.parse_json_times
convert_dates_from(data)
else
diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb
index 77b5d8d227..48f4967892 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -1,344 +1,123 @@
-#encoding: us-ascii
-
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/module/delegation'
-require 'bigdecimal'
-require 'active_support/core_ext/big_decimal/conversions' # for #to_s
-require 'active_support/core_ext/hash/except'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/object/instance_variables'
-require 'time'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/date_time/conversions'
-require 'active_support/core_ext/date/conversions'
-require 'set'
-
module ActiveSupport
class << self
delegate :use_standard_json_time_format, :use_standard_json_time_format=,
+ :time_precision, :time_precision=,
:escape_html_entities_in_json, :escape_html_entities_in_json=,
- :encode_big_decimal_as_string, :encode_big_decimal_as_string=,
+ :json_encoder, :json_encoder=,
:to => :'ActiveSupport::JSON::Encoding'
end
module JSON
- # matches YAML-formatted dates
- DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
-
# Dumps objects in JSON (JavaScript Object Notation).
- # See www.json.org for more info.
+ # See http://www.json.org for more info.
#
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
def self.encode(value, options = nil)
- Encoding::Encoder.new(options).encode(value)
+ Encoding.json_encoder.new(options).encode(value)
end
module Encoding #:nodoc:
- class CircularReferenceError < StandardError; end
-
- class Encoder
+ class JSONGemEncoder #:nodoc:
attr_reader :options
def initialize(options = nil)
@options = options || {}
- @seen = Set.new
end
- def encode(value, use_options = true)
- check_for_circular_references(value) do
- jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
- jsonified.encode_json(self)
- end
+ # Encode the given object into a JSON string
+ def encode(value)
+ stringify jsonify value.as_json(options.dup)
end
- # like encode, but only calls as_json, without encoding to string.
- def as_json(value, use_options = true)
- check_for_circular_references(value) do
- use_options ? value.as_json(options_for(value)) : value.as_json
+ private
+ # Rails does more escaping than the JSON gem natively does (we
+ # escape \u2028 and \u2029 and optionally >, <, & to work around
+ # certain browser problems).
+ ESCAPED_CHARS = {
+ "\u2028" => '\u2028',
+ "\u2029" => '\u2029',
+ '>' => '\u003e',
+ '<' => '\u003c',
+ '&' => '\u0026',
+ }
+
+ ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
+ ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u
+
+ # This class wraps all the strings we see and does the extra escaping
+ class EscapedString < String #:nodoc:
+ def to_json(*)
+ if Encoding.escape_html_entities_in_json
+ super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ else
+ super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ end
+ end
end
- end
- def options_for(value)
- if value.is_a?(Array) || value.is_a?(Hash)
- # hashes and arrays need to get encoder in the options, so that
- # they can detect circular references.
- options.merge(:encoder => self)
- else
- options.dup
+ # Mark these as private so we don't leak encoding-specific constructs
+ private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES,
+ :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
+
+ # Convert an object into a "JSON-ready" representation composed of
+ # primitives like Hash, Array, String, Numeric, and true/false/nil.
+ # Recursively calls #as_json to the object to recursively build a
+ # fully JSON-ready object.
+ #
+ # This allows developers to implement #as_json without having to
+ # worry about what base types of objects they are allowed to return
+ # or having to remember to call #as_json recursively.
+ #
+ # Note: the +options+ hash passed to +object.to_json+ is only passed
+ # to +object.as_json+, not any of this method's recursive +#as_json+
+ # calls.
+ def jsonify(value)
+ case value
+ when String
+ EscapedString.new(value)
+ when Numeric, NilClass, TrueClass, FalseClass
+ value
+ when Hash
+ Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
+ when Array
+ value.map { |v| jsonify(v) }
+ else
+ jsonify value.as_json
+ end
end
- end
-
- def escape(string)
- Encoding.escape(string)
- end
- private
- def check_for_circular_references(value)
- unless @seen.add?(value.__id__)
- raise CircularReferenceError, 'object references itself'
- end
- yield
- ensure
- @seen.delete(value.__id__)
+ # Encode a "jsonified" Ruby data structure using the JSON gem
+ def stringify(jsonified)
+ ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
end
end
-
- ESCAPED_CHARS = {
- "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002',
- "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005',
- "\x06" => '\u0006', "\x07" => '\u0007', "\x0B" => '\u000B',
- "\x0E" => '\u000E', "\x0F" => '\u000F', "\x10" => '\u0010',
- "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013',
- "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016',
- "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019',
- "\x1A" => '\u001A', "\x1B" => '\u001B', "\x1C" => '\u001C',
- "\x1D" => '\u001D', "\x1E" => '\u001E', "\x1F" => '\u001F',
- "\010" => '\b',
- "\f" => '\f',
- "\n" => '\n',
- "\xe2\x80\xa8" => '\u2028',
- "\xe2\x80\xa9" => '\u2029',
- "\r" => '\r',
- "\t" => '\t',
- '"' => '\"',
- '\\' => '\\\\',
- '>' => '\u003E',
- '<' => '\u003C',
- '&' => '\u0026',
- "#{0xe2.chr}#{0x80.chr}#{0xa8.chr}" => '\u2028',
- "#{0xe2.chr}#{0x80.chr}#{0xa9.chr}" => '\u2029',
- }
-
class << self
# If true, use ISO 8601 format for dates and times. Otherwise, fall back
# to the Active Support legacy format.
attr_accessor :use_standard_json_time_format
- # If false, serializes BigDecimal objects as numeric instead of wrapping
- # them in a string.
- attr_accessor :encode_big_decimal_as_string
-
- attr_accessor :escape_regex
- attr_reader :escape_html_entities_in_json
+ # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e)
+ # as a safety measure.
+ attr_accessor :escape_html_entities_in_json
- def escape_html_entities_in_json=(value)
- self.escape_regex = \
- if @escape_html_entities_in_json = value
- /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\><&]/
- else
- /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\]/
- end
- end
+ # Sets the precision of encoded time values.
+ # Defaults to 3 (equivalent to millisecond precision)
+ attr_accessor :time_precision
- def escape(string)
- string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
- json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] }
- json = %("#{json}")
- json.force_encoding(::Encoding::UTF_8)
- json
- end
+ # Sets the encoder used by Rails to encode Ruby objects into JSON strings
+ # in +Object#to_json+ and +ActiveSupport::JSON.encode+.
+ attr_accessor :json_encoder
end
self.use_standard_json_time_format = true
self.escape_html_entities_in_json = true
- self.encode_big_decimal_as_string = true
- end
- end
-end
-
-class Object
- def as_json(options = nil) #:nodoc:
- if respond_to?(:to_hash)
- to_hash
- else
- instance_values
- end
- end
-end
-
-class Struct #:nodoc:
- def as_json(options = nil)
- Hash[members.zip(values)]
- end
-end
-
-class TrueClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class FalseClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class NilClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- 'null'
- end
-end
-
-class String
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- encoder.escape(self)
- end
-end
-
-class Symbol
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-class Numeric
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class Float
- # Encoding Infinity or NaN to JSON should return "null". The default returns
- # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]').
- def as_json(options = nil) #:nodoc:
- finite? ? self : nil
- end
-end
-
-class BigDecimal
- # A BigDecimal would be naturally represented as a JSON number. Most libraries,
- # however, parse non-integer JSON numbers directly as floats. Clients using
- # those libraries would get in general a wrong number and no way to recover
- # other than manually inspecting the string with the JSON code itself.
- #
- # That's why a JSON string is returned. The JSON literal is not numeric, but
- # if the other end knows by contract that the data is supposed to be a
- # BigDecimal, it still has the chance to post-process the string and get the
- # real value.
- #
- # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to
- # override this behavior.
- def as_json(options = nil) #:nodoc:
- if finite?
- ActiveSupport.encode_big_decimal_as_string ? to_s : self
- else
- nil
- end
- end
-end
-
-class Regexp
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-module Enumerable
- def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
- end
-end
-
-class Range
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-class Array
- def as_json(options = nil) #:nodoc:
- # use encoder as a proxy to call as_json on all elements, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- map { |v| encoder.as_json(v, options) }
- end
-
- def encode_json(encoder) #:nodoc:
- # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly
- "[#{map { |v| v.encode_json(encoder) } * ','}]"
- end
-end
-
-class Hash
- def as_json(options = nil) #:nodoc:
- # create a subset of the hash by applying :only or :except
- subset = if options
- if attrs = options[:only]
- slice(*Array(attrs))
- elsif attrs = options[:except]
- except(*Array(attrs))
- else
- self
- end
- else
- self
- end
-
- # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
- end
-
- def encode_json(encoder) #:nodoc:
- # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be
- # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields);
-
- # on the other hand, we need to run as_json on the elements, because the model representation may contain fields
- # like Time/Date in their original (not jsonified) form, etc.
-
- "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}"
- end
-end
-
-class Time
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
- end
- end
-end
-
-class Date
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- strftime("%Y-%m-%d")
- else
- strftime("%Y/%m/%d")
- end
- end
-end
-
-class DateTime
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- strftime('%Y/%m/%d %H:%M:%S %z')
+ self.json_encoder = JSONGemEncoder
+ self.time_precision = 3
end
end
end
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 598c46bce5..51d2da3a79 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -57,18 +57,16 @@ module ActiveSupport
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
if secret.blank?
- raise ArgumentError, "A secret is required to generate an " +
- "integrity hash for cookie session data. Use " +
- "config.secret_key_base = \"some secret phrase of at " +
- "least #{SECRET_MIN_LENGTH} characters\"" +
- "in config/initializers/secret_token.rb"
+ raise ArgumentError, "A secret is required to generate an integrity hash " \
+ "for cookie session data. Set a secret_key_base of at least " \
+ "#{SECRET_MIN_LENGTH} characters in config/secrets.yml."
end
if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, "Secret should be something secure, " +
- "like \"#{SecureRandom.hex(16)}\". The value you " +
- "provided, \"#{secret}\", is shorter than the minimum length " +
- "of #{SECRET_MIN_LENGTH} characters"
+ raise ArgumentError, "Secret should be something secure, " \
+ "like \"#{SecureRandom.hex(16)}\". The value you " \
+ "provided, \"#{secret}\", is shorter than the minimum length " \
+ "of #{SECRET_MIN_LENGTH} characters."
end
end
end
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 4a55bbb350..33fccdcf95 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/logger_silence'
require 'logger'
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index bffdfc6201..92ab6fe648 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -12,7 +12,7 @@ module ActiveSupport
# This can be used in situations similar to the <tt>MessageVerifier</tt>, but
# where you don't want users to be able to determine the value of the payload.
#
- # salt = SecureRandom.random_bytes(64)
+ # salt = SecureRandom.random_bytes(64)
# key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..."
# crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
# encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
@@ -40,6 +40,7 @@ module ActiveSupport
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
+ # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
@@ -47,7 +48,7 @@ module ActiveSupport
@secret = secret
@sign_secret = sign_secret
@cipher = options[:cipher] || 'aes-256-cbc'
- @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
+ @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)
@serializer = options[:serializer] || Marshal
end
@@ -76,12 +77,12 @@ module ActiveSupport
encrypted_data = cipher.update(@serializer.dump(value))
encrypted_data << cipher.final
- [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--")
+ "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
end
def _decrypt(encrypted_message)
cipher = new_cipher
- encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.decode64(v)}
+ encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)}
cipher.decrypt
cipher.key = @secret
@@ -91,7 +92,7 @@ module ActiveSupport
decrypted_data << cipher.final
@serializer.load(decrypted_data)
- rescue OpenSSLCipherError, TypeError
+ rescue OpenSSLCipherError, TypeError, ArgumentError
raise InvalidMessage
end
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index e0cd92ae3c..eee9bbaead 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -1,5 +1,6 @@
require 'base64'
require 'active_support/core_ext/object/blank'
+require 'active_support/security_utils'
module ActiveSupport
# +MessageVerifier+ makes it easy to generate and verify messages which are
@@ -27,37 +28,96 @@ module ActiveSupport
class InvalidSignature < StandardError; end
def initialize(secret, options = {})
+ raise ArgumentError, 'Secret should not be nil.' unless secret
@secret = secret
@digest = options[:digest] || 'SHA1'
@serializer = options[:serializer] || Marshal
end
- def verify(signed_message)
- raise InvalidSignature if signed_message.blank?
+ # Checks if a signed message could have been generated by signing an object
+ # with the +MessageVerifier+'s secret.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ # signed_message = verifier.generate 'a private message'
+ # verifier.valid_message?(signed_message) # => true
+ #
+ # tampered_message = signed_message.chop # editing the message invalidates the signature
+ # verifier.valid_message?(tampered_message) # => false
+ def valid_message?(signed_message)
+ return if signed_message.blank?
data, digest = signed_message.split("--")
- if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
- @serializer.load(::Base64.decode64(data))
- else
- raise InvalidSignature
+ data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
+ end
+
+ # Decodes the signed message using the +MessageVerifier+'s secret.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ #
+ # signed_message = verifier.generate 'a private message'
+ # verifier.verified(signed_message) # => 'a private message'
+ #
+ # Returns +nil+ if the message was not signed with the same secret.
+ #
+ # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
+ # other_verifier.verified(signed_message) # => nil
+ #
+ # Returns +nil+ if the message is not Base64-encoded.
+ #
+ # invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d"
+ # verifier.verified(invalid_message) # => nil
+ #
+ # Raises any error raised while decoding the signed message.
+ #
+ # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
+ # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
+ def verified(signed_message)
+ if valid_message?(signed_message)
+ begin
+ data = signed_message.split("--")[0]
+ @serializer.load(decode(data))
+ rescue ArgumentError => argument_error
+ return if argument_error.message =~ %r{invalid base64}
+ raise
+ end
end
end
+ # Decodes the signed message using the +MessageVerifier+'s secret.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ # signed_message = verifier.generate 'a private message'
+ #
+ # verifier.verify(signed_message) # => 'a private message'
+ #
+ # Raises +InvalidSignature+ if the message was not signed with the same
+ # secret or was not Base64-encoded.
+ #
+ # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
+ # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
+ def verify(signed_message)
+ verified(signed_message) || raise(InvalidSignature)
+ end
+
+ # Generates a signed message for the provided value.
+ #
+ # The message is signed with the +MessageVerifier+'s secret. Without knowing
+ # the secret, the original value cannot be extracted from the message.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
def generate(value)
- data = ::Base64.strict_encode64(@serializer.dump(value))
+ data = encode(@serializer.dump(value))
"#{data}--#{generate_digest(data)}"
end
private
- # constant-time comparison algorithm to prevent timing attacks
- def secure_compare(a, b)
- return false unless a.bytesize == b.bytesize
-
- l = a.unpack "C#{a.bytesize}"
+ def encode(data)
+ ::Base64.strict_encode64(data)
+ end
- res = 0
- b.each_byte { |byte| res |= byte ^ l.shift }
- res == 0
+ def decode(data)
+ ::Base64.strict_decode64(data)
end
def generate_digest(data)
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index a42e7f6542..3c0cf9f137 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -56,11 +56,10 @@ module ActiveSupport #:nodoc:
# Forward all undefined methods to the wrapped string.
def method_missing(method, *args, &block)
+ result = @wrapped_string.__send__(method, *args, &block)
if method.to_s =~ /!$/
- result = @wrapped_string.__send__(method, *args, &block)
self if result
else
- result = @wrapped_string.__send__(method, *args, &block)
result.kind_of?(String) ? chars(result) : result
end
end
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index 04e6b71580..35efebc65f 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -11,7 +11,7 @@ module ActiveSupport
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = '6.2.0'
+ UNICODE_VERSION = '7.0.0'
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -42,7 +42,6 @@ module ActiveSupport
0x0085, # White_Space # Cc <control-0085>
0x00A0, # White_Space # Zs NO-BREAK SPACE
0x1680, # White_Space # Zs OGHAM SPACE MARK
- 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR
(0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
0x2028, # White_Space # Zl LINE SEPARATOR
0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
@@ -212,37 +211,43 @@ module ActiveSupport
codepoints
end
- # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
- # resulting in a valid UTF-8 string.
- #
- # Passing +true+ will forcibly tidy all bytes, assuming that the string's
- # encoding is entirely CP1252 or ISO-8859-1.
- def tidy_bytes(string, force = false)
- return string if string.empty?
-
- if force
- return string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
+ if !defined?(Rubinius)
+ # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
+ # resulting in a valid UTF-8 string.
+ #
+ # Passing +true+ will forcibly tidy all bytes, assuming that the string's
+ # encoding is entirely CP1252 or ISO-8859-1.
+ def tidy_bytes(string, force = false)
+ return string if string.empty?
+ return recode_windows1252_chars(string) if force
+ string.scrub { |bad| recode_windows1252_chars(bad) }
end
+ else
+ def tidy_bytes(string, force = false)
+ return string if string.empty?
+ return recode_windows1252_chars(string) if force
+
+ # We can't transcode to the same format, so we choose a nearly-identical encoding.
+ # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
+ # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
+ # before returning.
+ reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
+
+ source = string.dup
+ out = ''.force_encoding(Encoding::UTF_16LE)
+
+ loop do
+ reader.primitive_convert(source, out)
+ _, _, _, error_bytes, _ = reader.primitive_errinfo
+ break if error_bytes.nil?
+ out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ end
- # We can't transcode to the same format, so we choose a nearly-identical encoding.
- # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
- # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
- # before returning.
- reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC)
-
- source = string.dup
- out = ''.force_encoding(Encoding::UTF_8_MAC)
+ reader.finish
- loop do
- reader.primitive_convert(source, out)
- _, _, _, error_bytes, _ = reader.primitive_errinfo
- break if error_bytes.nil?
- out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ out.encode!(Encoding::UTF_8)
end
-
- reader.finish
-
- out.encode!(Encoding::UTF_8)
end
# Returns the KC normalization of the string by default. NFKC is
@@ -287,6 +292,13 @@ module ActiveSupport
class Codepoint
attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
+ # Initializing Codepoint object with default values
+ def initialize
+ @combining_class = 0
+ @uppercase_mapping = 0
+ @lowercase_mapping = 0
+ end
+
def swapcase_mapping
uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
end
@@ -322,7 +334,7 @@ module ActiveSupport
begin
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
rescue => e
- raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
+ raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
end
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
@@ -354,6 +366,7 @@ module ActiveSupport
private
def apply_mapping(string, mapping) #:nodoc:
+ database.codepoints
string.each_codepoint.map do |codepoint|
cp = database.codepoints[codepoint]
if cp and (ncp = cp.send(mapping)) and ncp > 0
@@ -364,20 +377,13 @@ module ActiveSupport
end.pack('U*')
end
- def tidy_byte(byte)
- if byte < 160
- [database.cp1252[byte] || byte].pack("U").unpack("C*")
- elsif byte < 192
- [194, byte]
- else
- [195, byte - 64]
- end
+ def recode_windows1252_chars(string)
+ string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
def database
@database ||= UnicodeDatabase.new
end
-
end
end
end
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index b32aa75e59..b9f8e1ab2c 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -16,7 +16,7 @@ module ActiveSupport
# render text: 'Foo'
# end
#
- # That executes the block first and notifies all subscribers once done.
+ # That first executes the block and then notifies all subscribers once done.
#
# In the example above +render+ is the name of the event, and the rest is called
# the _payload_. The payload is a mechanism that allows instrumenters to pass
@@ -141,6 +141,11 @@ module ActiveSupport
#
# ActiveSupport::Notifications.unsubscribe(subscriber)
#
+ # You can also unsubscribe by passing the name of the subscriber object. Note
+ # that this will unsubscribe all subscriptions with the given name:
+ #
+ # ActiveSupport::Notifications.unsubscribe("render")
+ #
# == Default Queue
#
# Notifications ships with a queue implementation that consumes and publishes events
@@ -173,12 +178,12 @@ module ActiveSupport
unsubscribe(subscriber)
end
- def unsubscribe(args)
- notifier.unsubscribe(args)
+ def unsubscribe(subscriber_or_name)
+ notifier.unsubscribe(subscriber_or_name)
end
def instrumenter
- InstrumentationRegistry.instrumenter_for(notifier)
+ InstrumentationRegistry.instance.instrumenter_for(notifier)
end
end
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 99fe03e6d0..6bf8c7d5de 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -25,9 +25,15 @@ module ActiveSupport
subscriber
end
- def unsubscribe(subscriber)
+ def unsubscribe(subscriber_or_name)
synchronize do
- @subscribers.reject! { |s| s.matches?(subscriber) }
+ case subscriber_or_name
+ when String
+ @subscribers.reject! { |s| s.matches?(subscriber_or_name) }
+ else
+ @subscribers.delete(subscriber_or_name)
+ end
+
@listeners_for.clear
end
end
@@ -97,31 +103,27 @@ module ActiveSupport
end
def subscribed_to?(name)
- @pattern === name.to_s
+ @pattern === name
end
- def matches?(subscriber_or_name)
- self === subscriber_or_name ||
- @pattern && @pattern === subscriber_or_name
+ def matches?(name)
+ @pattern && @pattern === name
end
end
class Timed < Evented
- def initialize(pattern, delegate)
- @timestack = []
- super
- end
-
def publish(name, *args)
@delegate.call name, *args
end
def start(name, id, payload)
- @timestack.push Time.now
+ timestack = Thread.current[:_timestack] ||= []
+ timestack.push Time.now
end
def finish(name, id, payload)
- started = @timestack.pop
+ timestack = Thread.current[:_timestack]
+ started = timestack.pop
@delegate.call(name, started, Time.now, id, payload)
end
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index 0c9a729ce5..075ddc2382 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -54,10 +54,23 @@ module ActiveSupport
@transaction_id = transaction_id
@end = ending
@children = []
+ @duration = nil
end
+ # Returns the difference in milliseconds between when the execution of the
+ # event started and when it ended.
+ #
+ # ActiveSupport::Notifications.subscribe('wait') do |*args|
+ # @event = ActiveSupport::Notifications::Event.new(*args)
+ # end
+ #
+ # ActiveSupport::Notifications.instrument('wait') do
+ # sleep 1
+ # end
+ #
+ # @event.duration # => 1000.138
def duration
- 1000.0 * (self.end - time)
+ @duration ||= 1000.0 * (self.end - time)
end
def <<(event)
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 237ef7087f..cfca42bc69 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -1,115 +1,19 @@
-require 'active_support/core_ext/big_decimal/conversions'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/i18n'
-
module ActiveSupport
module NumberHelper
- extend self
-
- DEFAULTS = {
- # Used in number_to_delimited
- # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
- format: {
- # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
- separator: ".",
- # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
- delimiter: ",",
- # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
- precision: 3,
- # If set to true, precision will mean the number of significant digits instead
- # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
- significant: false,
- # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
- strip_insignificant_zeros: false
- },
-
- # Used in number_to_currency
- currency: {
- format: {
- format: "%u%n",
- negative_format: "-%u%n",
- unit: "$",
- # These five are to override number.format and are optional
- separator: ".",
- delimiter: ",",
- precision: 2,
- significant: false,
- strip_insignificant_zeros: false
- }
- },
-
- # Used in number_to_percentage
- percentage: {
- format: {
- delimiter: "",
- format: "%n%"
- }
- },
-
- # Used in number_to_rounded
- precision: {
- format: {
- delimiter: ""
- }
- },
-
- # Used in number_to_human_size and number_to_human
- human: {
- format: {
- # These five are to override number.format and are optional
- delimiter: "",
- precision: 3,
- significant: true,
- strip_insignificant_zeros: true
- },
- # Used in number_to_human_size
- storage_units: {
- # Storage units output formatting.
- # %u is the storage unit, %n is the number (default: 2 MB)
- format: "%n %u",
- units: {
- byte: "Bytes",
- kb: "KB",
- mb: "MB",
- gb: "GB",
- tb: "TB"
- }
- },
- # Used in number_to_human
- decimal_units: {
- format: "%n %u",
- # Decimal units output formatting
- # By default we will only quantify some of the exponents
- # but the commented ones might be defined or overridden
- # by the user.
- units: {
- # femto: Quadrillionth
- # pico: Trillionth
- # nano: Billionth
- # micro: Millionth
- # mili: Thousandth
- # centi: Hundredth
- # deci: Tenth
- unit: "",
- # ten:
- # one: Ten
- # other: Tens
- # hundred: Hundred
- thousand: "Thousand",
- million: "Million",
- billion: "Billion",
- trillion: "Trillion",
- quadrillion: "Quadrillion"
- }
- }
- }
- }
-
- DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
- -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :NumberConverter
+ autoload :NumberToRoundedConverter
+ autoload :NumberToDelimitedConverter
+ autoload :NumberToHumanConverter
+ autoload :NumberToHumanSizeConverter
+ autoload :NumberToPhoneConverter
+ autoload :NumberToCurrencyConverter
+ autoload :NumberToPercentageConverter
+ end
- STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+ extend self
# Formats a +number+ into a US phone number (e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
@@ -137,27 +41,7 @@ module ActiveSupport
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
# # => +1.123.555.1234 x 1343
def number_to_phone(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- number = number.to_s.strip
- area_code = options[:area_code]
- delimiter = options[:delimiter] || "-"
- extension = options[:extension]
- country_code = options[:country_code]
-
- if area_code
- number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
- else
- number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
- number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank?
- end
-
- str = ''
- str << "+#{country_code}#{delimiter}" unless country_code.blank?
- str << number
- str << " x #{extension}" unless extension.blank?
- str
+ NumberToPhoneConverter.convert(number, options)
end
# Formats a +number+ into a currency string (e.g., $13.65). You
@@ -199,25 +83,7 @@ module ActiveSupport
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
# # => 1234567890,50 &pound;
def number_to_currency(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- currency = i18n_format_options(options[:locale], :currency)
- currency[:negative_format] ||= "-" + currency[:format] if currency[:format]
-
- defaults = default_format_options(:currency).merge!(currency)
- defaults[:negative_format] = "-" + options[:format] if options[:format]
- options = defaults.merge!(options)
-
- unit = options.delete(:unit)
- format = options.delete(:format)
-
- if number.to_f.phase != 0
- format = options.delete(:negative_format)
- number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '')
- end
-
- format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit)
+ NumberToCurrencyConverter.convert(number, options)
end
# Formats a +number+ as a percentage string (e.g., 65%). You can
@@ -250,18 +116,11 @@ module ActiveSupport
# number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
# number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
# number_to_percentage(1000, locale: :fr) # => 1 000,000%
- # number_to_percentage:(1000, precision: nil) # => 1000%
+ # number_to_percentage:(1000, precision: nil) # => 1000%
# number_to_percentage('98a') # => 98a%
# number_to_percentage(100, format: '%n %') # => 100 %
def number_to_percentage(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- defaults = format_options(options[:locale], :percentage)
- options = defaults.merge!(options)
-
- format = options[:format] || "%n%"
- format.gsub('%n', self.number_to_rounded(number, options))
+ NumberToPercentageConverter.convert(number, options)
end
# Formats a +number+ with grouped thousands using +delimiter+
@@ -290,15 +149,7 @@ module ActiveSupport
# number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
# # => 98 765 432,98
def number_to_delimited(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
-
- options = format_options(options[:locale]).merge!(options)
-
- parts = number.to_s.split('.')
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
- parts.join(options[:separator])
+ NumberToDelimitedConverter.convert(number, options)
end
# Formats a +number+ with the specified level of
@@ -342,43 +193,7 @@ module ActiveSupport
# number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
# # => 1.111,23
def number_to_rounded(number, options = {})
- return number unless valid_float?(number)
- options = options.symbolize_keys
-
- defaults = format_options(options[:locale], :precision)
- options = defaults.merge!(options)
-
- precision = options.delete :precision
- significant = options.delete :significant
- strip_insignificant_zeros = options.delete :strip_insignificant_zeros
-
- if precision.nil?
- formatted_number = self.number_to_delimited(number, options)
- else
- number = Float(number)
- if significant && precision > 0
- if number == 0
- digits, rounded_number = 1, 0
- else
- digits = (Math.log10(number.abs) + 1).floor
- multiplier = 10 ** (digits - precision)
- rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier
- digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed
- end
- precision -= digits
- precision = 0 if precision < 0 # don't let it be negative
- else
- rounded_number = BigDecimal.new(number.to_s).round(precision).to_f
- rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
- end
- formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options)
- end
- if strip_insignificant_zeros
- escaped_separator = Regexp.escape(options[:separator])
- formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
- else
- formatted_number
- end
+ NumberToRoundedConverter.convert(number, options)
end
# Formats the bytes in +number+ into a more understandable
@@ -419,43 +234,10 @@ module ActiveSupport
# number_to_human_size(1234567, precision: 2) # => 1.2 MB
# number_to_human_size(483989, precision: 2) # => 470 KB
# number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
- #
- # Non-significant zeros after the fractional separator are stripped out by
- # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that):
- #
- # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
- # number_to_human_size(524288000, precision: 5) # => "500 MB"
+ # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
+ # number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
- number = Float(number)
-
- defaults = format_options(options[:locale], :human)
- options = defaults.merge!(options)
-
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
- options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
-
- storage_units_format = translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
-
- base = options[:prefix] == :si ? 1000 : 1024
-
- if number.to_i < base
- unit = translate_number_value_with_default('human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true)
- storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
- else
- max_exp = STORAGE_UNITS.size - 1
- exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base
- exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
- number /= base ** exponent
-
- unit_key = STORAGE_UNITS[exponent]
- unit = translate_number_value_with_default("human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true)
-
- formatted_number = self.number_to_rounded(number, options)
- storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit)
- end
+ NumberToHumanSizeConverter.convert(number, options)
end
# Pretty prints (formats and approximates) a number in a way it
@@ -466,7 +248,7 @@ module ActiveSupport
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
- # You can also define you own unit-quantifier names if you want
+ # You can also define your own unit-quantifier names if you want
# to use other decimal units (eg.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
@@ -492,12 +274,12 @@ module ActiveSupport
# string containing an i18n scope where to find this hash. It
# might have the following keys:
# * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
- # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
- # *<tt>:billion</tt>, <tt>:trillion</tt>,
- # *<tt>:quadrillion</tt>
+ # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
+ # <tt>:billion</tt>, <tt>:trillion</tt>,
+ # <tt>:quadrillion</tt>
# * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
- # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
- # *<tt>:pico</tt>, <tt>:femto</tt>
+ # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
+ # <tt>:pico</tt>, <tt>:femto</tt>
# * <tt>:format</tt> - Sets the format of the output string
# (defaults to "%n %u"). The field types are:
# * %u - The quantifier (ex.: 'thousand')
@@ -521,12 +303,15 @@ module ActiveSupport
# separator: ',',
# significant: false) # => "1,2 Million"
#
+ # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12345012345, significant: false) # => "12.345 Billion"
+ #
# Non-significant zeros after the decimal separator are stripped
# out by default (set <tt>:strip_insignificant_zeros</tt> to
# +false+ to change that):
#
- # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion"
- # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12.00001) # => "12"
+ # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
#
# ==== Custom Unit Quantifiers
#
@@ -556,88 +341,7 @@ module ActiveSupport
# number_to_human(1, units: :distance) # => "1 meter"
# number_to_human(0.34, units: :distance) # => "34 centimeters"
def number_to_human(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
- number = Float(number)
-
- defaults = format_options(options[:locale], :human)
- options = defaults.merge!(options)
-
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
- options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
-
- inverted_du = DECIMAL_UNITS.invert
-
- units = options.delete :units
- unit_exponents = case units
- when Hash
- units
- when String, Symbol
- I18n.translate(:"#{units}", :locale => options[:locale], :raise => true)
- when nil
- translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true)
- else
- raise ArgumentError, ":units must be a Hash or String translation scope."
- end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e}
-
- number_exponent = number != 0 ? Math.log10(number.abs).floor : 0
- display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0
- number /= 10 ** display_exponent
-
- unit = case units
- when Hash
- units[DECIMAL_UNITS[display_exponent]] || ''
- when String, Symbol
- I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
- else
- translate_number_value_with_default("human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
- end
-
- decimal_format = options[:format] || translate_number_value_with_default('human.decimal_units.format', :locale => options[:locale])
- formatted_number = self.number_to_rounded(number, options)
- decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip
- end
-
- def self.private_module_and_instance_method(method_name) #:nodoc:
- private method_name
- private_class_method method_name
- end
- private_class_method :private_module_and_instance_method
-
- def format_options(locale, namespace = nil) #:nodoc:
- default_format_options(namespace).merge!(i18n_format_options(locale, namespace))
- end
- private_module_and_instance_method :format_options
-
- def default_format_options(namespace = nil) #:nodoc:
- options = DEFAULTS[:format].dup
- options.merge!(DEFAULTS[namespace][:format]) if namespace
- options
- end
- private_module_and_instance_method :default_format_options
-
- def i18n_format_options(locale, namespace = nil) #:nodoc:
- options = I18n.translate(:'number.format', locale: locale, default: {}).dup
- if namespace
- options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
- end
- options
- end
- private_module_and_instance_method :i18n_format_options
-
- def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
- default = key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
-
- I18n.translate(key, { default: default, scope: :number }.merge!(i18n_options))
- end
- private_module_and_instance_method :translate_number_value_with_default
-
- def valid_float?(number) #:nodoc:
- Float(number)
- rescue ArgumentError, TypeError
- false
+ NumberToHumanConverter.convert(number, options)
end
- private_module_and_instance_method :valid_float?
end
end
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
new file mode 100644
index 0000000000..9d976f1831
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -0,0 +1,182 @@
+require 'active_support/core_ext/big_decimal/conversions'
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/hash/keys'
+require 'active_support/i18n'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveSupport
+ module NumberHelper
+ class NumberConverter # :nodoc:
+ # Default and i18n option namespace per class
+ class_attribute :namespace
+
+ # Does the object need a number that is a valid float?
+ class_attribute :validate_float
+
+ attr_reader :number, :opts
+
+ DEFAULTS = {
+ # Used in number_to_delimited
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format: {
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: ".",
+ # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: ",",
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
+ precision: 3,
+ # If set to true, precision will mean the number of significant digits instead
+ # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
+ significant: false,
+ # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
+ strip_insignificant_zeros: false
+ },
+
+ # Used in number_to_currency
+ currency: {
+ format: {
+ format: "%u%n",
+ negative_format: "-%u%n",
+ unit: "$",
+ # These five are to override number.format and are optional
+ separator: ".",
+ delimiter: ",",
+ precision: 2,
+ significant: false,
+ strip_insignificant_zeros: false
+ }
+ },
+
+ # Used in number_to_percentage
+ percentage: {
+ format: {
+ delimiter: "",
+ format: "%n%"
+ }
+ },
+
+ # Used in number_to_rounded
+ precision: {
+ format: {
+ delimiter: ""
+ }
+ },
+
+ # Used in number_to_human_size and number_to_human
+ human: {
+ format: {
+ # These five are to override number.format and are optional
+ delimiter: "",
+ precision: 3,
+ significant: true,
+ strip_insignificant_zeros: true
+ },
+ # Used in number_to_human_size
+ storage_units: {
+ # Storage units output formatting.
+ # %u is the storage unit, %n is the number (default: 2 MB)
+ format: "%n %u",
+ units: {
+ byte: "Bytes",
+ kb: "KB",
+ mb: "MB",
+ gb: "GB",
+ tb: "TB"
+ }
+ },
+ # Used in number_to_human
+ decimal_units: {
+ format: "%n %u",
+ # Decimal units output formatting
+ # By default we will only quantify some of the exponents
+ # but the commented ones might be defined or overridden
+ # by the user.
+ units: {
+ # femto: Quadrillionth
+ # pico: Trillionth
+ # nano: Billionth
+ # micro: Millionth
+ # mili: Thousandth
+ # centi: Hundredth
+ # deci: Tenth
+ unit: "",
+ # ten:
+ # one: Ten
+ # other: Tens
+ # hundred: Hundred
+ thousand: "Thousand",
+ million: "Million",
+ billion: "Billion",
+ trillion: "Trillion",
+ quadrillion: "Quadrillion"
+ }
+ }
+ }
+ }
+
+ def self.convert(number, options)
+ new(number, options).execute
+ end
+
+ def initialize(number, options)
+ @number = number
+ @opts = options.symbolize_keys
+ end
+
+ def execute
+ if !number
+ nil
+ elsif validate_float? && !valid_float?
+ number
+ else
+ convert
+ end
+ end
+
+ private
+
+ def options
+ @options ||= format_options.merge(opts)
+ end
+
+ def format_options #:nodoc:
+ default_format_options.merge!(i18n_format_options)
+ end
+
+ def default_format_options #:nodoc:
+ options = DEFAULTS[:format].dup
+ options.merge!(DEFAULTS[namespace][:format]) if namespace
+ options
+ end
+
+ def i18n_format_options #:nodoc:
+ locale = opts[:locale]
+ options = I18n.translate(:'number.format', locale: locale, default: {}).dup
+
+ if namespace
+ options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
+ end
+
+ options
+ end
+
+ def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
+ I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options))
+ end
+
+ def translate_in_locale(key, i18n_options = {})
+ translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options))
+ end
+
+ def default_value(key)
+ key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
+ end
+
+ def valid_float? #:nodoc:
+ Float(number)
+ rescue ArgumentError, TypeError
+ false
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
new file mode 100644
index 0000000000..fb5adb574a
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
@@ -0,0 +1,46 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToCurrencyConverter < NumberConverter # :nodoc:
+ self.namespace = :currency
+
+ def convert
+ number = self.number.to_s.strip
+ format = options[:format]
+
+ if is_negative?(number)
+ format = options[:negative_format]
+ number = absolute_value(number)
+ end
+
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit])
+ end
+
+ private
+
+ def is_negative?(number)
+ number.to_f.phase != 0
+ end
+
+ def absolute_value(number)
+ number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '')
+ end
+
+ def options
+ @options ||= begin
+ defaults = default_format_options.merge(i18n_opts)
+ # Override negative format if format options is given
+ defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
+ defaults.merge!(opts)
+ end
+ end
+
+ def i18n_opts
+ # Set International negative format if not exists
+ i18n = i18n_format_options
+ i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
+ i18n
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
new file mode 100644
index 0000000000..d85cc086d7
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -0,0 +1,23 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToDelimitedConverter < NumberConverter #:nodoc:
+ self.validate_float = true
+
+ DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
+
+ def convert
+ parts.join(options[:separator])
+ end
+
+ private
+
+ def parts
+ left, right = number.to_s.split('.')
+ left.gsub!(DELIMITED_REGEX) do |digit_to_delimit|
+ "#{digit_to_delimit}#{options[:delimiter]}"
+ end
+ [left, right].compact
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
new file mode 100644
index 0000000000..6940beb318
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
@@ -0,0 +1,66 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToHumanConverter < NumberConverter # :nodoc:
+ DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
+ -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
+ INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
+
+ self.namespace = :human
+ self.validate_float = true
+
+ def convert # :nodoc:
+ @number = Float(number)
+
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
+ unless options.key?(:strip_insignificant_zeros)
+ options[:strip_insignificant_zeros] = true
+ end
+
+ units = opts[:units]
+ exponent = calculate_exponent(units)
+ @number = number / (10 ** exponent)
+
+ unit = determine_unit(units, exponent)
+
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip
+ end
+
+ private
+
+ def format
+ options[:format] || translate_in_locale('human.decimal_units.format')
+ end
+
+ def determine_unit(units, exponent)
+ exp = DECIMAL_UNITS[exponent]
+ case units
+ when Hash
+ units[exp] || ''
+ when String, Symbol
+ I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i)
+ else
+ translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
+ end
+ end
+
+ def calculate_exponent(units)
+ exponent = number != 0 ? Math.log10(number.abs).floor : 0
+ unit_exponents(units).find { |e| exponent >= e } || 0
+ end
+
+ def unit_exponents(units)
+ case units
+ when Hash
+ units
+ when String, Symbol
+ I18n.translate(units.to_s, :locale => options[:locale], :raise => true)
+ when nil
+ translate_in_locale("human.decimal_units.units", raise: true)
+ else
+ raise ArgumentError, ":units must be a Hash or String translation scope."
+ end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
new file mode 100644
index 0000000000..78d2c9ae6e
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
@@ -0,0 +1,58 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToHumanSizeConverter < NumberConverter #:nodoc:
+ STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+
+ self.namespace = :human
+ self.validate_float = true
+
+ def convert
+ @number = Float(number)
+
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
+ unless options.key?(:strip_insignificant_zeros)
+ options[:strip_insignificant_zeros] = true
+ end
+
+ if smaller_than_base?
+ number_to_format = number.to_i.to_s
+ else
+ human_size = number / (base ** exponent)
+ number_to_format = NumberToRoundedConverter.convert(human_size, options)
+ end
+ conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit)
+ end
+
+ private
+
+ def conversion_format
+ translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
+ end
+
+ def unit
+ translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => number.to_i, :raise => true)
+ end
+
+ def storage_unit_key
+ key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent]
+ "human.storage_units.units.#{key_end}"
+ end
+
+ def exponent
+ max = STORAGE_UNITS.size - 1
+ exp = (Math.log(number) / Math.log(base)).to_i
+ exp = max if exp > max # avoid overflow for the highest unit
+ exp
+ end
+
+ def smaller_than_base?
+ number.to_i < base
+ end
+
+ def base
+ opts[:prefix] == :si ? 1000 : 1024
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
new file mode 100644
index 0000000000..1af294a03e
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
@@ -0,0 +1,12 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToPercentageConverter < NumberConverter # :nodoc:
+ self.namespace = :percentage
+
+ def convert
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ options[:format].gsub(/%n/, rounded_number)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
new file mode 100644
index 0000000000..af2ee56d91
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
@@ -0,0 +1,49 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToPhoneConverter < NumberConverter #:nodoc:
+ def convert
+ str = country_code(opts[:country_code])
+ str << convert_to_phone_number(number.to_s.strip)
+ str << phone_ext(opts[:extension])
+ end
+
+ private
+
+ def convert_to_phone_number(number)
+ if opts[:area_code]
+ convert_with_area_code(number)
+ else
+ convert_without_area_code(number)
+ end
+ end
+
+ def convert_with_area_code(number)
+ number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ number
+ end
+
+ def convert_without_area_code(number)
+ number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ number.slice!(0, 1) if start_with_delimiter?(number)
+ number
+ end
+
+ def start_with_delimiter?(number)
+ delimiter.present? && number.start_with?(delimiter)
+ end
+
+ def delimiter
+ opts[:delimiter] || "-"
+ end
+
+ def country_code(code)
+ code.blank? ? "" : "+#{code}#{delimiter}"
+ end
+
+ def phone_ext(ext)
+ ext.blank? ? "" : " x #{ext}"
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
new file mode 100644
index 0000000000..df316a08e6
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -0,0 +1,90 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToRoundedConverter < NumberConverter # :nodoc:
+ self.namespace = :precision
+ self.validate_float = true
+
+ def convert
+ precision = options.delete :precision
+
+ if precision
+ case number
+ when Float, String
+ @number = BigDecimal(number.to_s)
+ when Rational
+ @number = BigDecimal(number, digit_count(number.to_i) + precision)
+ else
+ @number = number.to_d
+ end
+
+ if options.delete(:significant) && precision > 0
+ digits, rounded_number = digits_and_rounded_number(precision)
+ precision -= digits
+ precision = 0 if precision < 0 # don't let it be negative
+ else
+ rounded_number = number.round(precision)
+ rounded_number = rounded_number.to_i if precision == 0
+ rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
+ end
+
+ formatted_string =
+ if BigDecimal === rounded_number && rounded_number.finite?
+ s = rounded_number.to_s('F') + '0'*precision
+ a, b = s.split('.', 2)
+ a + '.' + b[0, precision]
+ else
+ "%00.#{precision}f" % rounded_number
+ end
+ else
+ formatted_string = number
+ end
+
+ delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
+ format_number(delimited_number)
+ end
+
+ private
+
+ def digits_and_rounded_number(precision)
+ if zero?
+ [1, 0]
+ else
+ digits = digit_count(number)
+ multiplier = 10 ** (digits - precision)
+ rounded_number = calculate_rounded_number(multiplier)
+ digits = digit_count(rounded_number) # After rounding, the number of digits may have changed
+ [digits, rounded_number]
+ end
+ end
+
+ def calculate_rounded_number(multiplier)
+ (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
+ end
+
+ def digit_count(number)
+ number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor
+ end
+
+ def strip_insignificant_zeros
+ options[:strip_insignificant_zeros]
+ end
+
+ def format_number(number)
+ if strip_insignificant_zeros
+ escaped_separator = Regexp.escape(options[:separator])
+ number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
+ else
+ number
+ end
+ end
+
+ def absolute_number(number)
+ number.respond_to?(:abs) ? number.abs : number.to_d.abs
+ end
+
+ def zero?
+ number.respond_to?(:zero?) ? number.zero? : number.to_d.zero?
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index e55ffd12c3..dea84e437f 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -12,7 +12,7 @@ module ActiveSupport
private
def method_missing(method, *arguments, &block)
- if arguments.last.is_a?(Proc)
+ if arguments.first.is_a?(Proc)
proc = arguments.pop
arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
else
diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb
index 1a3693f766..4680d5acb7 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -28,6 +28,14 @@ module ActiveSupport
coder.represent_seq '!omap', map { |k,v| { k => v } }
end
+ def select(*args, &block)
+ dup.tap { |hash| hash.select!(*args, &block) }
+ end
+
+ def reject(*args, &block)
+ dup.tap { |hash| hash.reject!(*args, &block) }
+ end
+
def nested_under_indifferent_access
self
end
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index e03bb4ca0f..a33e2c58a9 100644
--- a/activesupport/lib/active_support/ordered_options.rb
+++ b/activesupport/lib/active_support/ordered_options.rb
@@ -41,7 +41,7 @@ module ActiveSupport
end
# +InheritableOptions+ provides a constructor to build an +OrderedOptions+
- # hash inherited from the another hash.
+ # hash inherited from another hash.
#
# Use this if you already have some hash and you want to create a new one based on it.
#
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
index aa682fb36c..ca2e4d5625 100644
--- a/activesupport/lib/active_support/per_thread_registry.rb
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -32,21 +32,22 @@ module ActiveSupport
#
# If the class has an initializer, it must accept no arguments.
module PerThreadRegistry
- protected
+ def self.extended(object)
+ object.instance_variable_set '@per_thread_registry_key', object.name.freeze
+ end
+
+ def instance
+ Thread.current[@per_thread_registry_key] ||= new
+ end
+ protected
def method_missing(name, *args, &block) # :nodoc:
# Caches the method definition as a singleton method of the receiver.
define_singleton_method(name) do |*a, &b|
- per_thread_registry_instance.public_send(name, *a, &b)
+ instance.public_send(name, *a, &b)
end
send(name, *args, &block)
end
-
- private
-
- def per_thread_registry_instance
- Thread.current[name] ||= new
- end
end
end
diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb
index 133aa6a054..6eba24b569 100644
--- a/activesupport/lib/active_support/railtie.rb
+++ b/activesupport/lib/active_support/railtie.rb
@@ -13,6 +13,13 @@ module ActiveSupport
end
end
+ initializer "active_support.halt_callback_chains_on_return_false", after: :load_config_initializers do |app|
+ if app.config.active_support.key? :halt_callback_chains_on_return_false
+ ActiveSupport::Callbacks::CallbackChain.halt_and_display_warning_on_return_false = \
+ app.config.active_support.halt_callback_chains_on_return_false
+ end
+ end
+
# Sets the default value for Time.zone
# If assigned value cannot be matched to a TimeZone, an exception will be raised.
initializer "active_support.initialize_time_zone" do |app|
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index a7eba91ac5..1a02acd5b1 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -60,7 +60,7 @@ module ActiveSupport
end
klasses.each do |klass|
- key = if klass.is_a?(Class) && klass <= Exception
+ key = if klass.is_a?(Module) && klass.respond_to?(:===)
klass.name
elsif klass.is_a?(String)
klass
@@ -101,7 +101,7 @@ module ActiveSupport
# itself when rescue_from CONSTANT is executed.
klass = self.class.const_get(klass_name) rescue nil
klass ||= klass_name.constantize rescue nil
- exception.is_a?(klass) if klass
+ klass === exception if klass
end
case rescuer
diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb
new file mode 100644
index 0000000000..64c4801179
--- /dev/null
+++ b/activesupport/lib/active_support/security_utils.rb
@@ -0,0 +1,20 @@
+module ActiveSupport
+ module SecurityUtils
+ # Constant time string comparison.
+ #
+ # The values compared should be of fixed length, such as strings
+ # that have already been processed by HMAC. This should not be used
+ # on variable length plaintext strings because it could leak length info
+ # via timing attacks.
+ def secure_compare(a, b)
+ return false unless a.bytesize == b.bytesize
+
+ l = a.unpack "C#{a.bytesize}"
+
+ res = 0
+ b.each_byte { |byte| res |= byte ^ l.shift }
+ res == 0
+ end
+ module_function :secure_compare
+ end
+end
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
index 34c6f900c1..98be78b41b 100644
--- a/activesupport/lib/active_support/subscriber.rb
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -31,22 +31,54 @@ module ActiveSupport
# Attach the subscriber to a namespace.
def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications)
+ @namespace = namespace
+ @subscriber = subscriber
+ @notifier = notifier
+
subscribers << subscriber
+ # Add event subscribers for all existing methods on the class.
subscriber.public_methods(false).each do |event|
- next if %w{ start finish }.include?(event.to_s)
+ add_event_subscriber(event)
+ end
+ end
- notifier.subscribe("#{event}.#{namespace}", subscriber)
+ # Adds event subscribers for all new methods added to the class.
+ def method_added(event)
+ # Only public methods are added as subscribers, and only if a notifier
+ # has been set up. This means that subscribers will only be set up for
+ # classes that call #attach_to.
+ if public_method_defined?(event) && notifier
+ add_event_subscriber(event)
end
end
def subscribers
@@subscribers ||= []
end
+
+ protected
+
+ attr_reader :subscriber, :notifier, :namespace
+
+ def add_event_subscriber(event)
+ return if %w{ start finish }.include?(event.to_s)
+
+ pattern = "#{event}.#{namespace}"
+
+ # don't add multiple subscribers (eg. if methods are redefined)
+ return if subscriber.patterns.include?(pattern)
+
+ subscriber.patterns << pattern
+ notifier.subscribe(pattern, subscriber)
+ end
end
+ attr_reader :patterns # :nodoc:
+
def initialize
@queue_key = [self.class.name, object_id].join "-"
+ @patterns = []
super
end
@@ -71,7 +103,7 @@ module ActiveSupport
private
def event_stack
- SubscriberQueueRegistry.get_queue(@queue_key)
+ SubscriberQueueRegistry.instance.get_queue(@queue_key)
end
end
diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb
index 18bc919734..9086a959aa 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/blank'
require 'logger'
require 'active_support/logger'
@@ -42,7 +43,9 @@ module ActiveSupport
end
def current_tags
- Thread.current[:activesupport_tagged_logging_tags] ||= []
+ # We use our object ID here to void conflicting with other instances
+ thread_key = @thread_key ||= "activesupport_tagged_logging_tags:#{object_id}".freeze
+ Thread.current[thread_key] ||= []
end
private
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index 2f27aff2bd..739823bd56 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -7,52 +7,56 @@ require 'active_support/testing/deprecation'
require 'active_support/testing/declarative'
require 'active_support/testing/isolation'
require 'active_support/testing/constant_lookup'
+require 'active_support/testing/time_helpers'
+require 'active_support/testing/file_fixtures'
require 'active_support/core_ext/kernel/reporting'
-require 'active_support/deprecation'
-
-begin
- silence_warnings { require 'mocha/setup' }
-rescue LoadError
-end
-
-module Minitest # :nodoc:
- class << self
- remove_method :__run
- end
-
- def self.__run reporter, options # :nodoc:
- # FIXME: MT5's runnables is not ordered. This is needed because
- # we have have tests have cross-class order-dependent bugs.
- suites = Runnable.runnables.sort_by { |ts| ts.name.to_s }
-
- parallel, serial = suites.partition { |s| s.test_order == :parallel }
-
- ParallelEach.new(parallel).map { |suite| suite.run reporter, options } +
- serial.map { |suite| suite.run reporter, options }
- end
-end
module ActiveSupport
class TestCase < ::Minitest::Test
Assertion = Minitest::Assertion
- alias_method :method_name, :name
+ class << self
+ # Sets the order in which test cases are run.
+ #
+ # ActiveSupport::TestCase.test_order = :random # => :random
+ #
+ # Valid values are:
+ # * +:random+ (to run tests in random order)
+ # * +:parallel+ (to run tests in parallel)
+ # * +:sorted+ (to run tests alphabetically by method name)
+ # * +:alpha+ (equivalent to +:sorted+)
+ def test_order=(new_order)
+ ActiveSupport.test_order = new_order
+ end
- $tags = {}
- def self.for_tag(tag)
- yield if $tags[tag]
- end
+ # Returns the order in which test cases are run.
+ #
+ # ActiveSupport::TestCase.test_order # => :random
+ #
+ # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
+ # Defaults to +:random+.
+ def test_order
+ test_order = ActiveSupport.test_order
+
+ if test_order.nil?
+ test_order = :random
+ self.test_order = test_order
+ end
- # FIXME: we have tests that depend on run order, we should fix that and
- # remove this method.
- def self.test_order # :nodoc:
- :sorted
+ test_order
+ end
+
+ alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent!
end
+ alias_method :method_name, :name
+
include ActiveSupport::Testing::TaggedLogging
include ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
+ include ActiveSupport::Testing::TimeHelpers
+ include ActiveSupport::Testing::FileFixtures
extend ActiveSupport::Testing::Declarative
# test/unit backwards compatibility methods
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index 76a591bc3b..8b649c193f 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -9,7 +9,7 @@ module ActiveSupport
#
# assert_not nil # => true
# assert_not false # => true
- # assert_not 'foo' # => 'foo' is not nil or false
+ # assert_not 'foo' # => Expected "foo" to be nil or false
#
# An error message can be specified.
#
@@ -66,7 +66,7 @@ module ActiveSupport
exps = expressions.map { |e|
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
}
- before = exps.map { |e| e.call }
+ before = exps.map(&:call)
yield
diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb
index 1b2a75c35d..07d477c0db 100644
--- a/activesupport/lib/active_support/testing/constant_lookup.rb
+++ b/activesupport/lib/active_support/testing/constant_lookup.rb
@@ -36,12 +36,8 @@ module ActiveSupport
while names.size > 0 do
names.last.sub!(/Test$/, "")
begin
- constant = names.join("::").constantize
+ constant = names.join("::").safe_constantize
break(constant) if yield(constant)
- rescue NoMethodError # subclass of NameError
- raise
- rescue NameError
- # Constant wasn't found, move on
ensure
names.pop
end
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
index 1fa73caefa..0bf3643a56 100644
--- a/activesupport/lib/active_support/testing/declarative.rb
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -1,23 +1,6 @@
module ActiveSupport
module Testing
module Declarative
-
- def self.extended(klass) #:nodoc:
- klass.class_eval do
-
- unless method_defined?(:describe)
- def self.describe(text)
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def self.name
- "#{text}"
- end
- RUBY_EVAL
- end
- end
-
- end
- end
-
unless defined?(Spec)
# Helper to define a test method using a String. Under the hood, it replaces
# spaces with underscores and defines the test method.
@@ -27,7 +10,7 @@ module ActiveSupport
# end
def test(name, &block)
test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
- defined = instance_method(test_name) rescue false
+ defined = method_defined? test_name
raise "#{test_name} is already defined in #{self}" if defined
if block_given?
define_method(test_name, &block)
diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb
index a8342904dc..6c94c611b6 100644
--- a/activesupport/lib/active_support/testing/deprecation.rb
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -19,18 +19,17 @@ module ActiveSupport
result
end
- private
- def collect_deprecations
- old_behavior = ActiveSupport::Deprecation.behavior
- deprecations = []
- ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
- deprecations << message
- end
- result = yield
- [result, deprecations]
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
+ def collect_deprecations
+ old_behavior = ActiveSupport::Deprecation.behavior
+ deprecations = []
+ ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
+ deprecations << message
end
+ result = yield
+ [result, deprecations]
+ ensure
+ ActiveSupport::Deprecation.behavior = old_behavior
+ end
end
end
end
diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb
new file mode 100644
index 0000000000..4c6a0801b8
--- /dev/null
+++ b/activesupport/lib/active_support/testing/file_fixtures.rb
@@ -0,0 +1,34 @@
+module ActiveSupport
+ module Testing
+ # Adds simple access to sample files called file fixtures.
+ # File fixtures are normal files stored in
+ # <tt>ActiveSupport::TestCase.file_fixture_path</tt>.
+ #
+ # File fixtures are represented as +Pathname+ objects.
+ # This makes it easy to extract specific information:
+ #
+ # file_fixture("example.txt").read # get the file's content
+ # file_fixture("example.mp3").size # get the file size
+ module FileFixtures
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :file_fixture_path, instance_writer: false
+ end
+
+ # Returns a +Pathname+ to the fixture file named +fixture_name+.
+ #
+ # Raises ArgumentError if +fixture_name+ can't be found.
+ def file_fixture(fixture_name)
+ path = Pathname.new(File.join(file_fixture_path, fixture_name))
+
+ if path.exist?
+ path
+ else
+ msg = "the directory '%s' does not contain a file named '%s'"
+ raise ArgumentError, msg % [file_fixture_path, fixture_name]
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index d5d31cecbe..247df7423b 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -1,21 +1,16 @@
-require 'rbconfig'
-require 'minitest/parallel_each'
-
module ActiveSupport
module Testing
module Isolation
require 'thread'
def self.included(klass) #:nodoc:
- klass.extend(Module.new {
- def test_methods
- ParallelEach.new super
- end
- })
+ klass.class_eval do
+ parallelize_me!
+ end
end
def self.forking_env?
- !ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/))
+ !ENV["NO_FORK"] && Process.respond_to?(:fork)
end
@@class_setup_mutex = Mutex.new
@@ -40,6 +35,8 @@ module ActiveSupport
module Forking
def run_in_isolation(&blk)
read, write = IO.pipe
+ read.binmode
+ write.binmode
pid = fork do
read.close
@@ -71,14 +68,24 @@ module ActiveSupport
exit!
else
Tempfile.open("isolation") do |tmpfile|
- ENV["ISOLATION_TEST"] = self.class.name
- ENV["ISOLATION_OUTPUT"] = tmpfile.path
+ env = {
+ ISOLATION_TEST: self.class.name,
+ ISOLATION_OUTPUT: tmpfile.path
+ }
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}`
-
- ENV.delete("ISOLATION_TEST")
- ENV.delete("ISOLATION_OUTPUT")
+ orig_args = ORIG_ARGV.join(" ")
+ test_opts = "-n#{self.class.name}##{self.name}"
+ command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}"
+
+ # IO.popen lets us pass env in a cross-platform way
+ child = IO.popen([env, command])
+
+ begin
+ Process.wait(child.pid)
+ rescue Errno::ECHILD # The child process may exit before we wait
+ nil
+ end
return tmpfile.read.unpack("m")[0]
end
diff --git a/activesupport/lib/active_support/testing/stream.rb b/activesupport/lib/active_support/testing/stream.rb
new file mode 100644
index 0000000000..895192ad05
--- /dev/null
+++ b/activesupport/lib/active_support/testing/stream.rb
@@ -0,0 +1,42 @@
+module ActiveSupport
+ module Testing
+ module Stream #:nodoc:
+ private
+
+ def silence_stream(stream)
+ old_stream = stream.dup
+ stream.reopen(IO::NULL)
+ stream.sync = true
+ yield
+ ensure
+ stream.reopen(old_stream)
+ old_stream.close
+ end
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb
index f4cee64091..843ce4a867 100644
--- a/activesupport/lib/active_support/testing/tagged_logging.rb
+++ b/activesupport/lib/active_support/testing/tagged_logging.rb
@@ -6,7 +6,7 @@ module ActiveSupport
attr_writer :tagged_logger
def before_setup
- if tagged_logger
+ if tagged_logger && tagged_logger.info?
heading = "#{self.class}: #{name}"
divider = '-' * heading.size
tagged_logger.info divider
diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
new file mode 100644
index 0000000000..df5186ddec
--- /dev/null
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -0,0 +1,132 @@
+module ActiveSupport
+ module Testing
+ class SimpleStubs # :nodoc:
+ Stub = Struct.new(:object, :method_name, :original_method)
+
+ def initialize
+ @stubs = {}
+ end
+
+ def stub_object(object, method_name, return_value)
+ key = [object.object_id, method_name]
+
+ if stub = @stubs[key]
+ unstub_object(stub)
+ end
+
+ new_name = "__simple_stub__#{method_name}"
+
+ @stubs[key] = Stub.new(object, method_name, new_name)
+
+ object.singleton_class.send :alias_method, new_name, method_name
+ object.define_singleton_method(method_name) { return_value }
+ end
+
+ def unstub_all!
+ @stubs.each_value do |stub|
+ unstub_object(stub)
+ end
+ @stubs = {}
+ end
+
+ private
+
+ def unstub_object(stub)
+ singleton_class = stub.object.singleton_class
+ singleton_class.send :undef_method, stub.method_name
+ singleton_class.send :alias_method, stub.method_name, stub.original_method
+ singleton_class.send :undef_method, stub.original_method
+ end
+ end
+
+ # Containing helpers that helps you test passage of time.
+ module TimeHelpers
+ # Changes current time to the time in the future or in the past by a given time difference by
+ # stubbing +Time.now+ and +Date.today+.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day
+ # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # Date.current # => Sun, 10 Nov 2013
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day do
+ # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel(duration, &block)
+ travel_to Time.now + duration, &block
+ end
+
+ # Changes current time to the given time by stubbing +Time.now+ and
+ # +Date.today+ to return the time or date passed into this method.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # Date.current # => Wed, 24 Nov 2004
+ #
+ # Dates are taken as their timestamp at the beginning of the day in the
+ # application time zone. <tt>Time.current</tt> returns said timestamp,
+ # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
+ # <tt>Date.current</tt> returns a date equal to the argument, and
+ # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
+ # be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
+ # or <tt>Date.today</tt>, in order to honor the application time zone
+ # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
+ #
+ # Note that the usec for the time passed will be set to 0 to prevent rounding
+ # errors with external services, like MySQL (which will round instead of floor,
+ # leading to off-by-one-second errors).
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44) do
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_to(date_or_time)
+ if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
+ now = date_or_time.midnight.to_time
+ else
+ now = date_or_time.to_time.change(usec: 0)
+ end
+
+ simple_stubs.stub_object(Time, :now, now)
+ simple_stubs.stub_object(Date, :today, now.to_date)
+ simple_stubs.stub_object(DateTime, :now, now.to_datetime)
+
+ if block_given?
+ begin
+ yield
+ ensure
+ travel_back
+ end
+ end
+ end
+
+ # Returns the current time back to its original state, by removing the stubs added by
+ # `travel` and `travel_to`.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # travel_back
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_back
+ simple_stubs.unstub_all!
+ end
+
+ private
+
+ def simple_stubs
+ @simple_stubs ||= SimpleStubs.new
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb
index 92a593965e..ea2d3391bd 100644
--- a/activesupport/lib/active_support/time.rb
+++ b/activesupport/lib/active_support/time.rb
@@ -1,5 +1,3 @@
-require 'active_support'
-
module ActiveSupport
autoload :Duration, 'active_support/duration'
autoload :TimeWithZone, 'active_support/time_with_zone'
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index 95b9b8e5ae..8ddf233b3e 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -45,7 +45,7 @@ module ActiveSupport
def initialize(utc_time, time_zone, local_time = nil, period = nil)
@utc, @time_zone, @time = utc_time, time_zone, local_time
- @period = @utc ? period : get_period_and_ensure_valid_local_time
+ @period = @utc ? period : get_period_and_ensure_valid_local_time(period)
end
# Returns a Time or DateTime instance that represents the time in +time_zone+.
@@ -75,8 +75,8 @@ module ActiveSupport
# Returns a <tt>Time.local()</tt> instance of the simultaneous time in your
# system's <tt>ENV['TZ']</tt> zone.
- def localtime
- utc.respond_to?(:getlocal) ? utc.getlocal : utc.to_time.getlocal
+ def localtime(utc_offset = nil)
+ utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset)
end
alias_method :getlocal, :localtime
@@ -121,19 +121,28 @@ module ActiveSupport
utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
- # Time uses +zone+ to display the time zone abbreviation, so we're
- # duck-typing it.
+ # Returns the time zone abbreviation.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
+ # Time.zone.now.zone # => "EST"
def zone
period.zone_identifier.to_s
end
+ # Returns a string of the object's date, time, zone and offset from UTC.
+ #
+ # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00"
def inspect
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
end
+ # Returns a string of the object's date and time in the ISO 8601 standard
+ # format.
+ #
+ # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
def xmlschema(fraction_digits = 0)
- fraction = if fraction_digits > 0
- (".%06i" % time.usec)[0, fraction_digits + 1]
+ fraction = if fraction_digits.to_i > 0
+ (".%06i" % time.usec)[0, fraction_digits.to_i + 1]
end
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
@@ -146,15 +155,15 @@ module ActiveSupport
# to +false+.
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
- # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
- # # => "2005-02-01T15:15:10Z"
+ # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
+ # # => "2005-02-01T05:15:10.000-10:00"
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
- # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
- # # => "2005/02/01 15:15:10 +0000"
+ # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
+ # # => "2005/02/01 05:15:10 -1000"
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
- xmlschema(3)
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
@@ -185,28 +194,27 @@ module ActiveSupport
end
alias_method :rfc822, :rfc2822
- # <tt>:db</tt> format outputs time in UTC; all others output time in local.
- # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly.
+ # Returns a string of the object's date and time.
+ # Accepts an optional <tt>format</tt>:
+ # * <tt>:default</tt> - default value, mimics Ruby Time#to_s format.
+ # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db).
+ # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb.
def to_s(format = :default)
if format == :db
utc.to_s(format)
elsif formatter = ::Time::DATE_FORMATS[format]
formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
else
- "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby 1.9 Time#to_s format
+ "#{time.strftime("%Y-%m-%d %H:%M:%S")} #{formatted_offset(false, 'UTC')}" # mimicking Ruby Time#to_s format
end
end
alias_method :to_formatted_s, :to_s
- # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and
- # +formatted_offset+, respectively, before passing to Time#strftime, so
- # that zone information is correct
+ # Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime,
+ # so that zone information is correct.
def strftime(format)
- format = format.gsub('%Z', zone)
- .gsub('%z', formatted_offset(false))
- .gsub('%:z', formatted_offset(true))
- .gsub('%::z', formatted_offset(true) + ":00")
- time.strftime(format)
+ format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}")
+ getlocal(utc_offset).strftime(format)
end
# Use the time in UTC for comparisons.
@@ -244,9 +252,23 @@ module ActiveSupport
utc.hash
end
+ # Adds an interval of time to the current object's time and return that
+ # value as a new TimeWithZone object.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
+ # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EDT -04:00
+ # now + 1000 # => Sun, 02 Nov 2014 01:43:08 EDT -04:00
+ #
+ # If we're adding a Duration of variable length (i.e., years, months, days),
+ # move forward from #time, otherwise move forward from #utc, for accuracy
+ # when moving across DST boundaries.
+ #
+ # For instance, a time + 24.hours will advance exactly 24 hours, while a
+ # time + 1.day will advance 23-25 hours, depending on the day.
+ #
+ # now + 24.hours # => Mon, 03 Nov 2014 00:26:28 EST -05:00
+ # now + 1.day # => Mon, 03 Nov 2014 01:26:28 EST -05:00
def +(other)
- # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
- # otherwise move forward from #utc, for accuracy when moving across DST boundaries
if duration_of_variable_length?(other)
method_missing(:+, other)
else
@@ -254,12 +276,27 @@ module ActiveSupport
result.in_time_zone(time_zone)
end
end
+ alias_method :since, :+
+ # Returns a new TimeWithZone object that represents the difference between
+ # the current object's time and the +other+ time.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
+ # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EST -05:00
+ # now - 1000 # => Sun, 02 Nov 2014 01:09:48 EST -05:00
+ #
+ # If subtracting a Duration of variable length (i.e., years, months, days),
+ # move backward from #time, otherwise move backward from #utc, for accuracy
+ # when moving across DST boundaries.
+ #
+ # For instance, a time - 24.hours will go subtract exactly 24 hours, while a
+ # time - 1.day will subtract 23-25 hours, depending on the day.
+ #
+ # now - 24.hours # => Sat, 01 Nov 2014 02:26:28 EDT -04:00
+ # now - 1.day # => Sat, 01 Nov 2014 01:26:28 EDT -04:00
def -(other)
- # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time,
- # otherwise move backwards #utc, for accuracy when moving across DST boundaries
if other.acts_like?(:time)
- utc.to_f - other.to_f
+ to_time - other.to_time
elsif duration_of_variable_length?(other)
method_missing(:-, other)
else
@@ -268,16 +305,6 @@ module ActiveSupport
end
end
- def since(other)
- # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time,
- # otherwise move forward from #utc, for accuracy when moving across DST boundaries
- if duration_of_variable_length?(other)
- method_missing(:since, other)
- else
- utc.since(other).in_time_zone(time_zone)
- end
- end
-
def ago(other)
since(-other)
end
@@ -304,15 +331,27 @@ module ActiveSupport
[time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
end
+ # Returns the object's date and time as a floating point number of seconds
+ # since the Epoch (January 1, 1970 00:00 UTC).
+ #
+ # Time.zone.now.to_f # => 1417709320.285418
def to_f
utc.to_f
end
+ # Returns the object's date and time as an integer number of seconds
+ # since the Epoch (January 1, 1970 00:00 UTC).
+ #
+ # Time.zone.now.to_i # => 1417709320
def to_i
utc.to_i
end
alias_method :tv_sec, :to_i
+ # Returns the object's date and time as a rational number of seconds
+ # since the Epoch (January 1, 1970 00:00 UTC).
+ #
+ # Time.zone.now.to_r # => (708854548642709/500000)
def to_r
utc.to_r
end
@@ -350,6 +389,14 @@ module ActiveSupport
initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc)
end
+ # respond_to_missing? is not called in some cases, such as when type conversion is
+ # performed with Kernel#String
+ def respond_to?(sym, include_priv = false)
+ # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow
+ return false if sym.to_sym == :to_str
+ super
+ end
+
# Ensure proxy class responds to all methods that underlying time instance
# responds to.
def respond_to_missing?(sym, include_priv)
@@ -367,12 +414,12 @@ module ActiveSupport
end
private
- def get_period_and_ensure_valid_local_time
+ def get_period_and_ensure_valid_local_time(period)
# we don't want a Time.local instance enforcing its own DST rules as well,
# so transfer time values to a utc constructor if necessary
@time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
begin
- @time_zone.period_for_local(@time)
+ period || @time_zone.period_for_local(@time)
rescue ::TZInfo::PeriodNotFound
# time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
@time += 1.hour
@@ -390,7 +437,8 @@ module ActiveSupport
def wrap_with_time_zone(time)
if time.acts_like?(:time)
- self.class.new(nil, time_zone, time)
+ periods = time_zone.periods_for_local(time)
+ self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil)
elsif time.is_a?(Range)
wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end)
else
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 3cf82a24b9..728b53849d 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,3 +1,5 @@
+require 'tzinfo'
+require 'thread_safe'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/try'
@@ -109,9 +111,11 @@ module ActiveSupport
"Jerusalem" => "Asia/Jerusalem",
"Harare" => "Africa/Harare",
"Pretoria" => "Africa/Johannesburg",
+ "Kaliningrad" => "Europe/Kaliningrad",
"Moscow" => "Europe/Moscow",
"St. Petersburg" => "Europe/Moscow",
- "Volgograd" => "Europe/Moscow",
+ "Volgograd" => "Europe/Volgograd",
+ "Samara" => "Europe/Samara",
"Kuwait" => "Asia/Kuwait",
"Riyadh" => "Asia/Riyadh",
"Nairobi" => "Africa/Nairobi",
@@ -168,6 +172,7 @@ module ActiveSupport
"Guam" => "Pacific/Guam",
"Port Moresby" => "Pacific/Port_Moresby",
"Magadan" => "Asia/Magadan",
+ "Srednekolymsk" => "Asia/Srednekolymsk",
"Solomon Is." => "Pacific/Guadalcanal",
"New Caledonia" => "Pacific/Noumea",
"Fiji" => "Pacific/Fiji",
@@ -182,18 +187,76 @@ module ActiveSupport
}
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
- UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
+ UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '')
- # Assumes self represents an offset from UTC in seconds (as returned from
- # Time#utc_offset) and turns this into an +HH:MM formatted string.
- #
- # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
- def self.seconds_to_utc_offset(seconds, colon = true)
- format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
- sign = (seconds < 0 ? '-' : '+')
- hours = seconds.abs / 3600
- minutes = (seconds.abs % 3600) / 60
- format % [sign, hours, minutes]
+ @lazy_zones_map = ThreadSafe::Cache.new
+
+ class << self
+ # Assumes self represents an offset from UTC in seconds (as returned from
+ # Time#utc_offset) and turns this into an +HH:MM formatted string.
+ #
+ # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
+ def seconds_to_utc_offset(seconds, colon = true)
+ format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
+ sign = (seconds < 0 ? '-' : '+')
+ hours = seconds.abs / 3600
+ minutes = (seconds.abs % 3600) / 60
+ format % [sign, hours, minutes]
+ end
+
+ def find_tzinfo(name)
+ TZInfo::Timezone.new(MAPPING[name] || name)
+ end
+
+ alias_method :create, :new
+
+ # Returns a TimeZone instance with the given name, or +nil+ if no
+ # such TimeZone instance exists. (This exists to support the use of
+ # this class with the +composed_of+ macro.)
+ def new(name)
+ self[name]
+ end
+
+ # Returns an array of all TimeZone objects. There are multiple
+ # TimeZone objects per time zone, in many cases, to make it easier
+ # for users to find their own time zone.
+ def all
+ @zones ||= zones_map.values.sort
+ end
+
+ def zones_map
+ @zones_map ||= begin
+ MAPPING.each_key {|place| self[place]} # load all the zones
+ @lazy_zones_map
+ end
+ end
+
+ # Locate a specific time zone object. If the argument is a string, it
+ # is interpreted to mean the name of the timezone to locate. If it is a
+ # numeric value it is either the hour offset, or the second offset, of the
+ # timezone to find. (The first one with that offset will be returned.)
+ # Returns +nil+ if no such time zone is known to the system.
+ def [](arg)
+ case arg
+ when String
+ begin
+ @lazy_zones_map[arg] ||= create(arg)
+ rescue TZInfo::InvalidTimezoneIdentifier
+ nil
+ end
+ when Numeric, ActiveSupport::Duration
+ arg *= 3600 if arg.abs <= 13
+ all.find { |z| z.utc_offset == arg.to_i }
+ else
+ raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
+ end
+ end
+
+ # A convenience method for returning a collection of TimeZone objects
+ # for time zones in the USA.
+ def us_zones
+ @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
+ end
end
include Comparable
@@ -205,8 +268,6 @@ module ActiveSupport
# (GMT). Seconds were chosen as the offset unit because that is the unit
# that Ruby uses to represent time zone offsets (see Time#utc_offset).
def initialize(name, utc_offset = nil, tzinfo = nil)
- self.class.send(:require_tzinfo)
-
@name = name
@utc_offset = utc_offset
@tzinfo = tzinfo || TimeZone.find_tzinfo(name)
@@ -218,8 +279,8 @@ module ActiveSupport
if @utc_offset
@utc_offset
else
- @current_period ||= tzinfo.try(:current_period)
- @current_period.try(:utc_offset)
+ @current_period ||= tzinfo.current_period if tzinfo
+ @current_period.utc_offset if @current_period
end
end
@@ -232,6 +293,7 @@ module ActiveSupport
# Compare this time zone to the parameter. The two are compared first on
# their offsets, and then by name.
def <=>(zone)
+ return unless zone.respond_to? :utc_offset
result = (utc_offset <=> zone.utc_offset)
result = (name <=> zone.name) if result == 0
result
@@ -279,14 +341,19 @@ module ActiveSupport
#
# Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
# Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
- def parse(str, now=now)
+ #
+ # However, if the date component is not provided, but any other upper
+ # components are supplied, then the day of the month defaults to 1:
+ #
+ # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
+ def parse(str, now=now())
parts = Date._parse(str, false)
return if parts.empty?
time = Time.new(
parts.fetch(:year, now.year),
parts.fetch(:mon, now.month),
- parts.fetch(:mday, now.day),
+ parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
@@ -314,6 +381,16 @@ module ActiveSupport
tzinfo.now.to_date
end
+ # Returns the next date in this time zone.
+ def tomorrow
+ today + 1
+ end
+
+ # Returns the previous date in this time zone.
+ def yesterday
+ today - 1
+ end
+
# Adjust the given time to the simultaneous time in the time zone
# represented by +self+. Returns a Time.utc() instance -- if you want an
# ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead.
@@ -339,91 +416,13 @@ module ActiveSupport
tzinfo.period_for_local(time, dst)
end
- def self.find_tzinfo(name)
- TZInfo::TimezoneProxy.new(MAPPING[name] || name)
- end
-
- class << self
- alias_method :create, :new
-
- # Return a TimeZone instance with the given name, or +nil+ if no
- # such TimeZone instance exists. (This exists to support the use of
- # this class with the +composed_of+ macro.)
- def new(name)
- self[name]
- end
-
- # Return an array of all TimeZone objects. There are multiple
- # TimeZone objects per time zone, in many cases, to make it easier
- # for users to find their own time zone.
- def all
- @zones ||= zones_map.values.sort
- end
-
- def zones_map
- @zones_map ||= begin
- new_zones_names = MAPPING.keys - lazy_zones_map.keys
- new_zones = Hash[new_zones_names.map { |place| [place, create(place)] }]
-
- lazy_zones_map.merge(new_zones)
- end
- end
-
- # Locate a specific time zone object. If the argument is a string, it
- # is interpreted to mean the name of the timezone to locate. If it is a
- # numeric value it is either the hour offset, or the second offset, of the
- # timezone to find. (The first one with that offset will be returned.)
- # Returns +nil+ if no such time zone is known to the system.
- def [](arg)
- case arg
- when String
- begin
- lazy_zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset }
- rescue TZInfo::InvalidTimezoneIdentifier
- nil
- end
- when Numeric, ActiveSupport::Duration
- arg *= 3600 if arg.abs <= 13
- all.find { |z| z.utc_offset == arg.to_i }
- else
- raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
- end
- end
-
- # A convenience method for returning a collection of TimeZone objects
- # for time zones in the USA.
- def us_zones
- @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
- end
-
- protected
-
- def require_tzinfo
- require 'tzinfo' unless defined?(::TZInfo)
- rescue LoadError
- $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install"
- raise
- end
-
- private
-
- def lookup(name)
- (tzinfo = find_tzinfo(name)) && create(tzinfo.name.freeze)
- end
-
- def lazy_zones_map
- require_tzinfo
-
- @lazy_zones_map ||= Hash.new do |hash, place|
- hash[place] = create(place) if MAPPING.has_key?(place)
- end
- end
+ def periods_for_local(time) #:nodoc:
+ tzinfo.periods_for_local(time)
end
private
-
- def time_now
- Time.now
- end
+ def time_now
+ Time.now
+ end
end
end
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
index 2571faa019..760be4c07a 100644
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ b/activesupport/lib/active_support/values/unicode_tables.dat
Binary files differ
diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb
index 8762330a6e..fe03984546 100644
--- a/activesupport/lib/active_support/version.rb
+++ b/activesupport/lib/active_support/version.rb
@@ -1,11 +1,8 @@
+require_relative 'gem_version'
+
module ActiveSupport
- # Returns the version of the currently loaded ActiveSupport as a Gem::Version
+ # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
def self.version
- Gem::Version.new "4.1.0.beta"
- end
-
- module VERSION #:nodoc:
- MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments
- STRING = ActiveSupport.version.to_s
+ gem_version
end
end
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index d082a0a499..009ee4db90 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -1,7 +1,9 @@
require 'time'
require 'base64'
+require 'bigdecimal'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/date_time/calculations'
module ActiveSupport
# = XmlMini
@@ -56,13 +58,13 @@ module ActiveSupport
# TODO use regexp instead of Date.parse
unless defined?(PARSING)
PARSING = {
- "symbol" => Proc.new { |symbol| symbol.to_sym },
+ "symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
"date" => Proc.new { |date| ::Date.parse(date) },
"datetime" => Proc.new { |time| Time.xmlschema(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) },
+ "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
"string" => Proc.new { |string| string.to_s },
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index 27c64c4dca..f303daa1a7 100644
--- a/activesupport/lib/active_support/xml_mini/jdom.rb
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -141,7 +141,7 @@ module ActiveSupport
(0...attributes.length).each do |i|
attribute_hash[CONTENT_KEY] ||= ''
attribute_hash[attributes.item(i).name] = attributes.item(i).value
- end
+ end
attribute_hash
end
diff --git a/activesupport/lib/active_support/xml_mini/libxml.rb b/activesupport/lib/active_support/xml_mini/libxml.rb
index 47a2824186..bb0ea9c582 100644
--- a/activesupport/lib/active_support/xml_mini/libxml.rb
+++ b/activesupport/lib/active_support/xml_mini/libxml.rb
@@ -75,5 +75,5 @@ module LibXML #:nodoc:
end
end
-LibXML::XML::Document.send(:include, LibXML::Conversions::Document)
-LibXML::XML::Node.send(:include, LibXML::Conversions::Node)
+LibXML::XML::Document.include(LibXML::Conversions::Document)
+LibXML::XML::Node.include(LibXML::Conversions::Node)
diff --git a/activesupport/lib/active_support/xml_mini/nokogiri.rb b/activesupport/lib/active_support/xml_mini/nokogiri.rb
index 7398d4fa82..619cc7522d 100644
--- a/activesupport/lib/active_support/xml_mini/nokogiri.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogiri.rb
@@ -77,7 +77,7 @@ module ActiveSupport
end
end
- Nokogiri::XML::Document.send(:include, Conversions::Document)
- Nokogiri::XML::Node.send(:include, Conversions::Node)
+ Nokogiri::XML::Document.include(Conversions::Document)
+ Nokogiri::XML::Node.include(Conversions::Node)
end
end