diff options
Diffstat (limited to 'activesupport/lib')
116 files changed, 1786 insertions, 754 deletions
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 588d6c49f9..94fe893149 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2015 David Heinemeier Hansson +# Copyright (c) 2005-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -34,6 +34,7 @@ module ActiveSupport autoload :Dependencies autoload :DescendantsTracker autoload :FileUpdateChecker + autoload :EventedFileUpdateChecker autoload :LogSubscriber autoload :Notifications @@ -75,11 +76,11 @@ module ActiveSupport cattr_accessor :test_order # :nodoc: def self.halt_callback_chains_on_return_false - Callbacks::CallbackChain.halt_and_display_warning_on_return_false + Callbacks.halt_and_display_warning_on_return_false end def self.halt_callback_chains_on_return_false=(value) - Callbacks::CallbackChain.halt_and_display_warning_on_return_false = value + Callbacks.halt_and_display_warning_on_return_false = value end end diff --git a/activesupport/lib/active_support/array_inquirer.rb b/activesupport/lib/active_support/array_inquirer.rb index e7188d7adb..f59ddf5403 100644 --- a/activesupport/lib/active_support/array_inquirer.rb +++ b/activesupport/lib/active_support/array_inquirer.rb @@ -23,7 +23,7 @@ module ActiveSupport super else candidates.any? do |candidate| - include?(candidate) || include?(candidate.to_sym) + include?(candidate.to_sym) || include?(candidate.to_s) end end end diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index d06f22ad5c..e161ec4cca 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -25,7 +25,7 @@ module ActiveSupport # 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. + # Inspired by the Quiet Backtrace gem by thoughtbot. class BacktraceCleaner def initialize @filters, @silencers = [], [] diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 837974bc85..610105f41c 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/string/strip' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. @@ -26,7 +27,7 @@ module ActiveSupport end class << self - # Creates a new CacheStore object according to the given options. + # Creates a new Store object according to the given options. # # If no arguments are passed to this method, then a new # ActiveSupport::Cache::MemoryStore object will be returned. @@ -254,10 +255,11 @@ module ActiveSupport # end # end # - # # val_1 => "new value 1" - # # val_2 => "original value" - # # sleep 10 # First thread extend the life of cache by another 10 seconds - # # cache.fetch('foo') => "new value 1" + # cache.fetch('foo') # => "original value" + # sleep 10 # First thread extended the life of cache by another 10 seconds + # cache.fetch('foo') # => "new value 1" + # val_1 # => "new value 1" + # val_2 # => "original value" # # Other options will be handled by the specific cache store implementation. # Internally, #fetch calls #read_entry, and calls #write_entry on a cache @@ -275,10 +277,15 @@ module ActiveSupport def fetch(name, options = nil) if block_given? options = merged_options(options) - key = namespaced_key(name, options) + key = normalize_key(name, options) - cached_entry = find_cached_entry(key, name, options) unless options[:force] - entry = handle_expired_entry(cached_entry, key, options) + entry = nil + instrument(:read, name, options) do |payload| + cached_entry = read_entry(key, options) unless options[:force] + entry = handle_expired_entry(cached_entry, key, options) + payload[:super_operation] = :fetch if payload + payload[:hit] = !!entry if payload + end if entry get_entry_value(entry, name, options) @@ -297,7 +304,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def read(name, options = nil) options = merged_options(options) - key = namespaced_key(name, options) + key = normalize_key(name, options) instrument(:read, name, options) do |payload| entry = read_entry(key, options) if entry @@ -329,7 +336,7 @@ module ActiveSupport instrument_multi(:read, names, options) do |payload| results = {} names.each do |name| - key = namespaced_key(name, options) + key = normalize_key(name, options) entry = read_entry(key, options) if entry if entry.expired? @@ -381,7 +388,7 @@ module ActiveSupport instrument(:write, name, options) do entry = Entry.new(value, options) - write_entry(namespaced_key(name, options), entry, options) + write_entry(normalize_key(name, options), entry, options) end end @@ -392,7 +399,7 @@ module ActiveSupport options = merged_options(options) instrument(:delete, name) do - delete_entry(namespaced_key(name, options), options) + delete_entry(normalize_key(name, options), options) end end @@ -403,7 +410,7 @@ module ActiveSupport options = merged_options(options) instrument(:exist?, name) do - entry = read_entry(namespaced_key(name, options), options) + entry = read_entry(normalize_key(name, options), options) (entry && !entry.expired?) || false end end @@ -524,7 +531,7 @@ module ActiveSupport # Prefix a key with the namespace. Namespace and key will be delimited # with a colon. - def namespaced_key(key, options) + def normalize_key(key, options) key = expanded_key(key) namespace = options[:namespace] if options prefix = namespace.is_a?(Proc) ? namespace.call : namespace @@ -532,8 +539,16 @@ module ActiveSupport key end + def namespaced_key(*args) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `namespaced_key` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key. + MESSAGE + normalize_key(*args) + end + def instrument(operation, key, options = nil) - log { "Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}" } + log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" } payload = { :key => key } payload.merge!(options) if options.is_a?(Hash) @@ -556,13 +571,6 @@ module ActiveSupport logger.debug(yield) end - def find_cached_entry(key, name, options) - instrument(:read, name, options) do |payload| - payload[:super_operation] = :fetch if payload - read_entry(key, options) - end - end - def handle_expired_entry(entry, key, options) if entry && entry.expired? race_ttl = options[:race_condition_ttl].to_i diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index e6a8b84214..99c55b1aa4 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -10,24 +10,25 @@ module ActiveSupport # FileStore implements the Strategy::LocalCache strategy which implements # an in-memory cache inside of a block. class FileStore < Store + prepend Strategy::LocalCache attr_reader :cache_path 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 + GITKEEP_FILES = ['.gitkeep', '.keep'].freeze def initialize(cache_path, options = nil) super(options) @cache_path = cache_path.to_s - 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 + # file store directory except for .keep or .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)} + root_dirs = exclude_from(cache_path, EXCLUDED_DIRS + GITKEEP_FILES) FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) rescue Errno::ENOENT end @@ -60,7 +61,7 @@ module ActiveSupport matcher = key_matcher(matcher, options) search_dir(cache_path) do |path| key = file_path_key(path) - delete_entry(key, options) if key.match(matcher) + delete_entry(path, options) if key.match(matcher) end end end @@ -68,9 +69,8 @@ module ActiveSupport protected def read_entry(key, options) - file_name = key_file_path(key) - if File.exist?(file_name) - File.open(file_name) { |f| Marshal.load(f) } + if File.exist?(key) + File.open(key) { |f| Marshal.load(f) } end rescue => e logger.error("FileStoreError (#{e}): #{e.message}") if logger @@ -78,23 +78,21 @@ module ActiveSupport end 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)} + return false if options[:unless_exist] && File.exist?(key) + ensure_cache_path(File.dirname(key)) + File.atomic_write(key, cache_path) {|f| Marshal.dump(entry, f)} true end def delete_entry(key, options) - file_name = key_file_path(key) - if File.exist?(file_name) + if File.exist?(key) begin - File.delete(file_name) - delete_empty_directories(File.dirname(file_name)) + File.delete(key) + delete_empty_directories(File.dirname(key)) true rescue => e # Just in case the error was caused by another process deleting the file first. - raise e if File.exist?(file_name) + raise e if File.exist?(key) false end end @@ -118,12 +116,14 @@ module ActiveSupport end # Translate a key into a file path. - def key_file_path(key) - if key.size > FILEPATH_MAX_SIZE - key = Digest::MD5.hexdigest(key) + def normalize_key(key, options) + key = super + fname = URI.encode_www_form_component(key) + + if fname.size > FILEPATH_MAX_SIZE + fname = Digest::MD5.hexdigest(key) end - fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) @@ -138,6 +138,14 @@ module ActiveSupport File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) end + def key_file_path(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `key_file_path` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key or nothing. + MESSAGE + key + end + # Translate a file path into a key. def file_path_key(path) fname = path[cache_path.to_s.size..-1].split(File::SEPARATOR, 4).last @@ -147,7 +155,7 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) return if File.realpath(dir) == File.realpath(cache_path) - if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty? + if exclude_from(dir, EXCLUDED_DIRS).empty? Dir.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) end @@ -174,7 +182,7 @@ module ActiveSupport # 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)) + file_name = normalize_key(name, options) lock_file(file_name) do options = merged_options(options) @@ -186,6 +194,11 @@ module ActiveSupport end end end + + # Exclude entries from source directory + def exclude_from(source, excludes) + Dir.entries(source).reject { |f| excludes.include?(f) } + 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 47133bf550..174913365a 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -24,6 +24,31 @@ module ActiveSupport # MemCacheStore implements the Strategy::LocalCache strategy which implements # an in-memory cache inside of a block. class MemCacheStore < Store + # Provide support for raw values in the local cache strategy. + module LocalCacheWithRaw # :nodoc: + protected + def read_entry(key, options) + entry = super + if options[:raw] && local_cache && entry + entry = deserialize_entry(entry.value) + end + entry + end + + def write_entry(key, entry, options) # :nodoc: + if options[:raw] && local_cache + raw_entry = Entry.new(entry.value.to_s) + raw_entry.expires_at = entry.expires_at + super(key, raw_entry, options) + else + super + end + end + end + + prepend Strategy::LocalCache + prepend LocalCacheWithRaw + ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n # Creates a new Dalli::Client instance with specified addresses and options. @@ -63,9 +88,6 @@ module ActiveSupport UNIVERSAL_OPTIONS.each{|name| mem_cache_options.delete(name)} @data = self.class.build_mem_cache(*(addresses + [mem_cache_options])) end - - extend Strategy::LocalCache - extend LocalCacheWithRaw end # Reads multiple values from the cache using a single call to the @@ -75,7 +97,7 @@ module ActiveSupport options = merged_options(options) instrument_multi(:read, names, options) do - keys_to_names = Hash[names.map{|name| [escape_key(namespaced_key(name, options)), name]}] + keys_to_names = Hash[names.map{|name| [normalize_key(name, options), name]}] raw_values = @data.get_multi(keys_to_names.keys, :raw => true) values = {} raw_values.each do |key, value| @@ -93,11 +115,10 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) instrument(:increment, name, :amount => amount) do - @data.incr(escape_key(namespaced_key(name, options)), amount) + rescue_error_with nil do + @data.incr(normalize_key(name, options), amount) + end end - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil end # Decrement a cached value. This method uses the memcached decr atomic @@ -107,20 +128,16 @@ module ActiveSupport def decrement(name, amount = 1, options = nil) # :nodoc: options = merged_options(options) instrument(:decrement, name, :amount => amount) do - @data.decr(escape_key(namespaced_key(name, options)), amount) + rescue_error_with nil do + @data.decr(normalize_key(name, options), amount) + end end - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil end # Clear the entire cache on all memcached servers. This method should # be used with care when shared cache is being used. def clear(options = nil) - @data.flush_all - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil + rescue_error_with(nil) { @data.flush_all } end # Get the statistics from the memcached servers. @@ -131,10 +148,7 @@ module ActiveSupport protected # Read an entry from the cache. def read_entry(key, options) # :nodoc: - deserialize_entry(@data.get(escape_key(key), options)) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - nil + rescue_error_with(nil) { deserialize_entry(@data.get(key, options)) } end # Write an entry to the cache. @@ -146,18 +160,14 @@ module ActiveSupport # Set the memcache expire a few minutes in the future to support race condition ttls on read expires_in += 5.minutes end - @data.send(method, escape_key(key), value, expires_in, options) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - false + rescue_error_with false do + @data.send(method, key, value, expires_in, options) + end end # Delete an entry from the cache. def delete_entry(key, options) # :nodoc: - @data.delete(escape_key(key)) - rescue Dalli::DalliError => e - logger.error("DalliError (#{e}): #{e.message}") if logger - false + rescue_error_with(false) { @data.delete(key) } end private @@ -165,44 +175,35 @@ module ActiveSupport # Memcache keys are binaries. So we need to force their encoding to binary # before applying the regular expression to ensure we are escaping all # characters properly. - def escape_key(key) - key = key.to_s.dup + def normalize_key(key, options) + key = super.dup key = key.force_encoding(Encoding::ASCII_8BIT) key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" } key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250 key end + def escape_key(key) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `escape_key` is deprecated and will be removed from Rails 5.1. + Please use `normalize_key` which will return a fully resolved key or nothing. + MESSAGE + key + end + def deserialize_entry(raw_value) if raw_value entry = Marshal.load(raw_value) rescue raw_value entry.is_a?(Entry) ? entry : Entry.new(entry) - else - nil end end - # Provide support for raw values in the local cache strategy. - module LocalCacheWithRaw # :nodoc: - protected - def read_entry(key, options) - entry = super - if options[:raw] && local_cache && entry - entry = deserialize_entry(entry.value) - end - entry - end - - def write_entry(key, entry, options) # :nodoc: - retval = super - if options[:raw] && local_cache && retval - raw_entry = Entry.new(entry.value.to_s) - raw_entry.expires_at = entry.expires_at - local_cache.write_entry(key, raw_entry, options) - end - retval - end - end + def rescue_error_with(fallback) + yield + rescue Dalli::DalliError => e + logger.error("DalliError (#{e}): #{e.message}") if logger + fallback + end end end end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 90bb2c38c3..896c28ad8b 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -75,30 +75,12 @@ module ActiveSupport # Increment an integer value in the cache. def increment(name, amount = 1, options = nil) - synchronize do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end # Decrement an integer value in the cache. def decrement(name, amount = 1, options = nil) - synchronize do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -167,6 +149,19 @@ module ActiveSupport !!entry end end + + private + + def modify_value(name, amount, options) + synchronize 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/null_store.rb b/activesupport/lib/active_support/cache/null_store.rb index 4427eaafcd..0564ce5312 100644 --- a/activesupport/lib/active_support/cache/null_store.rb +++ b/activesupport/lib/active_support/cache/null_store.rb @@ -8,10 +8,7 @@ module ActiveSupport # be cached inside blocks that utilize this strategy. See # ActiveSupport::Cache::Strategy::LocalCache for more details. class NullStore < Store - def initialize(options = nil) - super(options) - extend Strategy::LocalCache - end + prepend Strategy::LocalCache def clear(options = nil) end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index fe5bc82c30..df38dbcf11 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -60,6 +60,10 @@ module ActiveSupport def delete_entry(key, options) !!@data.delete(key) end + + def fetch_entry(key, options = nil) # :nodoc: + @data.fetch(key) { @data[key] = yield } + end end # Use a local cache for the duration of block. @@ -75,36 +79,35 @@ module ActiveSupport end def clear(options = nil) # :nodoc: - local_cache.clear(options) if local_cache + return super unless cache = local_cache + cache.clear(options) super end def cleanup(options = nil) # :nodoc: - local_cache.clear(options) if local_cache + return super unless cache = local_cache + cache.clear(options) super end def increment(name, amount = 1, options = nil) # :nodoc: + return super unless local_cache value = bypass_local_cache{super} - set_cache_value(value, name, amount, options) + write_cache_value(name, value, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: + return super unless local_cache value = bypass_local_cache{super} - set_cache_value(value, name, amount, options) + write_cache_value(name, value, options) value end protected def read_entry(key, options) # :nodoc: - if local_cache - entry = local_cache.read_entry(key, options) - unless entry - entry = super - local_cache.write_entry(key, entry, options) - end - entry + if cache = local_cache + cache.fetch_entry(key) { super } else super end @@ -121,13 +124,21 @@ module ActiveSupport end def set_cache_value(value, name, amount, options) # :nodoc: - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) + `set_cache_value` is deprecated and will be removed from Rails 5.1. + Please use `write_cache_value` + MESSAGE + write_cache_value name, value, options + end + + def write_cache_value(name, value, options) # :nodoc: + name = normalize_key(name, options) + cache = local_cache + cache.mute do + if value + cache.write(name, value, options) + else + cache.delete(name, options) end end end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index e8ab3a7db5..bf560ec1fa 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -4,6 +4,7 @@ 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/module/attribute_accessors' require 'active_support/core_ext/string/filters' require 'active_support/deprecation' require 'thread' @@ -66,6 +67,12 @@ module ActiveSupport CALLBACK_FILTER_TYPES = [:before, :after, :around] + # If true, Active Record and Active Model callbacks 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+. + mattr_accessor(:halt_and_display_warning_on_return_false) { true } + # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -80,8 +87,12 @@ module ActiveSupport # save # end def run_callbacks(kind, &block) - callbacks = send("_#{kind}_callbacks") + send "_run_#{kind}_callbacks", &block + end + + private + def __run_callbacks__(callbacks, &block) if callbacks.empty? yield if block_given? else @@ -91,8 +102,6 @@ module ActiveSupport end end - private - # 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. @@ -124,14 +133,10 @@ module ActiveSupport 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? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) - elsif chain_config.key? :terminator - halting(callback_sequence, user_callback, halted_lambda, filter) - elsif user_conditions.any? - conditional(callback_sequence, user_callback, user_conditions) else - simple callback_sequence, user_callback + halting(callback_sequence, user_callback, halted_lambda, filter) end end @@ -173,42 +178,15 @@ module ActiveSupport end end private_class_method :halting - - 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 - - env - end - end - private_class_method :conditional - - def self.simple(callback_sequence, user_callback) - callback_sequence.before do |env| - user_callback.call env.target, env.value - - env - end - end - private_class_method :simple end class After 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? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions) - elsif chain_config.key?(:terminator) - halting(callback_sequence, user_callback) - elsif user_conditions.any? - conditional callback_sequence, user_callback, user_conditions else - simple callback_sequence, user_callback + halting(callback_sequence, user_callback) end else if user_conditions.any? @@ -271,14 +249,10 @@ module ActiveSupport class Around def self.build(callback_sequence, user_callback, user_conditions, chain_config) - if chain_config.key?(:terminator) && user_conditions.any? + if user_conditions.any? halting_and_conditional(callback_sequence, user_callback, user_conditions) - elsif chain_config.key? :terminator - halting(callback_sequence, user_callback) - elsif user_conditions.any? - conditional(callback_sequence, user_callback, user_conditions) else - simple(callback_sequence, user_callback) + halting(callback_sequence, user_callback) end end @@ -316,38 +290,18 @@ module ActiveSupport end end private_class_method :halting - - 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) { - run.call.value - } - env - else - run.call - end - end - end - private_class_method :conditional - - def self.simple(callback_sequence, user_callback) - callback_sequence.around do |env, &run| - user_callback.call(env.target, env.value) { - run.call.value - } - env - end - end - private_class_method :simple end end class Callback #:nodoc:# def self.build(chain, filter, kind, options) + if filter.is_a?(String) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing string to define callback is deprecated and will be removed + in Rails 5.1 without replacement. + MSG + end + new chain.name, filter, kind, options, chain.config end @@ -510,12 +464,6 @@ module ActiveSupport 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 = { @@ -596,23 +544,12 @@ module ActiveSupport 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 + result_lambda.call if result_lambda.is_a?(Proc) + terminate = false 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 @@ -645,7 +582,7 @@ module ActiveSupport # set_callback :save, :before_meth # # 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 + # proc, lambda, or block; as a string to be instance evaluated(deprecated); or as an # object that responds to a certain method determined by the <tt>:scope</tt> # argument to +define_callbacks+. # @@ -739,14 +676,15 @@ module ActiveSupport # 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. + # The current object and the result lambda of the callback will be provided + # to the terminator lambda. # - # define_callbacks :validate, terminator: ->(target, result) { result == false } + # define_callbacks :validate, terminator: ->(target, result_lambda) { result_lambda.call == false } # # In this example, if any before validate callbacks returns +false+, # any successive before and around callback is not executed. - # Defaults to +false+, meaning no value halts the chain. + # + # The default terminator halts the chain when a callback throws +:abort+. # # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By @@ -806,18 +744,48 @@ module ActiveSupport 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 protected - def get_callbacks(name) + def get_callbacks(name) # :nodoc: send "_#{name}_callbacks" end - def set_callbacks(name, callbacks) + def set_callbacks(name, callbacks) # :nodoc: send "_#{name}_callbacks=", callbacks end + + def deprecated_false_terminator # :nodoc: + Proc.new do |target, result_lambda| + terminate = true + catch(:abort) do + result = result_lambda.call if result_lambda.is_a?(Proc) + if Callbacks.halt_and_display_warning_on_return_false && result == false + display_deprecation_warning_for_false_terminator + else + terminate = false + end + end + terminate + end + end + + private + + def display_deprecation_warning_for_false_terminator + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Returning `false` in Active Record and Active Model callbacks will not implicitly halt a callback chain in the next release of Rails. + To explicitly halt the callback chain, please use `throw :abort` instead. + MSG + end end end end diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index 4082d2d464..0403eb70ca 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -132,7 +132,7 @@ module ActiveSupport end def class_methods(&class_methods_module_definition) - mod = const_defined?(:ClassMethods) ? + mod = const_defined?(:ClassMethods, false) ? const_get(:ClassMethods) : const_set(:ClassMethods, Module.new) diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb index 1507de433e..4abe5ece6f 100644 --- a/activesupport/lib/active_support/concurrency/latch.rb +++ b/activesupport/lib/active_support/concurrency/latch.rb @@ -1,26 +1,18 @@ -require 'thread' -require 'monitor' +require 'concurrent/atomic/count_down_latch' module ActiveSupport module Concurrency - class Latch + class Latch < Concurrent::CountDownLatch + def initialize(count = 1) - @count = count - @lock = Monitor.new - @cv = @lock.new_cond + ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.") + super(count) end - def release - @lock.synchronize do - @count -= 1 if @count > 0 - @cv.broadcast if @count.zero? - end - end + alias_method :release, :count_down def await - @lock.synchronize do - @cv.wait_while { @count > 0 } - end + wait(nil) end end end diff --git a/activesupport/lib/active_support/concurrency/share_lock.rb b/activesupport/lib/active_support/concurrency/share_lock.rb new file mode 100644 index 0000000000..ca48164c54 --- /dev/null +++ b/activesupport/lib/active_support/concurrency/share_lock.rb @@ -0,0 +1,142 @@ +require 'thread' +require 'monitor' + +module ActiveSupport + module Concurrency + # A share/exclusive lock, otherwise known as a read/write lock. + # + # https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock + #-- + # Note that a pending Exclusive lock attempt does not block incoming + # Share requests (i.e., we are "read-preferring"). That seems + # consistent with the behavior of "loose" upgrades, but may be the + # wrong choice otherwise: it nominally reduces the possibility of + # deadlock by risking starvation instead. + class ShareLock + include MonitorMixin + + # We track Thread objects, instead of just using counters, because + # we need exclusive locks to be reentrant, and we need to be able + # to upgrade share locks to exclusive. + + + def initialize + super() + + @cv = new_cond + + @sharing = Hash.new(0) + @waiting = {} + @exclusive_thread = nil + @exclusive_depth = 0 + end + + # Returns false if +no_wait+ is set and the lock is not + # immediately available. Otherwise, returns true after the lock + # has been acquired. + # + # +purpose+ and +compatible+ work together; while this thread is + # waiting for the exclusive lock, it will yield its share (if any) + # to any other attempt whose +purpose+ appears in this attempt's + # +compatible+ list. This allows a "loose" upgrade, which, being + # less strict, prevents some classes of deadlocks. + # + # For many resources, loose upgrades are sufficient: if a thread + # is awaiting a lock, it is not running any other code. With + # +purpose+ matching, it is possible to yield only to other + # threads whose activity will not interfere. + def start_exclusive(purpose: nil, compatible: [], no_wait: false) + synchronize do + unless @exclusive_thread == Thread.current + if busy?(purpose) + return false if no_wait + + loose_shares = @sharing.delete(Thread.current) + @waiting[Thread.current] = compatible if loose_shares + + begin + @cv.wait_while { busy?(purpose) } + ensure + @waiting.delete Thread.current + @sharing[Thread.current] = loose_shares if loose_shares + end + end + @exclusive_thread = Thread.current + end + @exclusive_depth += 1 + + true + end + end + + # Relinquish the exclusive lock. Must only be called by the thread + # that called start_exclusive (and currently holds the lock). + def stop_exclusive + synchronize do + raise "invalid unlock" if @exclusive_thread != Thread.current + + @exclusive_depth -= 1 + if @exclusive_depth == 0 + @exclusive_thread = nil + @cv.broadcast + end + end + end + + def start_sharing + synchronize do + if @exclusive_thread && @exclusive_thread != Thread.current + @cv.wait_while { @exclusive_thread } + end + @sharing[Thread.current] += 1 + end + end + + def stop_sharing + synchronize do + if @sharing[Thread.current] > 1 + @sharing[Thread.current] -= 1 + else + @sharing.delete Thread.current + @cv.broadcast + end + end + end + + # Execute the supplied block while holding the Exclusive lock. If + # +no_wait+ is set and the lock is not immediately available, + # returns +nil+ without yielding. Otherwise, returns the result of + # the block. + # + # See +start_exclusive+ for other options. + def exclusive(purpose: nil, compatible: [], no_wait: false) + if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait) + begin + yield + ensure + stop_exclusive + end + end + end + + # Execute the supplied block while holding the Share lock. + def sharing + start_sharing + begin + yield + ensure + stop_sharing + end + end + + private + + # Must be called within synchronize + def busy?(purpose) + (@exclusive_thread && @exclusive_thread != Thread.current) || + @waiting.any? { |k, v| k != Thread.current && !v.include?(purpose) } || + @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0) + end + end + end +end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index d80df21e7d..8718b7e1e5 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -32,7 +32,7 @@ class Array # ['one', 'two', 'three'].to_sentence # => "one, two, and three" # # ['one', 'two'].to_sentence(passing: 'invalid option') - # # => ArgumentError: Unknown key :passing + # # => ArgumentError: Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale # # ['one', 'two'].to_sentence(two_words_connector: '-') # # => "one-two" @@ -85,7 +85,9 @@ class Array # Extends <tt>Array#to_s</tt> to convert a collection of elements into a # comma separated id list if <tt>:db</tt> argument is given as the format. # - # Blog.all.to_formatted_s(:db) # => "1,2,3" + # Blog.all.to_formatted_s(:db) # => "1,2,3" + # Blog.none.to_formatted_s(:db) # => "null" + # [1,2].to_formatted_s # => "[1, 2]" def to_formatted_s(format = :default) case format when :db 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 234283e792..22fc7ecf92 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,15 +1,14 @@ require 'bigdecimal' require 'bigdecimal/util' -class BigDecimal - DEFAULT_STRING_FORMAT = 'F' - alias_method :to_default_s, :to_s +module ActiveSupport + module BigDecimalWithDefaultFormat #:nodoc: + DEFAULT_STRING_FORMAT = 'F' - def to_s(format = nil, options = nil) - if format.is_a?(Symbol) - to_formatted_s(format, options || {}) - else - to_default_s(format || DEFAULT_STRING_FORMAT) + def to_s(format = nil) + super(format || DEFAULT_STRING_FORMAT) end end end + +BigDecimal.prepend(ActiveSupport::BigDecimalWithDefaultFormat) diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index c750a10bb2..ef903d59b5 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,3 +1,2 @@ require 'active_support/core_ext/class/attribute' -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 f2b7bb3ef1..802d988af2 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -75,11 +75,15 @@ class Class instance_predicate = options.fetch(:instance_predicate, true) attrs.each do |name| + remove_possible_singleton_method(name) define_singleton_method(name) { nil } + + remove_possible_singleton_method("#{name}?") define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate ivar = "@#{name}" + remove_possible_singleton_method("#{name}=") define_singleton_method("#{name}=") do |val| singleton_class.class_eval do remove_possible_method(name) @@ -110,10 +114,15 @@ class Class self.class.public_send name end end + + remove_possible_method "#{name}?" define_method("#{name}?") { !!public_send(name) } if instance_predicate end - attr_writer name if instance_writer + if instance_writer + remove_possible_method "#{name}=" + attr_writer name + end end end end diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb deleted file mode 100644 index 1c305c5970..0000000000 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ /dev/null @@ -1,45 +0,0 @@ -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. - _superclass_delegating_accessor("_#{name}", options) - - # Generate the public methods name, name=, and name?. - # These methods dispatch to the private _name, and _name= methods, making them - # overridable. - singleton_class.send(:define_method, name) { send("_#{name}") } - singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } - singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - - # If an instance_reader is needed, generate 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 - # variable and look up the superclass chain manually. - def _stash_object_in_method(object, method, instance_reader = true) - singleton_class.remove_possible_method(method) - singleton_class.send(:define_method, method) { object } - remove_possible_method(method) - define_method(method) { object } if instance_reader - end - - def _superclass_delegating_accessor(name, options = {}) - singleton_class.send(:define_method, "#{name}=") do |value| - _stash_object_in_method(value, name, options[:instance_reader] != false) - end - send("#{name}=", nil) - end -end diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 3c4bfc5f1e..b0f9a8be34 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -3,7 +3,8 @@ require 'active_support/core_ext/module/reachable' class Class begin - ObjectSpace.each_object(Class.new) {} + # Test if this Ruby supports each_object against singleton_class + ObjectSpace.each_object(Numeric.singleton_class) {} def descendants # :nodoc: descendants = [] @@ -12,7 +13,7 @@ class Class end descendants end - rescue StandardError # JRuby + rescue StandardError # JRuby 9.0.4.0 and earlier def descendants # :nodoc: descendants = [] ObjectSpace.each_object(Class) do |k| diff --git a/activesupport/lib/active_support/core_ext/date.rb b/activesupport/lib/active_support/core_ext/date.rb index 465fedda80..7f0f4639a2 100644 --- a/activesupport/lib/active_support/core_ext/date.rb +++ b/activesupport/lib/active_support/core_ext/date.rb @@ -1,5 +1,5 @@ require 'active_support/core_ext/date/acts_like' +require 'active_support/core_ext/date/blank' require 'active_support/core_ext/date/calculations' require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/date/zones' - diff --git a/activesupport/lib/active_support/core_ext/date/blank.rb b/activesupport/lib/active_support/core_ext/date/blank.rb new file mode 100644 index 0000000000..71627b6a6f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date/blank.rb @@ -0,0 +1,12 @@ +require 'date' + +class Date #:nodoc: + # No Date is blank: + # + # Date.today.blank? # => false + # + # @return [false] + def blank? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index c60e833441..d589b67bf7 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -26,7 +26,7 @@ class Date Thread.current[:beginning_of_week] = find_beginning_of_week!(week_start) end - # Returns week start day symbol (e.g. :monday), or raises an ArgumentError for invalid day symbol. + # Returns week start day symbol (e.g. :monday), or raises an +ArgumentError+ for invalid day symbol. def find_beginning_of_week!(week_start) raise ArgumentError, "Invalid beginning of week: #{week_start}" unless ::Date::DAYS_INTO_WEEK.key?(week_start) week_start diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 31479a1269..ed8bca77ac 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -75,10 +75,10 @@ class Date # # date = Date.new(2007, 11, 10) # => Sat, 10 Nov 2007 # - # date.to_time # => Sat Nov 10 00:00:00 0800 2007 - # date.to_time(:local) # => Sat Nov 10 00:00:00 0800 2007 + # date.to_time # => 2007-11-10 00:00:00 0800 + # date.to_time(:local) # => 2007-11-10 00:00:00 0800 # - # date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007 + # date.to_time(:utc) # => 2007-11-10 00:00:00 UTC def to_time(form = :local) ::Time.send(form, year, month, day) end 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 01153606c9..e079af594d 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 @@ -92,15 +92,28 @@ module DateAndTime end # Returns a new date/time at the start of the month. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Thu, 18 Jun 2015 + # today.beginning_of_month # => Mon, 01 Jun 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Thu, 18 Jun 2015 15:23:13 +0000 + # now.beginning_of_month # => Mon, 01 Jun 2015 00:00:00 +0000 def beginning_of_month first_hour(change(:day => 1)) end alias :at_beginning_of_month :beginning_of_month # Returns a new date/time at the start of the quarter. - # Example: 1st January, 1st July, 1st October. - # DateTime objects will have a time set to 0:00. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_quarter # => Wed, 01 Jul 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_quarter # => Wed, 01 Jul 2015 00:00:00 +0000 def beginning_of_quarter first_quarter_month = [10, 7, 4, 1].detect { |m| m <= month } beginning_of_month.change(:month => first_quarter_month) @@ -108,17 +121,29 @@ module DateAndTime alias :at_beginning_of_quarter :beginning_of_quarter # Returns a new date/time at the end of the quarter. - # Example: 31st March, 30th June, 30th September. - # DateTime objects will have a time set to 23:59:59. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.end_of_quarter # => Wed, 30 Sep 2015 + # + # +DateTime+ objects will have a time set to 23:59:59. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.end_of_quarter # => Wed, 30 Sep 2015 23:59:59 +0000 def end_of_quarter last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month } beginning_of_month.change(:month => last_quarter_month).end_of_month end alias :at_end_of_quarter :end_of_quarter - # Return a new date/time at the beginning of the year. - # Example: 1st January. - # DateTime objects will have a time set to 0:00. + # Returns a new date/time at the beginning of the year. + # + # today = Date.today # => Fri, 10 Jul 2015 + # today.beginning_of_year # => Thu, 01 Jan 2015 + # + # +DateTime+ objects will have a time set to 0:00. + # + # now = DateTime.current # => Fri, 10 Jul 2015 18:41:29 +0000 + # now.beginning_of_year # => Thu, 01 Jan 2015 00:00:00 +0000 def beginning_of_year change(:month => 1).beginning_of_month end @@ -138,8 +163,8 @@ module DateAndTime # # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. # - # now = Time.current # => Thu, 07 May 2015 13:31:16 UTC +00:00 - # now.next_week # => Mon, 11 May 2015 00:00:00 UTC +00:00 + # now = DateTime.current # => Thu, 07 May 2015 13:31:16 +0000 + # now.next_week # => Mon, 11 May 2015 00:00:00 +0000 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 diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb index 96c6df9407..d29a8db5cf 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb @@ -4,7 +4,7 @@ module DateAndTime # if Time.zone_default is set. Otherwise, it returns the current time. # # Time.zone = 'Hawaii' # => 'Hawaii' - # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 + # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00 # # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone @@ -14,7 +14,6 @@ module DateAndTime # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. # # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00 def in_time_zone(zone = ::Time.zone) time_zone = ::Time.find_zone! zone diff --git a/activesupport/lib/active_support/core_ext/date_time.rb b/activesupport/lib/active_support/core_ext/date_time.rb index e8a27b9f38..bcb228b09a 100644 --- a/activesupport/lib/active_support/core_ext/date_time.rb +++ b/activesupport/lib/active_support/core_ext/date_time.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/date_time/acts_like' +require 'active_support/core_ext/date_time/blank' require 'active_support/core_ext/date_time/calculations' require 'active_support/core_ext/date_time/conversions' require 'active_support/core_ext/date_time/zones' diff --git a/activesupport/lib/active_support/core_ext/date_time/blank.rb b/activesupport/lib/active_support/core_ext/date_time/blank.rb new file mode 100644 index 0000000000..56981b75fb --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_time/blank.rb @@ -0,0 +1,12 @@ +require 'date' + +class DateTime #:nodoc: + # No DateTime is ever blank: + # + # DateTime.now.blank? # => false + # + # @return [false] + def blank? + false + 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 55ad384f4f..95617fb8c2 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -168,7 +168,7 @@ class DateTime if other.kind_of?(Infinity) super elsif other.respond_to? :to_datetime - super other.to_datetime + super other.to_datetime rescue nil else nil 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 2a9c09fc29..f59d05b214 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -40,6 +40,8 @@ class DateTime alias_method :to_default_s, :to_s if instance_methods(false).include?(:to_s) alias_method :to_s, :to_formatted_s + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. # # datetime = DateTime.civil(2000, 1, 1, 0, 0, 0, Rational(-6, 24)) # datetime.formatted_offset # => "-06:00" diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 7a893292b3..8a74ad4d66 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -21,7 +21,7 @@ module Enumerable if block_given? map(&block).sum(identity) else - inject { |sum, element| sum + element } || identity + inject(:+) || identity end end @@ -71,6 +71,21 @@ module Enumerable def without(*elements) reject { |element| elements.include?(element) } end + + # Convert an enumerable to an array based on the given key. + # + # [{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) + # => ["David", "Rafael", "Aaron"] + # + # [{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name) + # => [[1, "David"], [2, "Rafael"]] + def pluck(*keys) + if keys.many? + map { |element| keys.map { |key| element[key] } } + else + map { |element| element[keys.first] } + end + end end class Range #:nodoc: diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index fad6fa8d9d..463fd78412 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -8,43 +8,45 @@ class File # file.write('hello') # end # - # If your temp directory is not on the same filesystem as the file you're - # trying to write, you can provide a different temporary directory. + # This method needs to create a temporary file. By default it will create it + # in the same directory as the destination file. If you don't like this + # behavior you can provide a different directory but it must be on the + # same physical filesystem as the file you're trying to write. # # File.atomic_write('/data/something.important', '/data/tmp') do |file| # file.write('hello') # end - def self.atomic_write(file_name, temp_dir = Dir.tmpdir) + def self.atomic_write(file_name, temp_dir = dirname(file_name)) require 'tempfile' unless defined?(Tempfile) - require 'fileutils' unless defined?(FileUtils) - temp_file = Tempfile.new(basename(file_name), temp_dir) - temp_file.binmode - return_val = yield temp_file - temp_file.close + Tempfile.open(".#{basename(file_name)}", temp_dir) do |temp_file| + temp_file.binmode + return_val = yield temp_file + temp_file.close - if File.exist?(file_name) - # Get original file permissions - old_stat = stat(file_name) - else - # If not possible, probe which are the default permissions in the - # destination directory. - old_stat = probe_stat_in(dirname(file_name)) - end - - # Overwrite original file with temp file - FileUtils.mv(temp_file.path, file_name) + old_stat = if exist?(file_name) + # Get original file permissions + stat(file_name) + elsif temp_dir != dirname(file_name) + # If not possible, probe which are the default permissions in the + # destination directory. + probe_stat_in(dirname(file_name)) + end - # Set correct permissions on new file - begin - chown(old_stat.uid, old_stat.gid, file_name) - # This operation will affect filesystem ACL's - chmod(old_stat.mode, file_name) + if old_stat + # Set correct permissions on new file + begin + chown(old_stat.uid, old_stat.gid, temp_file.path) + # This operation will affect filesystem ACL's + chmod(old_stat.mode, temp_file.path) + rescue Errno::EPERM, Errno::EACCES + # Changing file ownership failed, moving on. + end + end - # Make sure we return the result of the yielded block + # Overwrite original file with temp file + rename(temp_file.path, file_name) 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/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 2149d4439d..8594d9bf2e 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -106,7 +106,25 @@ class Hash # # => {"hash"=>{"foo"=>1, "bar"=>2}} # # +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. + # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to + # parse this XML. + # + # Custom +disallowed_types+ can also be passed in the form of an + # array. + # + # xml = <<-XML + # <?xml version="1.0" encoding="UTF-8"?> + # <hash> + # <foo type="integer">1</foo> + # <bar type="string">"David"</bar> + # </hash> + # XML + # + # hash = Hash.from_xml(xml, ['integer']) + # # => ActiveSupport::XMLConverter::DisallowedType: Disallowed type attribute: "integer" + # + # Note that passing custom disallowed types will override the default types, + # which are Symbol and YAML. def from_xml(xml, disallowed_types = nil) ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 6e397abf51..2f6d38c1f6 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,8 +1,9 @@ class Hash - # 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} + # Returns a hash that includes everything except given keys. + # hash = { a: true, b: false, c: nil } + # hash.except(:c) # => { a: true, b: false } + # hash.except(:a, :b) # => { c: nil } + # 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)) @@ -10,10 +11,10 @@ class Hash 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 } + # Removes the given keys from hash and returns it. + # 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 28cb3e2a3b..6df7b4121b 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -6,7 +6,7 @@ class Hash # # { a: 1 }.with_indifferent_access['a'] # => 1 def with_indifferent_access - ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self) + ActiveSupport::HashWithIndifferentAccess.new(self) end # Called when object is nested under an object that receives diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index c30044b9ff..8b2366c4b3 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,12 +1,16 @@ class Hash - # Returns 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"} + # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"} + # + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # + # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"} def transform_keys - return enum_for(:transform_keys) unless block_given? + return enum_for(:transform_keys) { size } unless block_given? result = self.class.new each_key do |key| result[yield(key)] = self[key] @@ -14,10 +18,10 @@ class Hash result end - # Destructively converts all keys using the block operations. - # Same as transform_keys but modifies +self+. + # Destructively converts all keys using the +block+ operations. + # Same as +transform_keys+ but modifies +self+. def transform_keys! - return enum_for(:transform_keys!) unless block_given? + return enum_for(:transform_keys!) { size } unless block_given? keys.each do |key| self[yield(key)] = delete(key) end @@ -60,7 +64,7 @@ class Hash alias_method :to_options!, :symbolize_keys! # Validates all keys in a hash match <tt>*valid_keys</tt>, raising - # ArgumentError on a mismatch. + # +ArgumentError+ on a mismatch. # # Note that keys are treated differently than HashWithIndifferentAccess, # meaning that string and symbol keys will not match. diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb index e9bcce761f..7d507ac998 100644 --- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -2,10 +2,15 @@ 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 } + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 } + # + # If you do not provide a +block+, it will return an Enumerator + # for chaining with other methods: + # + # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 } def transform_values - return enum_for(:transform_values) unless block_given? + return enum_for(:transform_values) { size } unless block_given? + return {} if empty? result = self.class.new each do |key, value| result[key] = yield(value) @@ -13,9 +18,10 @@ class Hash result end - # Destructive +transform_values+ + # Destructively converts all values using the +block+ operations. + # Same as +transform_values+ but modifies +self+. def transform_values! - return enum_for(:transform_values!) unless block_given? + return enum_for(:transform_values!) { size } unless block_given? each do |key, value| self[key] = yield(value) end diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb index f0b7382ef3..87185b024f 100644 --- a/activesupport/lib/active_support/core_ext/integer/time.rb +++ b/activesupport/lib/active_support/core_ext/integer/time.rb @@ -23,7 +23,7 @@ class Integer alias :month :months def years - ActiveSupport::Duration.new(self * 365.25.days, [[:years, self]]) + ActiveSupport::Duration.new(self * 365.25.days.to_i, [[:years, self]]) end alias :year :years end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 9189e6d977..8afc258df8 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -1,5 +1,3 @@ -require 'tempfile' - module Kernel # Sets $VERBOSE to nil for the duration of the block and back to its original # value afterwards. diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index d9fb392752..60732eb41a 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -23,7 +23,7 @@ class LoadError # 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$/, '') + location.sub(/\.rb$/, ''.freeze) == path.sub(/\.rb$/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index 20a0856e71..e333b26133 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -6,7 +6,7 @@ module ActiveSupport if exc.message.match(%r|undefined class/module (.+)|) # try loading the class/module $1.constantize - # if it is a IO we need to go back to read the object + # if it is an IO we need to go back to read the object source.rewind if source.respond_to?(:rewind) retry else diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index b4efff8b24..ef038331c2 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/module/introspection' 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/attribute_accessors_per_thread' require 'active_support/core_ext/module/attr_internal' require 'active_support/core_ext/module/concerning' require 'active_support/core_ext/module/delegation' diff --git a/activesupport/lib/active_support/core_ext/module/anonymous.rb b/activesupport/lib/active_support/core_ext/module/anonymous.rb index 0ecc67a855..510c9a5430 100644 --- a/activesupport/lib/active_support/core_ext/module/anonymous.rb +++ b/activesupport/lib/active_support/core_ext/module/anonymous.rb @@ -7,7 +7,7 @@ class Module # m = Module.new # m.name # => nil # - # +anonymous?+ method returns true if module does not have a name: + # +anonymous?+ method returns true if module does not have a name, false otherwise: # # Module.new.anonymous? # => true # @@ -18,8 +18,10 @@ class Module # via the +module+ or +class+ keyword or by an explicit assignment: # # m = Module.new # creates an anonymous module - # M = m # => m gets a name here as a side-effect + # m.anonymous? # => true + # M = m # m gets a name here as a side-effect # m.name # => "M" + # m.anonymous? # => false def anonymous? name.nil? end 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 d4e6b5a1ac..76825862d7 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/array/extract_options' # 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 + # The underlying class variable is set to +nil+, if it is not previously # defined. # # module HairColors @@ -19,9 +19,9 @@ class Module # The attribute name must be a valid method name in Ruby. # # module Foo - # mattr_reader :"1_Badname " + # mattr_reader :"1_Badname" # end - # # => NameError: invalid attribute name + # # => NameError: invalid attribute name: 1_Badname # # 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>. @@ -49,11 +49,11 @@ class Module # include HairColors # end # - # Person.hair_colors # => [:brown, :black, :blonde, :red] + # Person.new.hair_colors # => [:brown, :black, :blonde, :red] def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -105,7 +105,7 @@ class Module # # Also, you can pass a block to set up the attribute with a default value. # - # class HairColors + # module HairColors # mattr_writer :hair_colors do # [:brown, :black, :blonde, :red] # end @@ -119,7 +119,7 @@ class Module def mattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /\A[_A-Za-z]\w*\z/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -150,8 +150,8 @@ class Module # include HairColors # end # - # Person.hair_colors = [:brown, :black, :blonde, :red] - # Person.hair_colors # => [:brown, :black, :blonde, :red] + # HairColors.hair_colors = [:brown, :black, :blonde, :red] + # HairColors.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 @@ -161,8 +161,8 @@ class Module # class Male < Person # end # - # Male.hair_colors << :blue - # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] + # Male.new.hair_colors << :blue + # Person.new.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>. @@ -206,7 +206,7 @@ class Module # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_accessor(*syms, &blk) mattr_reader(*syms, &blk) - mattr_writer(*syms, &blk) + mattr_writer(*syms) end alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb new file mode 100644 index 0000000000..8a7e6776da --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb @@ -0,0 +1,141 @@ +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, but does so on a per-thread basis. +# +# So the values are scoped within the Thread.current space under the class name +# of the module. +class Module + # Defines a per-thread class attribute and creates class and instance reader methods. + # The underlying per-thread class variable is set to +nil+, if it is not previously defined. + # + # module Current + # thread_mattr_reader :user + # end + # + # Current.user # => nil + # Thread.current[:attr_Current_user] = "DHH" + # Current.user # => "DHH" + # + # The attribute name must be a valid method name in Ruby. + # + # module Foo + # thread_mattr_reader :"1_Badname" + # end + # # => NameError: invalid attribute name: 1_Badname + # + # 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>. + # + # class Current + # thread_mattr_reader :user, instance_reader: false + # end + # + # Current.new.user # => NoMethodError + def thread_mattr_reader(*syms) + options = syms.extract_options! + + syms.each do |sym| + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def self.#{sym} + Thread.current[:"attr_#{name}_#{sym}"] + end + EOS + + unless options[:instance_reader] == false || options[:instance_accessor] == false + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{sym} + Thread.current[:"attr_#{self.class.name}_#{sym}"] + end + EOS + end + end + end + alias :thread_cattr_reader :thread_mattr_reader + + # Defines a per-thread class attribute and creates a class and instance writer methods to + # allow assignment to the attribute. + # + # module Current + # thread_mattr_writer :user + # end + # + # Current.user = "DHH" + # Thread.current[:attr_Current_user] # => "DHH" + # + # If you want to opt out the instance writer method, pass + # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. + # + # class Current + # thread_mattr_writer :user, instance_writer: false + # end + # + # Current.new.user = "DHH" # => NoMethodError + def thread_mattr_writer(*syms) + options = syms.extract_options! + syms.each do |sym| + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def self.#{sym}=(obj) + Thread.current[:"attr_#{name}_#{sym}"] = obj + end + EOS + + unless options[:instance_writer] == false || options[:instance_accessor] == false + class_eval(<<-EOS, __FILE__, __LINE__ + 1) + def #{sym}=(obj) + Thread.current[:"attr_#{self.class.name}_#{sym}"] = obj + end + EOS + end + end + end + alias :thread_cattr_writer :thread_mattr_writer + + # Defines both class and instance accessors for class attributes. + # + # class Account + # thread_mattr_accessor :user + # end + # + # Account.user = "DHH" + # Account.user # => "DHH" + # Account.new.user # => "DHH" + # + # If a subclass changes the value, the parent class' value is not changed. + # Similarly, if the parent class changes the value, the value of subclasses + # is not changed. + # + # class Customer < Account + # end + # + # Customer.user = "Rafael" + # Customer.user # => "Rafael" + # Account.user # => "DHH" + # + # 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 Current + # thread_mattr_accessor :user, instance_writer: false, instance_reader: false + # end + # + # Current.new.user = "DHH" # => NoMethodError + # Current.new.user # => NoMethodError + # + # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # + # class Current + # mattr_accessor :user, instance_accessor: false + # end + # + # Current.new.user = "DHH" # => NoMethodError + # Current.new.user # => NoMethodError + def thread_mattr_accessor(*syms, &blk) + thread_mattr_reader(*syms, &blk) + thread_mattr_writer(*syms, &blk) + end + alias :thread_cattr_accessor :thread_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 index e26b594fc4..65b88b9bbd 100644 --- a/activesupport/lib/active_support/core_ext/module/concerning.rb +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -99,7 +99,7 @@ class Module # end # # Todo.ancestors - # # => Todo, Todo::EventTracking, Object + # # => [Todo, Todo::EventTracking, Object] # # This small step has some wonderful ripple effects. We can # * grok the behavior of our class in one glance, diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 9b7a429db9..0d46248582 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -5,10 +5,11 @@ class Module # 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) + DELEGATION_RESERVED_METHOD_NAMES = Set.new( + %w(_ arg args alias and BEGIN begin block 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' @@ -167,11 +168,11 @@ class Module '' end - file, line = caller(1, 1).first.split(':', 2) + file, line = caller(1, 1).first.split(':'.freeze, 2) line = line.to_i to = to.to_s - to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to) + to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= diff --git a/activesupport/lib/active_support/core_ext/module/qualified_const.rb b/activesupport/lib/active_support/core_ext/module/qualified_const.rb index 65525013db..3ea39d4267 100644 --- a/activesupport/lib/active_support/core_ext/module/qualified_const.rb +++ b/activesupport/lib/active_support/core_ext/module/qualified_const.rb @@ -3,13 +3,16 @@ require 'active_support/core_ext/string/inflections' #-- # Allows code reuse in the methods below without polluting Module. #++ -module QualifiedConstUtils - def self.raise_if_absolute(path) - raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/ - end - def self.names(path) - path.split('::') +module ActiveSupport + module QualifiedConstUtils + def self.raise_if_absolute(path) + raise NameError.new("wrong constant name #$&") if path =~ /\A::[^:]+/ + end + + def self.names(path) + path.split('::') + end end end @@ -24,9 +27,14 @@ end #++ class Module def qualified_const_defined?(path, search_parents=true) - QualifiedConstUtils.raise_if_absolute(path) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Module#qualified_const_defined? is deprecated in favour of the builtin + Module#const_defined? and will be removed in Rails 5.1. + MESSAGE - QualifiedConstUtils.names(path).inject(self) do |mod, name| + ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) + + ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name| return unless mod.const_defined?(name, search_parents) mod.const_get(name) end @@ -34,19 +42,29 @@ class Module end def qualified_const_get(path) - QualifiedConstUtils.raise_if_absolute(path) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Module#qualified_const_get is deprecated in favour of the builtin + Module#const_get and will be removed in Rails 5.1. + MESSAGE + + ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) - QualifiedConstUtils.names(path).inject(self) do |mod, name| + ActiveSupport::QualifiedConstUtils.names(path).inject(self) do |mod, name| mod.const_get(name) end end def qualified_const_set(path, value) - QualifiedConstUtils.raise_if_absolute(path) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Module#qualified_const_set is deprecated in favour of the builtin + Module#const_set and will be removed in Rails 5.1. + MESSAGE + + ActiveSupport::QualifiedConstUtils.raise_if_absolute(path) const_name = path.demodulize mod_name = path.deconstantize - mod = mod_name.empty? ? self : qualified_const_get(mod_name) + mod = mod_name.empty? ? self : const_get(mod_name) mod.const_set(const_name, value) end end 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 52632d2c6b..d5ec16d68a 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -6,10 +6,30 @@ class Module end end + # Removes the named singleton method, if it exists. + def remove_possible_singleton_method(method) + singleton_class.instance_eval do + remove_possible_method(method) + end + end + # Replaces the existing method definition, if there is one, with the passed # block as its body. def redefine_method(method, &block) + visibility = method_visibility(method) remove_possible_method(method) define_method(method, &block) + send(visibility, method) + end + + def method_visibility(method) # :nodoc: + case + when private_method_defined?(method) + :private + when protected_method_defined?(method) + :protected + else + :public + end end end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 0c8ff79237..9d832897ed 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -1,7 +1,8 @@ require 'active_support/core_ext/big_decimal/conversions' require 'active_support/number_helper' +require 'active_support/core_ext/module/deprecation' -class Numeric +module ActiveSupport::NumericWithFormat # Provides options for converting numbers into formatted strings. # Options are provided for phone numbers, currency, percentage, @@ -41,7 +42,7 @@ class Numeric # 1000.to_s(:percentage, delimiter: '.', separator: ',') # => 1.000,000% # 302.24398923423.to_s(:percentage, precision: 5) # => 302.24399% # 1000.to_s(:percentage, locale: :fr) # => 1 000,000% - # 100.to_s(:percentage, format: '%n %') # => 100 % + # 100.to_s(:percentage, format: '%n %') # => 100.000 % # # Delimited: # 12345678.to_s(:delimited) # => 12,345,678 @@ -75,10 +76,12 @@ class Numeric # 1234567.to_s(:human_size) # => 1.18 MB # 1234567890.to_s(:human_size) # => 1.15 GB # 1234567890123.to_s(:human_size) # => 1.12 TB + # 1234567890123456.to_s(:human_size) # => 1.1 PB + # 1234567890123456789.to_s(:human_size) # => 1.07 EB # 1234567.to_s(:human_size, precision: 2) # => 1.2 MB # 483989.to_s(:human_size, precision: 2) # => 470 KB # 1234567.to_s(:human_size, precision: 2, separator: ',') # => 1,2 MB - # 1234567890123.to_s(:human_size, precision: 5) # => "1.1229 TB" + # 1234567890123.to_s(:human_size, precision: 5) # => "1.1228 TB" # 524288000.to_s(:human_size, precision: 5) # => "500 MB" # # Human-friendly format: @@ -97,7 +100,10 @@ class Numeric # 1234567.to_s(:human, precision: 1, # separator: ',', # significant: false) # => "1,2 Million" - def to_formatted_s(format = :default, options = {}) + def to_s(*args) + format, options = args + options ||= {} + case format when :phone return ActiveSupport::NumberHelper.number_to_phone(self, options) @@ -114,32 +120,20 @@ class Numeric when :human_size return ActiveSupport::NumberHelper.number_to_human_size(self, options) else - self.to_default_s - end - end - - [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 + if is_a?(Float) || format.is_a?(Symbol) + super() + else + super end end end - Float.class_eval do - alias_method :to_default_s, :to_s - def to_s(*args) - if args.empty? - to_default_s - else - to_formatted_s(*args) - end - end + def to_formatted_s(*args) + to_s(*args) end + deprecate to_formatted_s: :to_s +end +[Fixnum, Bignum, Float, BigDecimal].each do |klass| + klass.prepend(ActiveSupport::NumericWithFormat) end diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index 38e43478df..039c50a4a2 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -1,12 +1,10 @@ -# encoding: utf-8 - class Object # An object is blank if it's false, empty, or a whitespace string. - # For example, '', ' ', +nil+, [], and {} are all blank. + # For example, +false+, '', ' ', +nil+, [], and {} are all blank. # # This simplifies # - # address.nil? || address.empty? + # !address || address.empty? # # to # @@ -129,3 +127,14 @@ class Numeric #:nodoc: false end end + +class Time #:nodoc: + # No Time is blank: + # + # Time.now.blank? # => false + # + # @return [false] + def blank? + false + end +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 ad5b2af161..8dfeed0066 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -39,9 +39,15 @@ class Hash # hash[:a][:c] # => nil # dup[:a][:c] # => "c" def deep_dup - each_with_object(dup) do |(key, value), hash| - hash.delete(key) - hash[key.deep_dup] = value.deep_dup + hash = dup + each_pair do |key, value| + if key.frozen? && ::String === key + hash[key] = value.deep_dup + else + hash.delete(key) + hash[key.deep_dup] = value.deep_dup + end end + hash end end diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 620f7b6561..befa5aee21 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, number objects; + # False for +nil+, +false+, +true+, symbol, number, method objects; # true otherwise. def duplicable? true @@ -78,6 +78,10 @@ end require 'bigdecimal' class BigDecimal + # BigDecimals are duplicable: + # + # BigDecimal.new("1.2").duplicable? # => true + # BigDecimal.new("1.2").dup # => #<BigDecimal:...,'0.12E1',18(18)> def duplicable? true end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index 55f281b213..d4c17dfb07 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -5,7 +5,7 @@ class Object # characters = ["Konata", "Kagami", "Tsukasa"] # "Konata".in?(characters) # => true # - # This will throw an ArgumentError if the argument doesn't respond + # This will throw an +ArgumentError+ if the argument doesn't respond # to +#include?+. def in?(another_object) another_object.include?(self) @@ -18,7 +18,7 @@ class Object # # params[:bucket_type].presence_in %w( project calendar ) # - # This will throw an ArgumentError if the argument doesn't respond to +#include?+. + # This will throw an +ArgumentError+ if the argument doesn't respond to +#include?+. # # @return [Object] def presence_in(another_object) diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 69be6c4abc..8c16d95b62 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -8,7 +8,7 @@ module ActiveSupport def try!(*a, &b) if a.empty? && block_given? - if b.arity.zero? + if b.arity == 0 instance_eval(&b) else yield self @@ -94,7 +94,7 @@ class Object # :call-seq: # try!(*a, &b) # - # Same as #try, but raises a NoMethodError exception if the receiver is + # 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" diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index 83eced50bf..965436c23a 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -1,34 +1,31 @@ -class Range +module ActiveSupport::RangeWithFormat RANGE_FORMATS = { :db => Proc.new { |start, stop| "BETWEEN '#{start.to_s(:db)}' AND '#{stop.to_s(:db)}'" } } # Convert range to a formatted string. See RANGE_FORMATS for predefined formats. # - # This method is aliased to <tt>to_s</tt>. - # # range = (1..100) # => 1..100 # - # range.to_formatted_s # => "1..100" # range.to_s # => "1..100" - # - # range.to_formatted_s(:db) # => "BETWEEN '1' AND '100'" # range.to_s(:db) # => "BETWEEN '1' AND '100'" # - # == Adding your own range formats to to_formatted_s + # == Adding your own range formats to to_s # You can add your own formats to the Range::RANGE_FORMATS hash. # Use the format name as the hash key and a Proc instance. # # # config/initializers/range_formats.rb # Range::RANGE_FORMATS[:short] = ->(start, stop) { "Between #{start.to_s(:db)} and #{stop.to_s(:db)}" } - def to_formatted_s(format = :default) + def to_s(format = :default) if formatter = RANGE_FORMATS[format] formatter.call(first, last) else - to_default_s + super() end end alias_method :to_default_s, :to_s - alias_method :to_s, :to_formatted_s + alias_method :to_formatted_s, :to_s end + +Range.prepend(ActiveSupport::RangeWithFormat) diff --git a/activesupport/lib/active_support/core_ext/securerandom.rb b/activesupport/lib/active_support/core_ext/securerandom.rb index 6cdbea1f37..98cf7430f7 100644 --- a/activesupport/lib/active_support/core_ext/securerandom.rb +++ b/activesupport/lib/active_support/core_ext/securerandom.rb @@ -10,8 +10,8 @@ module SecureRandom # # The result may contain alphanumeric characters except 0, O, I and l # - # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE" - # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7" + # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE" + # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7" # def self.base58(n = 16) SecureRandom.random_bytes(n).unpack("C*").map do |byte| diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 3e0cb8a7ac..fd79a40e31 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -14,7 +14,7 @@ class String # "06:12".to_time # => 2012-12-13 06:12:00 +0100 # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100 # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100 - # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC + # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 06:12:00 UTC # "12/13/2012".to_time # => ArgumentError: argument out of range def to_time(form = :local) parts = Date._parse(self, false) diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 97f9720b2b..cc71b8155d 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -164,15 +164,33 @@ class String # # <%= link_to(@person.name, person_path) %> # # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a> - def parameterize(sep = '-') - ActiveSupport::Inflector.parameterize(self, sep) + # + # To preserve the case of the characters in a string, use the `preserve_case` argument. + # + # class Person + # def to_param + # "#{id}-#{name.parameterize(preserve_case: true)}" + # end + # end + # + # @person = Person.find(1) + # # => #<Person id: 1, name: "Donald E. Knuth"> + # + # <%= link_to(@person.name, person_path) %> + # # => <a href="/person/1-Donald-E-Knuth">Donald E. Knuth</a> + def parameterize(sep = :unused, separator: '-', preserve_case: false) + unless sep == :unused + ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") + separator = sep + end + ActiveSupport::Inflector.parameterize(self, separator: separator, preserve_case: preserve_case) end # 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" + # 'ham_and_egg'.tableize # => "ham_and_eggs" # 'fancyCategory'.tableize # => "fancy_categories" def tableize ActiveSupport::Inflector.tableize(self) @@ -182,7 +200,7 @@ class String # Note that this returns a string and not a class. (To convert to an actual class # follow +classify+ with +constantize+.) # - # 'egg_and_hams'.classify # => "EggAndHam" + # 'ham_and_eggs'.classify # => "HamAndEgg" # 'posts'.classify # => "Post" def classify ActiveSupport::Inflector.classify(self) diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 7055f7f699..cc6f2158e7 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -9,12 +9,10 @@ class String # encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy # class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string. # - # name = 'Claus Müller' - # name.reverse # => "rell??M sualC" - # name.length # => 13 - # - # name.mb_chars.reverse.to_s # => "rellüM sualC" - # name.mb_chars.length # => 12 + # >> "lj".upcase + # => "lj" + # >> "lj".mb_chars.upcase.to_s + # => "LJ" # # == Method chaining # 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 c676b26b06..04ed8e7cd8 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -5,7 +5,6 @@ class ERB module Util HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } 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 @@ -37,7 +36,7 @@ class ERB if s.html_safe? s else - s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) + CGI.escapeHTML(ActiveSupport::Multibyte::Unicode.tidy_bytes(s)) end end module_function :unwrapped_html_escape @@ -50,7 +49,7 @@ class ERB # html_escape_once('<< Accept & Checkout') # # => "<< Accept & Checkout" def html_escape_once(s) - result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) + result = ActiveSupport::Multibyte::Unicode.tidy_bytes(s.to_s).gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) s.html_safe? ? result.html_safe : result end @@ -86,7 +85,7 @@ class ERB # use inside HTML attributes. # # 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. + # 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. # @@ -243,8 +242,7 @@ module ActiveSupport #:nodoc: 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) + (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s) end end end diff --git a/activesupport/lib/active_support/core_ext/string/strip.rb b/activesupport/lib/active_support/core_ext/string/strip.rb index 086c610976..55b9b87352 100644 --- a/activesupport/lib/active_support/core_ext/string/strip.rb +++ b/activesupport/lib/active_support/core_ext/string/strip.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/try' - class String # Strips indentation in heredocs. # @@ -17,10 +15,9 @@ class String # # the user would see the usage message aligned against the left margin. # - # Technically, it looks for the least indented line in the whole string, and removes - # that amount of leading whitespace. + # Technically, it looks for the least indented non-empty line + # in the whole string, and removes that amount of leading whitespace. def strip_heredoc - indent = scan(/^[ \t]*(?=\S)/).min.try(:size) || 0 - gsub(/^[ \t]{#{indent}}/, '') + gsub(/^#{scan(/^[ \t]*(?=\S)/).min}/, ''.freeze) end end diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 1ce68ea7c7..768c9a1b2c 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -3,6 +3,7 @@ require 'active_support/core_ext/time/conversions' require 'active_support/time_with_zone' require 'active_support/core_ext/time/zones' require 'active_support/core_ext/date_and_time/calculations' +require 'active_support/core_ext/date/calculations' class Time include DateAndTime::Calculations @@ -15,9 +16,9 @@ class Time super || (self == Time && other.is_a?(ActiveSupport::TimeWithZone)) end - # Return the number of days in the given month. + # Returns the number of days in the given month. # If no year is specified, it will use the current year. - def days_in_month(month, year = now.year) + def days_in_month(month, year = current.year) if month == 2 && ::Date.gregorian_leap?(year) 29 else @@ -25,6 +26,12 @@ class Time end end + # Returns the number of days in the given year. + # If no year is specified, it will use the current year. + def days_in_year(year = current.year) + days_in_month(2, year) + 337 + end + # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>. def current ::Time.zone ? ::Time.zone.now : ::Time.now @@ -50,9 +57,9 @@ class Time # Returns the number of seconds since 00:00:00. # - # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0 - # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296 - # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399 + # Time.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0.0 + # Time.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296.0 + # Time.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399.0 def seconds_since_midnight to_i - change(:hour => 0).to_i + (usec / 1.0e+6) end @@ -98,7 +105,7 @@ class Time 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 + raise ArgumentError, 'argument out of range' if new_usec >= 1000000 ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) end end @@ -108,6 +115,12 @@ class Time # 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>. + # + # Time.new(2015, 8, 1, 14, 35, 0).advance(seconds: 1) # => 2015-08-01 14:35:01 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(minutes: 1) # => 2015-08-01 14:36:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(hours: 1) # => 2015-08-01 15:35:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(days: 1) # => 2015-08-02 14:35:00 -0700 + # Time.new(2015, 8, 1, 14, 35, 0).advance(weeks: 1) # => 2015-08-08 14:35:00 -0700 def advance(options) unless options[:weeks].nil? options[:weeks], partial_weeks = options[:weeks].divmod(1) @@ -149,7 +162,6 @@ class Time # Returns a new Time representing the start of the day (0:00) def beginning_of_day - #(self - seconds_since_midnight).change(usec: 0) change(:hour => 0) end alias :midnight :beginning_of_day diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index dbf1f2f373..536c4bf525 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -6,6 +6,7 @@ class Time :db => '%Y-%m-%d %H:%M:%S', :number => '%Y%m%d%H%M%S', :nsec => '%Y%m%d%H%M%S%9N', + :usec => '%Y%m%d%H%M%S%6N', :time => '%H:%M', :short => '%d %b %H:%M', :long => '%B %d, %Y %H:%M', @@ -24,7 +25,7 @@ class Time # # This method is aliased to <tt>to_s</tt>. # - # time = Time.now # => Thu Jan 18 06:10:17 CST 2007 + # time = Time.now # => 2007-01-18 06:10:17 -06:00 # # time.to_formatted_s(:time) # => "06:10" # time.to_s(:time) # => "06:10" @@ -55,7 +56,8 @@ class Time alias_method :to_default_s, :to_s alias_method :to_s, :to_formatted_s - # Returns the UTC offset as an +HH:MM formatted string. + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. # # Time.local(2000).formatted_offset # => "-06:00" # Time.local(2000).formatted_offset(false) # => "-0600" diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index d683e7c777..877dc84ec8 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -53,7 +53,7 @@ class Time # 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. + # 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" ...> @@ -65,7 +65,8 @@ class Time if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) time_zone else - # lookup timezone based on identifier (unless we've been passed a TZInfo::Timezone) + # Look up the timezone based on the identifier (unless we've been + # passed a TZInfo::Timezone) unless time_zone.respond_to?(:period_for_local) time_zone = ActiveSupport::TimeZone[time_zone] || TZInfo::Timezone.get(time_zone) end diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index bfe0832b37..c6c183edd9 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'uri' str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. parser = URI::Parser.new @@ -12,7 +10,7 @@ unless str == parser.unescape(parser.escape(str)) # YK: My initial experiments say yes, but let's be sure please enc = str.encoding enc = Encoding::UTF_8 if enc == Encoding::US_ASCII - str.gsub(escaped) { [$&[1, 2].hex].pack('C') }.force_encoding(enc) + str.gsub(escaped) { |match| [match[1, 2].hex].pack('C') }.force_encoding(enc) end end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 664cc15a29..af18ff746f 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -1,6 +1,6 @@ require 'set' require 'thread' -require 'thread_safe' +require 'concurrent/map' require 'pathname' require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/attribute_accessors' @@ -12,12 +12,40 @@ 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' +require "active_support/dependencies/interlock" require 'active_support/inflector' module ActiveSupport #:nodoc: module Dependencies #:nodoc: extend self + mattr_accessor :interlock + self.interlock = Interlock.new + + # :doc: + + # Execute the supplied block without interference from any + # concurrent loads. + def self.run_interlock + Dependencies.interlock.running { yield } + end + + # Execute the supplied block while holding an exclusive lock, + # preventing any other thread from being inside a #run_interlock + # block at the same time. + def self.load_interlock + Dependencies.interlock.loading { yield } + end + + # Execute the supplied block while holding an exclusive lock, + # preventing any other thread from being inside a #run_interlock + # block at the same time. + def self.unload_interlock + Dependencies.interlock.unloading { yield } + end + + # :nodoc: + # Should we turn on Ruby warnings on the first load of dependent files? mattr_accessor :warnings_on_first_load self.warnings_on_first_load = false @@ -128,7 +156,7 @@ module ActiveSupport #:nodoc: # Normalize the list of new constants, and add them to the list we will return new_constants.each do |suffix| - constants << ([namespace, suffix] - ["Object"]).join("::") + constants << ([namespace, suffix] - ["Object"]).join("::".freeze) end end constants @@ -325,9 +353,11 @@ module ActiveSupport #:nodoc: def clear log_call - loaded.clear - loading.clear - remove_unloadable_constants! + Dependencies.unload_interlock do + loaded.clear + loading.clear + remove_unloadable_constants! + end end def require_or_load(file_name, const_path = nil) @@ -336,39 +366,44 @@ module ActiveSupport #:nodoc: expanded = File.expand_path(file_name) return if loaded.include?(expanded) - # Record that we've seen this file *before* loading it to avoid an - # infinite loop with mutual dependencies. - loaded << expanded - loading << expanded + Dependencies.load_interlock do + # Maybe it got loaded while we were waiting for our lock: + return if loaded.include?(expanded) - begin - if load? - log "loading #{file_name}" + # Record that we've seen this file *before* loading it to avoid an + # infinite loop with mutual dependencies. + loaded << expanded + loading << expanded - # Enable warnings if this file has not been loaded before and - # warnings_on_first_load is set. - load_args = ["#{file_name}.rb"] - load_args << const_path unless const_path.nil? - - if !warnings_on_first_load or history.include?(expanded) - result = load_file(*load_args) + begin + if load? + log "loading #{file_name}" + + # Enable warnings if this file has not been loaded before and + # warnings_on_first_load is set. + load_args = ["#{file_name}.rb"] + load_args << const_path unless const_path.nil? + + if !warnings_on_first_load or history.include?(expanded) + result = load_file(*load_args) + else + enable_warnings { result = load_file(*load_args) } + end else - enable_warnings { result = load_file(*load_args) } + log "requiring #{file_name}" + result = require file_name end - else - log "requiring #{file_name}" - result = require file_name + rescue Exception + loaded.delete expanded + raise + ensure + loading.pop end - rescue Exception - loaded.delete expanded - raise - ensure - loading.pop - end - # Record history *after* loading so first load gets warnings. - history << expanded - result + # Record history *after* loading so first load gets warnings. + history << expanded + result + end end # Is the provided constant path defined? @@ -386,13 +421,13 @@ module ActiveSupport #:nodoc: bases.each do |root| expanded_root = File.expand_path(root) - next unless %r{\A#{Regexp.escape(expanded_root)}(/|\\)} =~ expanded_path + next unless expanded_path.start_with?(expanded_root) - nesting = expanded_path[(expanded_root.size)..-1] - nesting = nesting[1..-1] if nesting && nesting[0] == ?/ - next if nesting.blank? + root_size = expanded_root.size + next if expanded_path[root_size] != ?/.freeze - paths << nesting.camelize + nesting = expanded_path[(root_size + 1)..-1] + paths << nesting.camelize unless nesting.blank? end paths.uniq! @@ -401,7 +436,7 @@ module ActiveSupport #:nodoc: # Search for a file in autoload_paths matching the provided suffix. def search_for_file(path_suffix) - path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb") + path_suffix = path_suffix.sub(/(\.rb)?$/, ".rb".freeze) autoload_paths.each do |root| path = File.join(root, path_suffix) @@ -486,7 +521,7 @@ module ActiveSupport #:nodoc: if file_path expanded = File.expand_path(file_path) - expanded.sub!(/\.rb\z/, '') + expanded.sub!(/\.rb\z/, ''.freeze) if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" @@ -550,7 +585,7 @@ module ActiveSupport #:nodoc: class ClassCache def initialize - @store = ThreadSafe::Cache.new + @store = Concurrent::Map.new end def empty? diff --git a/activesupport/lib/active_support/dependencies/interlock.rb b/activesupport/lib/active_support/dependencies/interlock.rb new file mode 100644 index 0000000000..fbeb904684 --- /dev/null +++ b/activesupport/lib/active_support/dependencies/interlock.rb @@ -0,0 +1,47 @@ +require 'active_support/concurrency/share_lock' + +module ActiveSupport #:nodoc: + module Dependencies #:nodoc: + class Interlock + def initialize # :nodoc: + @lock = ActiveSupport::Concurrency::ShareLock.new + end + + def loading + @lock.exclusive(purpose: :load, compatible: [:load]) do + yield + end + end + + def unloading + @lock.exclusive(purpose: :unload, compatible: [:load, :unload]) do + yield + end + end + + # Attempt to obtain an "unloading" (exclusive) lock. If possible, + # execute the supplied block while holding the lock. If there is + # concurrent activity, return immediately (without executing the + # block) instead of waiting. + def attempt_unloading + @lock.exclusive(purpose: :unload, compatible: [:load, :unload], no_wait: true) do + yield + end + end + + def start_running + @lock.start_sharing + end + + def done_running + @lock.stop_sharing + end + + def running + @lock.sharing do + yield + end + end + end + end +end diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 46e9996d59..24545d766c 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 = '5.0', gem_name = 'Rails') + def initialize(deprecation_horizon = '5.1', 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 0cdc7e96f7..28d2d78643 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -39,8 +39,8 @@ module ActiveSupport } # Behavior module allows to determine how to display deprecation messages. - # You can set any behaviors from +DEFAULT_BEHAVIORS+ constant or create - # custom behavior. Available behaviors: + # You can create a custom behavior or set any from the +DEFAULT_BEHAVIORS+ + # constant. Available behaviors are: # # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>. # [+stderr+] Log all deprecation warnings to +$stderr+. @@ -49,7 +49,7 @@ module ActiveSupport # [+silence+] Do nothing. # # Setting behaviors only affects deprecations that happen after boot time. - # For more information you can read documentation for +behavior=+ method. + # For more information you can read the documentation of the +behavior=+ method. module Behavior # Whether to print a backtrace along with the warning. attr_accessor :debug diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb index c74e9c40ac..f5ea6669ce 100644 --- a/activesupport/lib/active_support/deprecation/method_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb @@ -9,37 +9,61 @@ module ActiveSupport # module Fred # extend self # - # def foo; end - # def bar; end - # def baz; end + # def aaa; end + # def bbb; end + # def ccc; end + # def ddd; end + # def eee; end # end # - # ActiveSupport::Deprecation.deprecate_methods(Fred, :foo, bar: :qux, baz: 'use Bar#baz instead') - # # => [:foo, :bar, :baz] + # Using the default deprecator: + # ActiveSupport::Deprecation.deprecate_methods(Fred, :aaa, bbb: :zzz, ccc: 'use Bar#ccc instead') + # # => [:aaa, :bbb, :ccc] # - # Fred.foo - # # => "DEPRECATION WARNING: foo is deprecated and will be removed from Rails 4.1." + # Fred.aaa + # # DEPRECATION WARNING: aaa is deprecated and will be removed from Rails 5.1. (called from irb_binding at (irb):10) + # # => nil # - # Fred.bar - # # => "DEPRECATION WARNING: bar is deprecated and will be removed from Rails 4.1 (use qux instead)." + # Fred.bbb + # # DEPRECATION WARNING: bbb is deprecated and will be removed from Rails 5.1 (use zzz instead). (called from irb_binding at (irb):11) + # # => nil # - # Fred.baz - # # => "DEPRECATION WARNING: baz is deprecated and will be removed from Rails 4.1 (use Bar#baz instead)." + # Fred.ccc + # # DEPRECATION WARNING: ccc is deprecated and will be removed from Rails 5.1 (use Bar#ccc instead). (called from irb_binding at (irb):12) + # # => nil + # + # Passing in a custom deprecator: + # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem') + # ActiveSupport::Deprecation.deprecate_methods(Fred, ddd: :zzz, deprecator: custom_deprecator) + # # => [:ddd] + # + # Fred.ddd + # DEPRECATION WARNING: ddd is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):15) + # # => nil + # + # Using a custom deprecator directly: + # custom_deprecator = ActiveSupport::Deprecation.new('next-release', 'MyGem') + # custom_deprecator.deprecate_methods(Fred, eee: :zzz) + # # => [:eee] + # + # Fred.eee + # DEPRECATION WARNING: eee is deprecated and will be removed from MyGem next-release (use zzz instead). (called from irb_binding at (irb):18) + # # => nil def deprecate_methods(target_module, *method_names) options = method_names.extract_options! - deprecator = options.delete(:deprecator) || ActiveSupport::Deprecation.instance + deprecator = options.delete(:deprecator) || self method_names += options.keys - method_names.each do |method_name| - mod = Module.new do + mod = Module.new do + method_names.each do |method_name| define_method(method_name) do |*args, &block| deprecator.deprecation_warning(method_name, options[method_name]) super(*args, &block) end end - - target_module.prepend(mod) end + + target_module.prepend(mod) end end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index c6d2b5e795..6f0ad445fc 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -20,7 +20,7 @@ module ActiveSupport private def method_missing(called, *args, &block) - warn caller, called, args + warn caller_locations, called, args target.__send__(called, *args, &block) end end @@ -111,7 +111,7 @@ module ActiveSupport # # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) # - # (In a later update, the orignal implementation of `PLANETS` has been removed.) + # (In a later update, the original implementation of `PLANETS` has been removed.) # # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') @@ -127,7 +127,7 @@ module ActiveSupport @deprecator = deprecator end - # Returns class of a new constant. + # Returns the class of the new constant. # # PLANETS_POST_2006 = %w(mercury venus earth mars jupiter saturn uranus neptune) # PLANETS = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006') diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb index a7d265d732..f89fc0fe14 100644 --- a/activesupport/lib/active_support/deprecation/reporting.rb +++ b/activesupport/lib/active_support/deprecation/reporting.rb @@ -14,7 +14,7 @@ module ActiveSupport def warn(message = nil, callstack = nil) return if silenced - callstack ||= caller(2) + callstack ||= caller_locations(2) deprecation_message(callstack, message).tap do |m| behavior.each { |b| b.call(m, callstack) } end @@ -37,7 +37,7 @@ module ActiveSupport end def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil) - caller_backtrace ||= caller(2) + caller_backtrace ||= caller_locations(2) deprecated_method_warning(deprecated_method_name, message).tap do |msg| warn(msg, caller_backtrace) end @@ -79,6 +79,17 @@ module ActiveSupport end def extract_callstack(callstack) + return _extract_callstack(callstack) if callstack.first.is_a? String + + rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/" + offending_line = callstack.find { |frame| + frame.absolute_path && !frame.absolute_path.start_with?(rails_gem_root) + } || callstack.first + [offending_line.path, offending_line.lineno, offending_line.label] + end + + def _extract_callstack(callstack) + warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE rails_gem_root = File.expand_path("../../../../..", __FILE__) + "/" offending_line = callstack.find { |line| !line.start_with?(rails_gem_root) } || callstack.first if offending_line diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 4c0d1197fe..c63b61e97a 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -52,6 +52,10 @@ module ActiveSupport end end + # Returns the amount of seconds a duration covers as a string. + # For more information check to_i method. + # + # 1.day.to_s # => "86400" def to_s @value.to_s end diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb new file mode 100644 index 0000000000..315be85fb3 --- /dev/null +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -0,0 +1,150 @@ +require 'set' +require 'pathname' +require 'concurrent/atomic/atomic_boolean' + +module ActiveSupport + class EventedFileUpdateChecker #:nodoc: all + def initialize(files, dirs = {}, &block) + @ph = PathHelper.new + @files = files.map { |f| @ph.xpath(f) }.to_set + + @dirs = {} + dirs.each do |dir, exts| + @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) } + end + + @block = block + @updated = Concurrent::AtomicBoolean.new(false) + @lcsp = @ph.longest_common_subpath(@dirs.keys) + + if (dtw = directories_to_watch).any? + # Loading listen triggers warnings. These are originated by a legit + # usage of attr_* macros for private attributes, but adds a lot of noise + # to our test suite. Thus, we lazy load it and disable warnings locally. + silence_warnings { require 'listen' } + Listen.to(*dtw, &method(:changed)).start + end + end + + def updated? + @updated.true? + end + + def execute + @updated.make_false + @block.call + end + + def execute_if_updated + if updated? + execute + true + end + end + + private + + def changed(modified, added, removed) + unless updated? + @updated.make_true if (modified + added + removed).any? { |f| watching?(f) } + end + end + + def watching?(file) + file = @ph.xpath(file) + + if @files.member?(file) + true + elsif file.directory? + false + else + ext = @ph.normalize_extension(file.extname) + + file.dirname.ascend do |dir| + if @dirs.fetch(dir, []).include?(ext) + break true + elsif dir == @lcsp || dir.root? + break false + end + end + end + end + + def directories_to_watch + dtw = (@files + @dirs.keys).map { |f| @ph.existing_parent(f) } + dtw.compact! + dtw.uniq! + + @ph.filter_out_descendants(dtw) + end + + class PathHelper + using Module.new { + refine Pathname do + def ascendant_of?(other) + self != other && other.ascend do |ascendant| + break true if self == ascendant + end + end + end + } + + def xpath(path) + Pathname.new(path).expand_path + end + + def normalize_extension(ext) + ext.to_s.sub(/\A\./, '') + end + + # Given a collection of Pathname objects returns the longest subpath + # common to all of them, or +nil+ if there is none. + def longest_common_subpath(paths) + return if paths.empty? + + lcsp = Pathname.new(paths[0]) + + paths[1..-1].each do |path| + until lcsp.ascendant_of?(path) + if lcsp.root? + # If we get here a root directory is not an ascendant of path. + # This may happen if there are paths in different drives on + # Windows. + return + else + lcsp = lcsp.parent + end + end + end + + lcsp + end + + # Returns the deepest existing ascendant, which could be the argument itself. + def existing_parent(dir) + dir.ascend do |ascendant| + break ascendant if ascendant.directory? + end + end + + # Filters out directories which are descendants of others in the collection (stable). + def filter_out_descendants(dirs) + return dirs if dirs.length < 2 + + dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length } + descendants = [] + + until dirs_sorted_by_nparts.empty? + dir = dirs_sorted_by_nparts.shift + + dirs_sorted_by_nparts.reject! do |possible_descendant| + dir.ascendant_of?(possible_descendant) && descendants << possible_descendant + end + end + + # Array#- preserves order. + dirs - descendants + end + end + end +end diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 78b627c286..1fa9335080 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -35,7 +35,7 @@ module ActiveSupport # This method must also receive a block that will be called once a path # changes. The array of files and list of directories cannot be changed # after FileUpdateChecker has been initialized. - def initialize(files, dirs={}, &block) + def initialize(files, dirs = {}, &block) @files = files.freeze @glob = compile_glob(dirs) @block = block diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb index 7068f09d87..7790a9b2c0 100644 --- a/activesupport/lib/active_support/gem_version.rb +++ b/activesupport/lib/active_support/gem_version.rb @@ -1,5 +1,5 @@ module ActiveSupport - # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt> + # 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 @@ -8,7 +8,7 @@ module ActiveSupport MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 4f71f13971..b878f31e75 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -59,25 +59,28 @@ module ActiveSupport if constructor.respond_to?(:to_hash) super() update(constructor) + + hash = constructor.to_hash + self.default = hash.default if hash.default + self.default_proc = hash.default_proc if hash.default_proc else super(constructor) end end - def default(key = nil) - if key.is_a?(Symbol) && include?(key = key.to_s) - self[key] - else - super - end + def default(*args) + key = args.first + args[0] = key.to_s if key.is_a?(Symbol) + super(*args) 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 + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default` + has been deprecated, and will be removed in Rails 5.1. The behavior of + this method is now identical to the behavior of `.new`. + MSG + new(hash) end def self.[](*args) @@ -92,7 +95,7 @@ module ActiveSupport # hash = ActiveSupport::HashWithIndifferentAccess.new # hash[:key] = 'value' # - # This value can be later fetched using either +:key+ or +'key'+. + # This value can be later fetched using either +:key+ or <tt>'key'</tt>. def []=(key, value) regular_writer(convert_key(key), convert_value(value, for: :assignment)) end @@ -154,6 +157,20 @@ module ActiveSupport alias_method :has_key?, :key? alias_method :member?, :key? + + # Same as <tt>Hash#[]</tt> where the key passed as argument can be + # either a string or a symbol: + # + # counters = ActiveSupport::HashWithIndifferentAccess.new + # counters[:foo] = 1 + # + # counters['foo'] # => 1 + # counters[:foo] # => 1 + # counters[:zoo] # => nil + def [](key) + super(convert_key(key)) + end + # Same as <tt>Hash#fetch</tt> where the key passed as argument can be # either a string or a symbol: # @@ -188,7 +205,7 @@ module ActiveSupport # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| - new_hash.default = default + set_defaults(new_hash) end end @@ -206,7 +223,7 @@ module ActiveSupport # hash['a'] = nil # hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1} def reverse_merge(other_hash) - super(self.class.new_from_hash_copying_default(other_hash)) + super(self.class.new(other_hash)) end # Same semantics as +reverse_merge+ but modifies the receiver in-place. @@ -219,7 +236,7 @@ module ActiveSupport # h = { "a" => 100, "b" => 200 } # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400} def replace(other_hash) - super(self.class.new_from_hash_copying_default(other_hash)) + super(self.class.new(other_hash)) end # Removes the specified key from the hash. @@ -238,16 +255,20 @@ module ActiveSupport def to_options!; self end def select(*args, &block) + return to_enum(:select) unless block_given? dup.tap { |hash| hash.select!(*args, &block) } end def reject(*args, &block) + return to_enum(:reject) unless block_given? dup.tap { |hash| hash.reject!(*args, &block) } end # Convert to a regular hash with string keys. def to_hash - _new_hash = Hash.new(default) + _new_hash = Hash.new + set_defaults(_new_hash) + each do |key, value| _new_hash[key] = convert_value(value, for: :to_hash) end @@ -275,6 +296,14 @@ module ActiveSupport value end end + + def set_defaults(target) + if default_proc + target.default_proc = default_proc.dup + else + target.default = default + end + end end end diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 95f3f6255a..82aacf3b24 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -37,10 +37,12 @@ module I18n enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? I18n.enforce_available_locales = false + reloadable_paths = [] app.config.i18n.each do |setting, value| case setting when :railties_load_path - app.config.i18n.load_path.unshift(*value) + reloadable_paths = value + app.config.i18n.load_path.unshift(*value.map(&:existent).flatten) when :load_path I18n.load_path += value else @@ -53,7 +55,14 @@ module I18n # 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! } + directories = watched_dirs_with_extensions(reloadable_paths) + reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do + I18n.load_path.keep_if { |p| File.exist?(p) } + I18n.load_path |= reloadable_paths.map(&:existent).flatten + + I18n.reload! + end + app.reloaders << reloader ActionDispatch::Reloader.to_prepare do reloader.execute_if_updated @@ -96,5 +105,11 @@ module I18n raise "Unexpected fallback type #{fallbacks.inspect}" end end + + def self.watched_dirs_with_extensions(paths) + paths.each_with_object({}) do |path, result| + result[path.absolute_current] = path.extensions + end + end end end diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 486838bd15..f3e52b48ac 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -1,4 +1,4 @@ -require 'thread_safe' +require 'concurrent/map' require 'active_support/core_ext/array/prepend_and_append' require 'active_support/i18n' @@ -25,7 +25,38 @@ module ActiveSupport # singularization rules that is runs. This guarantees that your rules run # before any of the rules that may already have been loaded. class Inflections - @__instance__ = ThreadSafe::Cache.new + @__instance__ = Concurrent::Map.new + + class Uncountables < Array + def initialize + @regex_array = [] + super + end + + def delete(entry) + super entry + @regex_array.delete(to_regex(entry)) + end + + def <<(*word) + add(word) + end + + def add(words) + self.concat(words.flatten.map(&:downcase)) + @regex_array += self.map {|word| to_regex(word) } + self + end + + def uncountable?(str) + @regex_array.any? { |regex| regex === str } + end + + private + def to_regex(string) + /\b#{::Regexp.escape(string)}\Z/i + end + end def self.instance(locale = :en) @__instance__[locale] ||= new @@ -34,7 +65,7 @@ module ActiveSupport attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex def initialize - @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], [], [], {}, /(?=a)b/ + @plurals, @singulars, @uncountables, @humans, @acronyms, @acronym_regex = [], [], Uncountables.new, [], {}, /(?=a)b/ end # Private, for the test suite. @@ -160,7 +191,7 @@ module ActiveSupport # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - @uncountables += words.flatten.map(&:downcase) + @uncountables.add(words) end # Specifies a humanized form of a string by a regular expression rule or @@ -185,7 +216,7 @@ module ActiveSupport def clear(scope = :all) case scope when :all - @plurals, @singulars, @uncountables, @humans = [], [], [], [] + @plurals, @singulars, @uncountables, @humans = [], [], Uncountables.new, [] else instance_variable_set "@#{scope}", [] end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index bde70d772d..f741c0bfb8 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,5 +1,3 @@ -# encoding: utf-8 - require 'active_support/inflections' module ActiveSupport @@ -68,9 +66,9 @@ module ActiveSupport def camelize(term, uppercase_first_letter = true) string = term.to_s if uppercase_first_letter - string = string.sub(/^[a-z\d]*/) { inflections.acronyms[$&] || $&.capitalize } + string = string.sub(/^[a-z\d]*/) { |match| inflections.acronyms[match] || match.capitalize } else - string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } + string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { |match| match.downcase } end string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } string.gsub!('/'.freeze, '::'.freeze) @@ -91,10 +89,10 @@ module ActiveSupport def underscore(camel_cased_word) return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ word = camel_cased_word.to_s.gsub('::'.freeze, '/'.freeze) - 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!("-", "_") + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }#{$2.downcase}" } + word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze) + word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze) + word.tr!("-".freeze, "_".freeze) word.downcase! word end @@ -127,9 +125,9 @@ module ActiveSupport inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.sub!(/\A_+/, '') - result.sub!(/_id\z/, '') - result.tr!('_', ' ') + result.sub!(/\A_+/, ''.freeze) + result.sub!(/_id\z/, ''.freeze) + result.tr!('_'.freeze, ' '.freeze) result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" @@ -153,14 +151,14 @@ module ActiveSupport # 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 } + humanize(underscore(word)).gsub(/\b(?<!['’`])[a-z]/) { |match| match.capitalize } end # 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. # # tableize('RawScaledScorer') # => "raw_scaled_scorers" - # tableize('egg_and_ham') # => "egg_and_hams" + # tableize('ham_and_egg') # => "ham_and_eggs" # tableize('fancyCategory') # => "fancy_categories" def tableize(class_name) pluralize(underscore(class_name)) @@ -170,22 +168,22 @@ module ActiveSupport # names to models. Note that this returns a string and not a Class (To # convert to an actual class follow +classify+ with #constantize). # - # classify('egg_and_hams') # => "EggAndHam" + # classify('ham_and_eggs') # => "HamAndEgg" # classify('posts') # => "Post" # # Singular names are not handled correctly: # - # classify('calculus') # => "Calculu" + # classify('calculus') # => "Calculus" def classify(table_name) # strip out any leading schema name - camelize(singularize(table_name.to_s.sub(/.*\./, ''))) + camelize(singularize(table_name.to_s.sub(/.*\./, ''.freeze))) end # Replaces underscores with dashes in the string. # # dasherize('puni_puni') # => "puni-puni" def dasherize(underscored_word) - underscored_word.tr('_', '-') + underscored_word.tr('_'.freeze, '-'.freeze) end # Removes the module part from the expression in the string. @@ -248,7 +246,7 @@ module ActiveSupport # NameError is raised when the name is not in CamelCase or the constant is # unknown. def constantize(camel_cased_word) - names = camel_cased_word.split('::') + names = camel_cased_word.split('::'.freeze) # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? @@ -354,7 +352,7 @@ module ActiveSupport # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: - parts = camel_cased_word.split("::") + parts = camel_cased_word.split("::".freeze) return Regexp.escape(camel_cased_word) if parts.blank? @@ -372,7 +370,7 @@ module ActiveSupport def apply_inflections(word, rules) result = word.to_s.dup - if word.empty? || inflections.uncountables.include?(result.downcase[/\b\w+\Z/]) + if word.empty? || inflections.uncountables.uncountable?(result) result else rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) } diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index edea142e82..871cfb8a72 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/core_ext/string/multibyte' require 'active_support/i18n' @@ -58,7 +57,7 @@ module ActiveSupport # I18n.locale = :de # transliterate('Jürgen') # # => "Juergen" - def transliterate(string, replacement = "?") + def transliterate(string, replacement = "?".freeze) I18n.transliterate(ActiveSupport::Multibyte::Unicode.normalize( ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c), :replacement => replacement) @@ -69,19 +68,45 @@ module ActiveSupport # # parameterize("Donald E. Knuth") # => "donald-e-knuth" # parameterize("^trés|Jolie-- ") # => "tres-jolie" - def parameterize(string, sep = '-') - # replace accented chars with their ascii equivalents + # + # To use a custom separator, override the `separator` argument. + # + # parameterize("Donald E. Knuth", separator: '_') # => "donald_e_knuth" + # parameterize("^trés|Jolie-- ", separator: '_') # => "tres_jolie" + # + # To preserve the case of the characters in a string, use the `preserve_case` argument. + # + # parameterize("Donald E. Knuth", preserve_case: true) # => "Donald-E-Knuth" + # parameterize("^trés|Jolie-- ", preserve_case: true) # => "tres-Jolie" + # + def parameterize(string, sep = :unused, separator: '-', preserve_case: false) + unless sep == :unused + ActiveSupport::Deprecation.warn("Passing the separator argument as a positional parameter is deprecated and will soon be removed. Use `separator: '#{sep}'` instead.") + separator = sep + end + # Replace accented chars with their ASCII equivalents. parameterized_string = transliterate(string) - # Turn unwanted chars into the separator - parameterized_string.gsub!(/[^a-z0-9\-_]+/i, sep) - unless sep.nil? || sep.empty? - re_sep = Regexp.escape(sep) + + # Turn unwanted chars into the separator. + parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator) + + unless separator.nil? || separator.empty? + if separator == "-".freeze + re_duplicate_separator = /-{2,}/ + re_leading_trailing_separator = /^-|-$/i + else + re_sep = Regexp.escape(separator) + re_duplicate_separator = /#{re_sep}{2,}/ + re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i + end # No more than one of the separator in a row. - parameterized_string.gsub!(/#{re_sep}{2,}/, sep) + parameterized_string.gsub!(re_duplicate_separator, separator) # Remove leading/trailing separator. - parameterized_string.gsub!(/^#{re_sep}|#{re_sep}$/i, '') + parameterized_string.gsub!(re_leading_trailing_separator, ''.freeze) end - parameterized_string.downcase + + parameterized_string.downcase! unless preserve_case + parameterized_string end end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 48f4967892..031c5e9339 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -57,6 +57,10 @@ module ActiveSupport super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS end end + + def to_s + self + end end # Mark these as private so we don't leak encoding-specific constructs diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 51d2da3a79..7f73f9ddfc 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -1,8 +1,8 @@ -require 'thread_safe' +require 'concurrent/map' require 'openssl' module ActiveSupport - # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2 + # KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2. # It can be used to derive a number of keys for various purposes from a given secret. # This lets Rails applications have a single secure secret, but avoid reusing that # key in multiple incompatible contexts. @@ -24,11 +24,11 @@ module ActiveSupport # CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid # re-executing the key generation process when it's called using the same salt and - # key_size + # key_size. class CachingKeyGenerator def initialize(key_generator) @key_generator = key_generator - @cache_keys = ThreadSafe::Cache.new + @cache_keys = Concurrent::Map.new end # Returns a derived key suitable for use. The default key_size is chosen diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index a4563ace8f..c64b7598ee 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -106,6 +106,8 @@ en: mb: "MB" gb: "GB" tb: "TB" + pb: "PB" + eb: "EB" # Used in NumberHelper.number_to_human() decimal_units: format: "%n %u" diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index e95dc5a866..e782cd2d4b 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -95,7 +95,7 @@ module ActiveSupport METHOD end - # Set color by using a string or one of the defined constants. If a third + # Set color by using a symbol or one of the defined constants. If a third # option is set to +true+, it also adds bold to the string. This is based # on the Highline implementation and will automatically append CLEAR to the # end of the returned String. diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 75f353f62c..588ed67c81 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -10,7 +10,7 @@ module ActiveSupport # class SyncLogSubscriberTest < ActiveSupport::TestCase # include ActiveSupport::LogSubscriber::TestHelper # - # def setup + # setup do # ActiveRecord::LogSubscriber.attach_to(:active_record) # end # @@ -33,7 +33,7 @@ module ActiveSupport # you can collect them doing @logger.logged(level), where level is the level # used in logging, like info, debug, warn and so on. module TestHelper - def setup + def setup # :nodoc: @logger = MockLogger.new @notifier = ActiveSupport::Notifications::Fanout.new @@ -44,7 +44,7 @@ module ActiveSupport ActiveSupport::Notifications.notifier = @notifier end - def teardown + def teardown # :nodoc: set_logger(nil) ActiveSupport::Notifications.notifier = @old_notifier end diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 33fccdcf95..7626b28108 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/module/attribute_accessors' require 'active_support/logger_silence' require 'logger' @@ -6,6 +5,17 @@ module ActiveSupport class Logger < ::Logger include LoggerSilence + # Returns true if the logger destination matches one of the sources + # + # logger = Logger.new(STDOUT) + # ActiveSupport::Logger.logger_outputs_to?(logger, STDOUT) + # # => true + def self.logger_outputs_to?(logger, *sources) + logdev = logger.instance_variable_get("@logdev") + logger_source = logdev.dev if logdev.respond_to?(:dev) + sources.any? { |source| source == logger_source } + end + # Broadcasts logs to multiple loggers. def self.broadcast(logger) # :nodoc: Module.new do @@ -44,6 +54,20 @@ module ActiveSupport def initialize(*args) super @formatter = SimpleFormatter.new + after_initialize if respond_to? :after_initialize + end + + def add(severity, message = nil, progname = nil, &block) + return true if @logdev.nil? || (severity || UNKNOWN) < level + super + end + + Logger::Severity.constants.each do |severity| + class_eval(<<-EOT, __FILE__, __LINE__ + 1) + def #{severity.downcase}? # def debug? + Logger::#{severity} >= level # DEBUG >= level + end # end + EOT end # Simple formatter which only displays the message. diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb index a8efdef944..125d81d973 100644 --- a/activesupport/lib/active_support/logger_silence.rb +++ b/activesupport/lib/active_support/logger_silence.rb @@ -1,21 +1,42 @@ require 'active_support/concern' +require 'active_support/core_ext/module/attribute_accessors' +require 'concurrent' module LoggerSilence extend ActiveSupport::Concern - + included do cattr_accessor :silencer + attr_reader :local_levels self.silencer = true end + def after_initialize + @local_levels = Concurrent::Map.new(:initial_capacity => 2) + end + + def local_log_id + Thread.current.__id__ + end + + def level + local_levels[local_log_id] || super + end + # Silences the logger for the duration of the block. def silence(temporary_level = Logger::ERROR) if silencer begin - old_logger_level, self.level = level, temporary_level + old_local_level = local_levels[local_log_id] + local_levels[local_log_id] = temporary_level + yield self ensure - self.level = old_logger_level + if old_local_level + local_levels[local_log_id] = old_local_level + else + local_levels.delete(local_log_id) + end end else yield self diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 92ab6fe648..2dde01c844 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -34,8 +34,8 @@ module ActiveSupport # Initialize a new MessageEncryptor. +secret+ must be at least as long as # the cipher key size. For the default 'aes-256-cbc' cipher, this is 256 # bits. If you are using a user-entered secret, you can generate a suitable - # key with <tt>OpenSSL::Digest::SHA256.new(user_secret).digest</tt> or - # similar. + # key by using <tt>ActiveSupport::KeyGenerator</tt> or a similar key + # derivation function. # # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by @@ -82,7 +82,7 @@ module ActiveSupport def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} + encrypted_data, iv = encrypted_message.split("--".freeze).map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index eee9bbaead..854029bf83 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -15,7 +15,7 @@ module ActiveSupport # In the authentication filter: # # id, time = @verifier.verify(cookies[:remember_me]) - # if time < Time.now + # if Time.now < time # self.current_user = User.find(id) # end # @@ -44,9 +44,9 @@ module ActiveSupport # 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? + return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank? - data, digest = signed_message.split("--") + data, digest = signed_message.split("--".freeze) data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) end @@ -74,7 +74,7 @@ module ActiveSupport def verified(signed_message) if valid_message?(signed_message) begin - data = signed_message.split("--")[0] + data = signed_message.split("--".freeze)[0] @serializer.load(decode(data)) rescue ArgumentError => argument_error return if argument_error.message =~ %r{invalid base64} diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index 3c0cf9f137..707cf200b5 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 require 'active_support/json' require 'active_support/core_ext/string/access' require 'active_support/core_ext/string/behavior' @@ -86,10 +85,20 @@ module ActiveSupport #:nodoc: @wrapped_string.split(*args).map { |i| self.class.new(i) } end - # Works like like <tt>String#slice!</tt>, but returns an instance of - # Chars, or nil if the string was not modified. + # Works like <tt>String#slice!</tt>, but returns an instance of + # Chars, or nil if the string was not modified. The string will not be + # modified if the range given is out of bounds + # + # string = 'Welcome' + # string.mb_chars.slice!(3) # => #<ActiveSupport::Multibyte::Chars:0x000000038109b8 @wrapped_string="c"> + # string # => 'Welome' + # string.mb_chars.slice!(0..3) # => #<ActiveSupport::Multibyte::Chars:0x00000002eb80a0 @wrapped_string="Welo"> + # string # => 'me' def slice!(*args) - chars(@wrapped_string.slice!(*args)) + string_sliced = @wrapped_string.slice!(*args) + if string_sliced + chars(string_sliced) + end end # Reverses all characters in the string. diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 35efebc65f..72b20fff06 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -1,4 +1,3 @@ -# encoding: utf-8 module ActiveSupport module Multibyte module Unicode @@ -11,7 +10,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '7.0.0' + UNICODE_VERSION = '8.0.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -58,7 +57,7 @@ module ActiveSupport # Returns a regular expression pattern that matches the passed Unicode # codepoints. def self.codepoints_to_pattern(array_of_codepoints) #:nodoc: - array_of_codepoints.collect{ |e| [e].pack 'U*' }.join('|') + array_of_codepoints.collect{ |e| [e].pack 'U*'.freeze }.join('|'.freeze) end TRAILERS_PAT = /(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+\Z/u LEADERS_PAT = /\A(#{codepoints_to_pattern(LEADERS_AND_TRAILERS)})+/u @@ -88,19 +87,44 @@ module ActiveSupport pos += 1 previous = codepoints[pos-1] current = codepoints[pos] - if ( - # CR X LF - ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or - # L X (L|V|LV|LVT) - ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or - # (LV|V) X (V|T) - ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or - # (LVT|T) X (T) - ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or - # X Extend - (database.boundary[:extend] === current) - ) - else + + should_break = + # GB3. CR X LF + if previous == database.boundary[:cr] and current == database.boundary[:lf] + false + # GB4. (Control|CR|LF) ÷ + elsif previous and in_char_class?(previous, [:control,:cr,:lf]) + true + # GB5. ÷ (Control|CR|LF) + elsif in_char_class?(current, [:control,:cr,:lf]) + true + # GB6. L X (L|V|LV|LVT) + elsif database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) + false + # GB7. (LV|V) X (V|T) + elsif in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) + false + # GB8. (LVT|T) X (T) + elsif in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current + false + # GB8a. Regional_Indicator X Regional_Indicator + elsif database.boundary[:regional_indicator] === previous and database.boundary[:regional_indicator] === current + false + # GB9. X Extend + elsif database.boundary[:extend] === current + false + # GB9a. X SpacingMark + elsif database.boundary[:spacingmark] === current + false + # GB9b. Prepend X + elsif database.boundary[:prepend] === previous + false + # GB10. Any ÷ Any + else + true + end + + if should_break unpacked << codepoints[marker..pos-1] marker = pos end @@ -257,7 +281,7 @@ module ActiveSupport # * <tt>string</tt> - The string to perform normalization on. # * <tt>form</tt> - The form you want to normalize in. Should be one of # the following: <tt>:c</tt>, <tt>:kc</tt>, <tt>:d</tt>, or <tt>:kd</tt>. - # Default is ActiveSupport::Multibyte.default_normalization_form. + # Default is ActiveSupport::Multibyte::Unicode.default_normalization_form. def normalize(string, form=nil) form ||= @default_normalization_form # See http://www.unicode.org/reports/tr15, Table 1 @@ -273,7 +297,7 @@ module ActiveSupport compose(reorder_characters(decompose(:compatibility, codepoints))) else raise ArgumentError, "#{form} is not a valid normalization variant", caller - end.pack('U*') + end.pack('U*'.freeze) end def downcase(string) @@ -338,7 +362,7 @@ module ActiveSupport end # Redefine the === method so we can write shorter rules for grapheme cluster breaks - @boundary.each do |k,_| + @boundary.each_key do |k| @boundary[k].instance_eval do def ===(other) detect { |i| i === other } ? true : false diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index b9f8e1ab2c..823d68e507 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -69,8 +69,8 @@ module ActiveSupport # is able to take the arguments as they come and provide an object-oriented # interface to that data. # - # It is also possible to pass an object as the second parameter passed to the - # <tt>subscribe</tt> method instead of a block: + # It is also possible to pass an object which responds to <tt>call</tt> method + # as the second parameter to the <tt>subscribe</tt> method instead of a block: # # module ActionController # class PageRequest diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 6bf8c7d5de..c53f9c1039 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -1,5 +1,5 @@ require 'mutex_m' -require 'thread_safe' +require 'concurrent/map' module ActiveSupport module Notifications @@ -12,7 +12,7 @@ module ActiveSupport def initialize @subscribers = [] - @listeners_for = ThreadSafe::Cache.new + @listeners_for = Concurrent::Map.new super end @@ -42,8 +42,8 @@ module ActiveSupport listeners_for(name).each { |s| s.start(name, id, payload) } end - def finish(name, id, payload) - listeners_for(name).each { |s| s.finish(name, id, payload) } + def finish(name, id, payload, listeners = listeners_for(name)) + listeners.each { |s| s.finish(name, id, payload) } end def publish(name, *args) @@ -51,7 +51,7 @@ module ActiveSupport end def listeners_for(name) - # this is correctly done double-checked locking (ThreadSafe::Cache's lookups have volatile semantics) + # this is correctly done double-checked locking (Concurrent::Map's lookups have volatile semantics) @listeners_for[name] || synchronize do # use synchronisation when accessing @subscribers @listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) } @@ -111,7 +111,7 @@ module ActiveSupport end end - class Timed < Evented + class Timed < Evented # :nodoc: def publish(name, *args) @delegate.call name, *args end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 075ddc2382..91f94cb2d7 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -15,14 +15,16 @@ module ActiveSupport # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. def instrument(name, payload={}) - start name, payload + # some of the listeners might have state + listeners_state = start name, payload begin yield payload rescue Exception => e payload[:exception] = [e.class.name, e.message] + payload[:exception_object] = e raise e ensure - finish name, payload + finish_with_state listeners_state, name, payload end end @@ -36,6 +38,10 @@ module ActiveSupport @notifier.finish name, @id, payload end + def finish_with_state(listeners_state, name, payload) + @notifier.finish name, @id, payload, listeners_state + end + private def unique_id diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 258d9b34e1..64d9e71f37 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -47,6 +47,14 @@ module ActiveSupport # Formats a +number+ into a currency string (e.g., $13.65). You # can customize the format in the +options+ hash. # + # The currency unit and number formatting of the current locale will be used + # unless otherwise specified in the provided options. No currency conversion + # is performed. If the user is given a way to change their locale, they will + # also be able to change the relative value of the currency displayed with + # this helper. If your application will ever support multiple locales, you + # may want to specify a constant <tt>:locale</tt> option or consider + # using a library capable of currency conversion. + # # ==== Options # # * <tt>:locale</tt> - Sets the locale to be used for formatting @@ -115,10 +123,10 @@ module ActiveSupport # number_to_percentage(100, precision: 0) # => 100% # 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, locale: :fr) # => 1000,000% + # number_to_percentage(1000, precision: nil) # => 1000% # number_to_percentage('98a') # => 98a% - # number_to_percentage(100, format: '%n %') # => 100 % + # number_to_percentage(100, format: '%n %') # => 100.000 % def number_to_percentage(number, options = {}) NumberToPercentageConverter.convert(number, options) end @@ -135,6 +143,9 @@ module ActiveSupport # to ","). # * <tt>:separator</tt> - Sets the separator between the # fractional and integer digits (defaults to "."). + # * <tt>:delimiter_pattern</tt> - Sets a custom regular expression used for + # deriving the placement of delimiter. Helpful when using currency formats + # like INR. # # ==== Examples # @@ -147,7 +158,10 @@ module ActiveSupport # number_to_delimited(12345678.05, locale: :fr) # => 12 345 678,05 # number_to_delimited('112a') # => 112a # number_to_delimited(98765432.98, delimiter: ' ', separator: ',') - # # => 98 765 432,98 + # # => 98 765 432,98 + # number_to_delimited("123456.78", + # delimiter_pattern: /(\d+?)(?=(\d\d)+(\d)(?!\d))/) + # # => 1,23,456.78 def number_to_delimited(number, options = {}) NumberToDelimitedConverter.convert(number, options) end @@ -220,8 +234,6 @@ module ActiveSupport # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes # insignificant zeros after the decimal separator (defaults to # +true+) - # * <tt>:prefix</tt> - If +:si+ formats the number using the SI - # prefix (defaults to :binary) # # ==== Examples # @@ -231,6 +243,8 @@ module ActiveSupport # number_to_human_size(1234567) # => 1.18 MB # number_to_human_size(1234567890) # => 1.15 GB # number_to_human_size(1234567890123) # => 1.12 TB + # number_to_human_size(1234567890123456) # => 1.1 PB + # number_to_human_size(1234567890123456789) # => 1.07 EB # 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 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 index d85cc086d7..45ae8f1a93 100644 --- a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -3,7 +3,7 @@ module ActiveSupport class NumberToDelimitedConverter < NumberConverter #:nodoc: self.validate_float = true - DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ + DEFAULT_DELIMITER_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ def convert parts.join(options[:separator]) @@ -13,11 +13,16 @@ module ActiveSupport def parts left, right = number.to_s.split('.') - left.gsub!(DELIMITED_REGEX) do |digit_to_delimit| + left.gsub!(delimiter_pattern) do |digit_to_delimit| "#{digit_to_delimit}#{options[:delimiter]}" end [left, right].compact end + + def delimiter_pattern + options.fetch(:delimiter_pattern, DEFAULT_DELIMITER_REGEX) + 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 index 5c6fe2df83..7a1f8171c0 100644 --- a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -20,9 +20,11 @@ module ActiveSupport exponent = calculate_exponent(units) @number = number / (10 ** exponent) + until (rounded_number = NumberToRoundedConverter.convert(number, options)) != NumberToRoundedConverter.convert(1000, options) + @number = number / 1000.0 + exponent += 3 + end unit = determine_unit(units, exponent) - - rounded_number = NumberToRoundedConverter.convert(number, options) format.gsub('%n'.freeze, rounded_number).gsub('%u'.freeze, unit).strip 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 index ac0d20b454..a83b368b7f 100644 --- 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 @@ -1,12 +1,16 @@ module ActiveSupport module NumberHelper class NumberToHumanSizeConverter < NumberConverter #:nodoc: - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb, :pb, :eb] self.namespace = :human self.validate_float = true def convert + if opts.key?(:prefix) + ActiveSupport::Deprecation.warn('The :prefix option of `number_to_human_size` is deprecated and will be removed in Rails 5.1 with no replacement.') + end + @number = Float(number) # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 4680d5acb7..b1658f0f27 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -5,7 +5,7 @@ YAML.add_builtin_type("omap") do |type, val| end module ActiveSupport - # <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves + # DEPRECATED: <tt>ActiveSupport::OrderedHash</tt> implements a hash that preserves # insertion order. # # oh = ActiveSupport::OrderedHash.new diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index a33e2c58a9..53a55bd986 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -6,6 +6,7 @@ module ActiveSupport # h[:girl] = 'Mary' # h[:boy] # => 'John' # h[:girl] # => 'Mary' + # h[:dog] # => nil # # Using +OrderedOptions+, the above code could be reduced to: # @@ -14,6 +15,13 @@ module ActiveSupport # h.girl = 'Mary' # h.boy # => 'John' # h.girl # => 'Mary' + # h.dog # => nil + # + # To raise an exception when the value is blank, append a + # bang to the key name, like: + # + # h.dog! # => raises KeyError: key not found: :dog + # class OrderedOptions < Hash alias_method :_get, :[] # preserve the original #[] method protected :_get # make it protected @@ -31,7 +39,13 @@ module ActiveSupport if name_string.chomp!('=') self[name_string] = args.first else - self[name] + bangs = name_string.chomp!('!') + + if bangs + fetch(name_string.to_sym).presence || raise(KeyError.new("#{name_string} is blank.")) + else + self[name_string] + end end end diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index ca2e4d5625..88e2b12cc7 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -1,4 +1,9 @@ +require 'active_support/core_ext/module/delegation' + module ActiveSupport + # NOTE: This approach has been deprecated for end-user code in favor of thread_mattr_accessor and friends. + # Please use that approach instead. + # # This module is used to encapsulate access to thread local variables. # # Instead of polluting the thread locals namespace: @@ -43,9 +48,9 @@ module ActiveSupport 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| - instance.public_send(name, *a, &b) - end + # + # By letting #delegate handle it, we avoid an enclosure that'll capture args. + singleton_class.delegate name, to: :instance send(name, *args, &block) end diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index cd0fb51009..845788b669 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -26,7 +26,7 @@ module ActiveSupport unless zone_default raise 'Value assigned to config.time_zone not recognized. ' \ - 'Run "rake -D time" for a list of tasks for finding appropriate time zone names.' + 'Run "rake time:zones:all" for a time zone names list.' end Time.zone_default = zone_default diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 411dd29df5..29305e0082 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/object/blank' module ActiveSupport module Testing module Assertions - # Assert that an expression is not truthy. Passes if <tt>object</tt> is + # Asserts that an expression is not truthy. Passes if <tt>object</tt> is # +nil+ or +false+. "Truthy" means "considered true in a conditional" # like <tt>if foo</tt>. # @@ -28,7 +28,7 @@ module ActiveSupport # # An arbitrary expression is passed in and evaluated. # - # assert_difference 'assigns(:article).comments(:reload).size' do + # assert_difference 'Article.last.comments(:reload).size' do # post :create, params: { comment: {...} } # end # @@ -68,13 +68,15 @@ module ActiveSupport } before = exps.map(&:call) - yield + retval = yield expressions.zip(exps).each_with_index do |(code, e), i| error = "#{code.inspect} didn't change by #{difference}" error = "#{message}.\n#{error}" if message assert_equal(before[i] + difference, e.call, error) end + + retval end # Assertion that the numeric result of evaluating an expression is not diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb index 5aa5f46310..84c6b89340 100644 --- a/activesupport/lib/active_support/testing/autorun.rb +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -2,4 +2,11 @@ gem 'minitest' require 'minitest' -Minitest.autorun +if Minitest.respond_to?(:run_with_rails_extension) + unless Minitest.run_with_rails_extension + Minitest.run_with_autorun = true + Minitest.autorun + end +else + Minitest.autorun +end diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index 6c94c611b6..5dfa14eeba 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -3,8 +3,8 @@ require 'active_support/deprecation' module ActiveSupport module Testing module Deprecation #:nodoc: - def assert_deprecated(match = nil, &block) - result, warnings = collect_deprecations(&block) + def assert_deprecated(match = nil, deprecator = nil, &block) + result, warnings = collect_deprecations(deprecator, &block) assert !warnings.empty?, "Expected a deprecation warning within the block but received none" if match match = Regexp.new(Regexp.escape(match)) unless match.is_a?(Regexp) @@ -13,22 +13,23 @@ module ActiveSupport result end - def assert_not_deprecated(&block) - result, deprecations = collect_deprecations(&block) + def assert_not_deprecated(deprecator = nil, &block) + result, deprecations = collect_deprecations(deprecator, &block) assert deprecations.empty?, "Expected no deprecation warning within the block but received #{deprecations.size}: \n #{deprecations * "\n "}" result end - def collect_deprecations - old_behavior = ActiveSupport::Deprecation.behavior + def collect_deprecations(deprecator = nil) + deprecator ||= ActiveSupport::Deprecation + old_behavior = deprecator.behavior deprecations = [] - ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| + deprecator.behavior = Proc.new do |message, callstack| deprecations << message end result = yield [result, deprecations] ensure - ActiveSupport::Deprecation.behavior = old_behavior + deprecator.behavior = old_behavior end end end diff --git a/activesupport/lib/active_support/testing/file_fixtures.rb b/activesupport/lib/active_support/testing/file_fixtures.rb index 4c6a0801b8..affb84cda5 100644 --- a/activesupport/lib/active_support/testing/file_fixtures.rb +++ b/activesupport/lib/active_support/testing/file_fixtures.rb @@ -18,7 +18,7 @@ module ActiveSupport # Returns a +Pathname+ to the fixture file named +fixture_name+. # - # Raises ArgumentError if +fixture_name+ can't be found. + # Raises +ArgumentError+ if +fixture_name+ can't be found. def file_fixture(fixture_name) path = Pathname.new(File.join(file_fixture_path, fixture_name)) diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 1de0a19998..edf8b30a0a 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -41,7 +41,23 @@ module ActiveSupport pid = fork do read.close yield - write.puts [Marshal.dump(self.dup)].pack("m") + begin + if error? + failures.map! { |e| + begin + Marshal.dump e + e + rescue TypeError + ex = Exception.new e.message + ex.set_backtrace e.backtrace + Minitest::UnexpectedError.new ex + end + } + end + result = Marshal.dump(self.dup) + end + + write.puts [result].pack("m") exit! end diff --git a/activesupport/lib/active_support/testing/method_call_assertions.rb b/activesupport/lib/active_support/testing/method_call_assertions.rb new file mode 100644 index 0000000000..fccaa54f40 --- /dev/null +++ b/activesupport/lib/active_support/testing/method_call_assertions.rb @@ -0,0 +1,41 @@ +require 'minitest/mock' + +module ActiveSupport + module Testing + module MethodCallAssertions # :nodoc: + private + def assert_called(object, method_name, message = nil, times: 1, returns: nil) + times_called = 0 + + object.stub(method_name, proc { times_called += 1; returns }) { yield } + + error = "Expected #{method_name} to be called #{times} times, " \ + "but was called #{times_called} times" + error = "#{message}.\n#{error}" if message + assert_equal times, times_called, error + end + + def assert_called_with(object, method_name, args = [], returns: nil) + mock = Minitest::Mock.new + + if args.all? { |arg| arg.is_a?(Array) } + args.each { |arg| mock.expect(:call, returns, arg) } + else + mock.expect(:call, returns, args) + end + + object.stub(method_name, mock) { yield } + + mock.verify + end + + def assert_not_called(object, method_name, message = nil, &block) + assert_called(object, method_name, message, times: 0, &block) + end + + def stub_any_instance(klass, instance: klass.new) + klass.stub(:new, instance) { yield instance } + end + end + end +end diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index 3478b09423..fca0947c5b 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -39,7 +39,7 @@ module ActiveSupport end end - # Contain helpers that help you test passage of time. + # Contains helpers that help 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+, +Date.today+, and +DateTime.now+. diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index b0d7f3299f..79cc748cf5 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -1,3 +1,4 @@ +require 'active_support/duration' require 'active_support/values/time_zone' require 'active_support/core_ext/object/acts_like' @@ -13,7 +14,7 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00 - # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 + # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00 # Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # @@ -40,6 +41,9 @@ module ActiveSupport 'Time' end + PRECISIONS = Hash.new { |h, n| h[n] = "%FT%T.%#{n}N".freeze } + PRECISIONS[0] = '%FT%T'.freeze + include Comparable attr_reader :time_zone @@ -98,7 +102,7 @@ module ActiveSupport # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.now.utc? # => false def utc? - time_zone.name == 'UTC' + period.offset.abbreviation == :UTC || period.offset.abbreviation == :UCT end alias_method :gmt?, :utc? @@ -131,7 +135,7 @@ module ActiveSupport # 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" + # Time.zone.now.inspect # => "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 @@ -141,11 +145,7 @@ module ActiveSupport # # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) - 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')}" + "#{time.strftime(PRECISIONS[fraction_digits.to_i])}#{formatted_offset(true, 'Z'.freeze)}" end alias_method :iso8601, :xmlschema @@ -245,8 +245,9 @@ module ActiveSupport utc.future? end + # Returns +true+ if +other+ is equal to current object. def eql?(other) - utc.eql?(other) + other.eql?(utc) end def hash @@ -283,8 +284,8 @@ module ActiveSupport # 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 + # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now - 1000 # => Mon, 03 Nov 2014 00: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 @@ -293,8 +294,8 @@ module ActiveSupport # 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 + # now - 24.hours # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now - 1.day # => Sun, 02 Nov 2014 00:26:28 EDT -04:00 def -(other) if other.acts_like?(:time) to_time - other.to_time @@ -306,10 +307,48 @@ module ActiveSupport end end + # Subtracts an interval of time from the current object's time and returns + # the result as a new TimeWithZone object. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now.ago(1000) # => Mon, 03 Nov 2014 00:09:48 EST -05:00 + # + # If we're 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, <tt>time.ago(24.hours)</tt> will move back exactly 24 hours, + # while <tt>time.ago(1.day)</tt> will move back 23-25 hours, depending on + # the day. + # + # now.ago(24.hours) # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now.ago(1.day) # => Sun, 02 Nov 2014 00:26:28 EDT -04:00 def ago(other) since(-other) end + # Uses Date to provide precise Time calculations for years, months, and days + # according to the proleptic Gregorian calendar. The result is returned as a + # new TimeWithZone object. + # + # 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>. + # + # If advancing by a value of variable length (i.e., years, weeks, months, + # days), move forward from #time, otherwise move forward from #utc, for + # accuracy when moving across DST boundaries. + # + # 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.advance(seconds: 1) # => Sun, 02 Nov 2014 01:26:29 EDT -04:00 + # now.advance(minutes: 1) # => Sun, 02 Nov 2014 01:27:28 EDT -04:00 + # now.advance(hours: 1) # => Sun, 02 Nov 2014 01:26:28 EST -05:00 + # now.advance(days: 1) # => Mon, 03 Nov 2014 01:26:28 EST -05:00 + # now.advance(weeks: 1) # => Sun, 09 Nov 2014 01:26:28 EST -05:00 + # now.advance(months: 1) # => Tue, 02 Dec 2014 01:26:28 EST -05:00 + # now.advance(years: 1) # => Mon, 02 Nov 2015 01:26:28 EST -05:00 def advance(options) # If we're advancing a value of variable length (i.e., years, weeks, months, days), advance from #time, # otherwise advance from #utc, for accuracy when moving across DST boundaries @@ -328,6 +367,11 @@ module ActiveSupport EOV end + # Returns Array of parts of Time in sequence of + # [seconds, minutes, hours, day, month, year, weekday, yearday, dst?, zone]. + # + # now = Time.zone.now # => Tue, 18 Aug 2015 02:29:27 UTC +00:00 + # now.to_a # => [27, 29, 2, 18, 8, 2015, 2, 230, false, "UTC"] def to_a [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end @@ -357,11 +401,15 @@ module ActiveSupport utc.to_r end - # Return an instance of Time in the system timezone. + # Returns an instance of Time in the system timezone. def to_time utc.to_time end + # Returns an instance of DateTime with the timezone's UTC offset + # + # Time.zone.now.to_datetime # => Tue, 18 Aug 2015 02:32:20 +0000 + # Time.current.in_time_zone('Hawaii').to_datetime # => Mon, 17 Aug 2015 16:32:20 -1000 def to_datetime utc.to_datetime.new_offset(Rational(utc_offset, 86_400)) end @@ -377,6 +425,11 @@ module ActiveSupport end alias_method :kind_of?, :is_a? + # An instance of ActiveSupport::TimeWithZone is never blank + def blank? + false + end + def freeze period; utc; time # preload instance variables before freezing super diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 2699a064d7..7ca3592520 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,5 +1,5 @@ require 'tzinfo' -require 'thread_safe' +require 'concurrent/map' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' @@ -23,15 +23,9 @@ module ActiveSupport # config.time_zone = 'Eastern Time (US & Canada)' # end # - # Time.zone # => #<TimeZone:0x514834...> + # Time.zone # => #<ActiveSupport::TimeZone:0x514834...> # Time.zone.name # => "Eastern Time (US & Canada)" # Time.zone.now # => Sun, 18 May 2008 14:30:44 EDT -04:00 - # - # The version of TZInfo bundled with Active Support only includes the - # definitions necessary to support the zones defined by the TimeZone class. - # If you need to use zones that aren't defined by TimeZone, you'll need to - # install the TZInfo gem (if a recent version of the gem is installed locally, - # this will be used instead of the bundled version.) class TimeZone # Keys are Rails TimeZone names, values are TZInfo identifiers. MAPPING = { @@ -92,7 +86,8 @@ module ActiveSupport "Paris" => "Europe/Paris", "Amsterdam" => "Europe/Amsterdam", "Berlin" => "Europe/Berlin", - "Bern" => "Europe/Berlin", + "Bern" => "Europe/Zurich", + "Zurich" => "Europe/Zurich", "Rome" => "Europe/Rome", "Stockholm" => "Europe/Stockholm", "Vienna" => "Europe/Vienna", @@ -189,13 +184,13 @@ module ActiveSupport UTC_OFFSET_WITH_COLON = '%s%02d:%02d' UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') - @lazy_zones_map = ThreadSafe::Cache.new + @lazy_zones_map = Concurrent::Map.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" + # ActiveSupport::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 ? '-' : '+') @@ -285,8 +280,12 @@ module ActiveSupport end end - # Returns the offset of this time zone as a formatted string, of the - # format "+HH:MM". + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. + # + # zone = ActiveSupport::TimeZone['Central Time (US & Canada)'] + # zone.formatted_offset # => "-06:00" + # zone.formatted_offset(false) # => "-0600" def formatted_offset(colon=true, alternate_utc_string = nil) utc_offset == 0 && alternate_utc_string || self.class.seconds_to_utc_offset(utc_offset, colon) end @@ -384,7 +383,7 @@ module ActiveSupport time_now.utc.in_time_zone(self) end - # Return the current date in this time zone. + # Returns the current date in this time zone. def today tzinfo.now.to_date end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex 760be4c07a..dd2c178fb6 100644 --- a/activesupport/lib/active_support/values/unicode_tables.dat +++ b/activesupport/lib/active_support/values/unicode_tables.dat diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index 009ee4db90..df7b081993 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -78,6 +78,9 @@ module ActiveSupport ) end + attr_accessor :depth + self.depth = 100 + delegate :parse, :to => :backend def backend diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index f303daa1a7..94751bbc04 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -46,7 +46,7 @@ module ActiveSupport xml_string_reader = StringReader.new(data) xml_input_source = InputSource.new(xml_string_reader) doc = @dbf.new_document_builder.parse(xml_input_source) - merge_element!({CONTENT_KEY => ''}, doc.document_element) + merge_element!({CONTENT_KEY => ''}, doc.document_element, XmlMini.depth) end end @@ -58,9 +58,10 @@ module ActiveSupport # Hash to merge the converted element into. # element:: # XML element to merge into hash - def merge_element!(hash, element) + def merge_element!(hash, element, depth) + raise 'Document too deep!' if depth == 0 delete_empty(hash) - merge!(hash, element.tag_name, collapse(element)) + merge!(hash, element.tag_name, collapse(element, depth)) end def delete_empty(hash) @@ -71,14 +72,14 @@ module ActiveSupport # # element:: # The document element to be collapsed. - def collapse(element) + def collapse(element, depth) hash = get_attributes(element) child_nodes = element.child_nodes if child_nodes.length > 0 (0...child_nodes.length).each do |i| child = child_nodes.item(i) - merge_element!(hash, child) unless child.node_type == Node.TEXT_NODE + merge_element!(hash, child, depth - 1) unless child.node_type == Node.TEXT_NODE end merge_texts!(hash, element) unless empty_content?(element) hash diff --git a/activesupport/lib/active_support/xml_mini/rexml.rb b/activesupport/lib/active_support/xml_mini/rexml.rb index 5c7c78bf70..924ed72345 100644 --- a/activesupport/lib/active_support/xml_mini/rexml.rb +++ b/activesupport/lib/active_support/xml_mini/rexml.rb @@ -29,7 +29,7 @@ module ActiveSupport doc = REXML::Document.new(data) if doc.root - merge_element!({}, doc.root) + merge_element!({}, doc.root, XmlMini.depth) else raise REXML::ParseException, "The document #{doc.to_s.inspect} does not have a valid root" @@ -44,19 +44,20 @@ module ActiveSupport # Hash to merge the converted element into. # element:: # XML element to merge into hash - def merge_element!(hash, element) - merge!(hash, element.name, collapse(element)) + def merge_element!(hash, element, depth) + raise REXML::ParseException, "The document is too deep" if depth == 0 + merge!(hash, element.name, collapse(element, depth)) end # Actually converts an XML document element into a data structure. # # element:: # The document element to be collapsed. - def collapse(element) + def collapse(element, depth) hash = get_attributes(element) if element.has_elements? - element.each_element {|child| merge_element!(hash, child) } + element.each_element {|child| merge_element!(hash, child, depth - 1) } merge_texts!(hash, element) unless empty_content?(element) hash else |