diff options
Diffstat (limited to 'activesupport/lib/active_support')
178 files changed, 4679 insertions, 3778 deletions
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index f1aff8a8e3..d06f22ad5c 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -13,17 +13,17 @@ module ActiveSupport # can focus on the rest. # # bc = BacktraceCleaner.new - # bc.add_filter { |line| line.gsub(Rails.root, '') } - # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } - # bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems + # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix + # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems + # bc.clean(exception.backtrace) # perform the cleanup # # To reconfigure an existing BacktraceCleaner (like the default one in Rails) # and show as much data as possible, you can always call # <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the # backtrace to a pristine state. If you need to reconfigure an existing # BacktraceCleaner so that it does not filter or modify the paths of any lines - # of the backtrace, you can call BacktraceCleaner#remove_filters! These two - # methods will give you a completely untouched backtrace. + # 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. class BacktraceCleaner @@ -65,13 +65,16 @@ module ActiveSupport @silencers << block end - # Will remove all silencers, but leave in the filters. This is useful if - # your context of debugging suddenly expands as you suspect a bug in one of + # Removes all silencers, but leaves in the filters. Useful if your + # context of debugging suddenly expands as you suspect a bug in one of # the libraries you use. def remove_silencers! @silencers = [] end + # Removes all filters, but leaves in the silencers. Useful if you suddenly + # need to see entire filepaths in the backtrace that you had already + # filtered out. def remove_filters! @filters = [] end @@ -94,11 +97,7 @@ module ActiveSupport end def noise(backtrace) - @silencers.each do |s| - backtrace = backtrace.select { |line| s.call(line) } - end - - backtrace + backtrace - silence(backtrace) end end end diff --git a/activesupport/lib/active_support/basic_object.rb b/activesupport/lib/active_support/basic_object.rb deleted file mode 100644 index 91aac6db64..0000000000 --- a/activesupport/lib/active_support/basic_object.rb +++ /dev/null @@ -1,11 +0,0 @@ -require 'active_support/deprecation' -require 'active_support/proxy_object' - -module ActiveSupport - class BasicObject < ProxyObject # :nodoc: - def self.inherited(*) - ::ActiveSupport::Deprecation.warn 'ActiveSupport::BasicObject is deprecated! Use ActiveSupport::ProxyObject instead.' - super - end - end -end diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb index 6413502b53..805b7a714f 100644 --- a/activesupport/lib/active_support/benchmarkable.rb +++ b/activesupport/lib/active_support/benchmarkable.rb @@ -45,15 +45,5 @@ module ActiveSupport yield end end - - # Silence the logger during the execution of the block. - def silence - message = "ActiveSupport::Benchmarkable#silence is deprecated. It will be removed from Rails 4.1." - ActiveSupport::Deprecation.warn message - old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger - yield - ensure - logger.level = old_logger_level if logger - end end end diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb deleted file mode 100644 index 1cd0c2f790..0000000000 --- a/activesupport/lib/active_support/buffered_logger.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'active_support/deprecation' -require 'active_support/logger' - -module ActiveSupport - class BufferedLogger < Logger - - def initialize(*args) - self.class._deprecation_warning - super - end - - def self.inherited(*) - _deprecation_warning - super - end - - def self._deprecation_warning - ::ActiveSupport::Deprecation.warn 'ActiveSupport::BufferedLogger is deprecated! Use ActiveSupport::Logger instead.' - end - end -end diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index fdec2de1d5..dddd1e136e 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -3,20 +3,20 @@ require 'zlib' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/benchmark' -require 'active_support/core_ext/exception' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/string/inflections' +require 'active_support/deprecation' module ActiveSupport # See ActiveSupport::Cache::Store for documentation. module Cache - autoload :FileStore, 'active_support/cache/file_store' - autoload :MemoryStore, 'active_support/cache/memory_store' + autoload :FileStore, 'active_support/cache/file_store' + autoload :MemoryStore, 'active_support/cache/memory_store' autoload :MemCacheStore, 'active_support/cache/mem_cache_store' - autoload :NullStore, 'active_support/cache/null_store' + autoload :NullStore, 'active_support/cache/null_store' # These options mean something to all cache implementations. Individual cache # implementations may support additional options. @@ -57,16 +57,7 @@ module ActiveSupport case store when Symbol - store_class_name = store.to_s.camelize - store_class = - begin - require "active_support/cache/#{store}" - rescue LoadError => e - raise "Could not find cache store adapter for #{store} (#{e})" - else - ActiveSupport::Cache.const_get(store_class_name) - end - store_class.new(*parameters) + retrieve_store_class(store).new(*parameters) when nil ActiveSupport::Cache::MemoryStore.new else @@ -74,6 +65,18 @@ module ActiveSupport end end + # Expands out the +key+ argument into a key that can be used for the + # cache store. Optionally accepts a namespace, and all keys will be + # scoped within that namespace. + # + # If the +key+ argument provided is an array, or responds to +to_a+, then + # each of elements in the array will be turned into parameters/keys and + # concatenated into a single key. For example: + # + # expand_cache_key([:foo, :bar]) # => "foo/bar" + # expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar" + # + # The +key+ argument can also respond to +cache_key+ or +to_param+. def expand_cache_key(key, namespace = nil) expanded_cache_key = namespace ? "#{namespace}/" : "" @@ -86,15 +89,24 @@ module ActiveSupport end private + def retrieve_cache_key(key) + case + when key.respond_to?(:cache_key) then key.cache_key + when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param + when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) + else key.to_param + end.to_s + end - def retrieve_cache_key(key) - case - when key.respond_to?(:cache_key) then key.cache_key - when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param - when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a) - else key.to_param - end.to_s - end + # Obtains the specified cache store class, given the name of the +store+. + # Raises an error when the store class cannot be found. + def retrieve_store_class(store) + require "active_support/cache/#{store}" + rescue LoadError => e + raise "Could not find cache store adapter for #{store} (#{e})" + else + ActiveSupport::Cache.const_get(store.to_s.camelize) + end end # An abstract cache store class. There are multiple cache store @@ -141,7 +153,6 @@ module ActiveSupport # or +write+. To specify the threshold at which to compress values, set the # <tt>:compress_threshold</tt> option. The default threshold is 16K. class Store - cattr_accessor :logger, :instance_writer => true attr_reader :silence, :options @@ -168,14 +179,16 @@ module ActiveSupport @silence = previous_silence end - # Set to +true+ if cache stores should be instrumented. - # Default is +false+. + # :deprecated: def self.instrument=(boolean) - Thread.current[:instrument_cache_store] = boolean + ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument= is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." + true end + # :deprecated: def self.instrument - Thread.current[:instrument_cache_store] || false + ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it." + true end # Fetches data from the cache, using the given key. If there is data in @@ -216,15 +229,15 @@ module ActiveSupport # # Setting <tt>:race_condition_ttl</tt> is very useful in situations where # a cache entry is used very frequently and is under heavy load. If a - # cache expires and due to heavy load seven different processes will try + # cache expires and due to heavy load several different processes will try # to read data natively and then they all will try to write to cache. To # avoid that case the first process to find an expired cache entry will # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. # Yes, this process is extending the time for a stale value by another few # seconds. Because of extended life of the previous cache, other processes - # will continue to use slightly stale data for a just a big longer. In the + # will continue to use slightly stale data for a just a bit longer. In the # meantime that first process will go ahead and will write into cache the - # new value. After that all the processes will start getting new value. + # new value. After that all the processes will start getting the new value. # The key is to keep <tt>:race_condition_ttl</tt> small. # # If the process regenerating the entry errors out, the entry will be @@ -276,34 +289,14 @@ module ActiveSupport if block_given? options = merged_options(options) key = namespaced_key(name, options) - unless options[:force] - entry = instrument(:read, name, options) do |payload| - payload[:super_operation] = :fetch if payload - read_entry(key, options) - end - end - if entry && entry.expired? - race_ttl = options[:race_condition_ttl].to_i - if race_ttl && (Time.now - entry.expires_at <= race_ttl) - # When an entry has :race_condition_ttl defined, put the stale entry back into the cache - # for a brief period while the entry is begin recalculated. - entry.expires_at = Time.now + race_ttl - write_entry(key, entry, :expires_in => race_ttl * 2) - else - delete_entry(key, options) - end - entry = nil - end + + cached_entry = find_cached_entry(key, name, options) unless options[:force] + entry = handle_expired_entry(cached_entry, key, options) if entry - instrument(:fetch_hit, name, options) { |payload| } - entry.value + get_entry_value(entry, name, options) else - result = instrument(:generate, name, options) do |payload| - yield(name) - end - write(name, result, options) - result + save_block_result_to_cache(name, options) { |_name| yield _name } end else read(name, options) @@ -360,12 +353,40 @@ module ActiveSupport results end + # Fetches data from the cache, using the given keys. If there is data in + # the cache with the given keys, then that data is returned. Otherwise, + # the supplied block is called for each key for which there was no data, + # and the result will be written to the cache and returned. + # + # Options are passed to the underlying cache implementation. + # + # Returns a hash with the data for each of the names. For example: + # + # cache.write("bim", "bam") + # cache.fetch_multi("bim", "boom") { |key| key * 2 } + # # => { "bam" => "bam", "boom" => "boomboom" } + # + def fetch_multi(*names) + options = names.extract_options! + options = merged_options(options) + results = read_multi(*names, options) + + names.each_with_object({}) do |name, memo| + memo[name] = results.fetch(name) do + value = yield name + write(name, value, options) + value + end + end + end + # Writes the value to the cache, with the key. # # Options are passed to the underlying cache implementation. def write(name, value, options = nil) options = merged_options(options) - instrument(:write, name, options) do |payload| + + instrument(:write, name, options) do entry = Entry.new(value, options) write_entry(namespaced_key(name, options), entry, options) end @@ -376,19 +397,21 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def delete(name, options = nil) options = merged_options(options) - instrument(:delete, name) do |payload| + + instrument(:delete, name) do delete_entry(namespaced_key(name, options), options) end end - # Return +true+ if the cache contains an entry for the given key. + # Returns +true+ if the cache contains an entry for the given key. # # Options are passed to the underlying cache implementation. def exist?(name, options = nil) options = merged_options(options) - instrument(:exist?, name) do |payload| + + instrument(:exist?, name) do entry = read_entry(namespaced_key(name, options), options) - entry && !entry.expired? + (entry && !entry.expired?) || false end end @@ -431,7 +454,7 @@ module ActiveSupport # Clear the entire cache. Be careful with this method since it could # affect other processes if shared cache is being used. # - # Options are passed to the underlying cache implementation. + # The options hash is passed to the underlying cache implementation. # # All implementations may not support this method. def clear(options = nil) @@ -519,19 +542,52 @@ module ActiveSupport def instrument(operation, key, options = nil) log(operation, key, options) - if self.class.instrument - payload = { :key => key } - payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } - else - yield(nil) - end + payload = { :key => key } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } end def log(operation, key, options = nil) return unless logger && logger.debug? && !silence? logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}") 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 + if race_ttl && (Time.now.to_f - entry.expires_at <= race_ttl) + # When an entry has :race_condition_ttl defined, put the stale entry back into the cache + # for a brief period while the entry is begin recalculated. + entry.expires_at = Time.now + race_ttl + write_entry(key, entry, :expires_in => race_ttl * 2) + else + delete_entry(key, options) + end + entry = nil + end + entry + end + + def get_entry_value(entry, name, options) + instrument(:fetch_hit, name, options) { |payload| } + entry.value + end + + def save_block_result_to_cache(name, options) + result = instrument(:generate, name, options) do |payload| + yield(name) + end + + write(name, result, options) + result + end end # This class is used to represent cache entries. Cache entries have a value and an optional @@ -547,38 +603,37 @@ module ActiveSupport # +:compress+, +:compress_threshold+, and +:expires_in+. def initialize(value, options = {}) if should_compress?(value, options) - @v = compress(value) - @c = true + @value = compress(value) + @compressed = true else - @v = value - end - if expires_in = options[:expires_in] - @x = (Time.now + expires_in).to_i + @value = value end + + @created_at = Time.now.to_f + @expires_in = options[:expires_in] + @expires_in = @expires_in.to_f if @expires_in end def value - convert_version_3_entry! if defined?(@value) - compressed? ? uncompress(@v) : @v + compressed? ? uncompress(@value) : @value end # Check if the entry is expired. The +expires_in+ parameter can override # the value set when the entry was created. def expired? - convert_version_3_entry! if defined?(@value) - if defined?(@x) - @x && @x < Time.now.to_i - else - false - end + @expires_in && @created_at + @expires_in <= Time.now.to_f end def expires_at - Time.at(@x) if defined?(@x) + @expires_in ? @created_at + @expires_in : nil end def expires_at=(value) - @x = value.to_i + if value + @expires_in = value.to_f - @created_at + else + @expires_in = nil + end end # Returns the size of the cached value. This could be less than @@ -591,9 +646,9 @@ module ActiveSupport when NilClass 0 when String - @v.bytesize + @value.bytesize else - @s = Marshal.dump(@v).bytesize + @s = Marshal.dump(@value).bytesize end end end @@ -601,12 +656,11 @@ module ActiveSupport # Duplicate the value in a class. This is used by cache implementations that don't natively # serialize entries to protect against accidental cache modifications. def dup_value! - convert_version_3_entry! if defined?(@value) - if @v && !compressed? && !(@v.is_a?(Numeric) || @v == true || @v == false) - if @v.is_a?(String) - @v = @v.dup + if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) + if @value.is_a?(String) + @value = @value.dup else - @v = Marshal.load(Marshal.dump(@v)) + @value = Marshal.load(Marshal.dump(@value)) end end end @@ -616,13 +670,15 @@ module ActiveSupport if value && options[:compress] compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize + return true if serialized_value_size >= compress_threshold end + false end def compressed? - defined?(@c) ? @c : false + defined?(@compressed) ? @compressed : false end def compress(value) @@ -632,24 +688,6 @@ module ActiveSupport def uncompress(value) Marshal.load(Zlib::Inflate.inflate(value)) end - - # The internals of this method changed between Rails 3.x and 4.0. This method provides the glue - # to ensure that cache entries created under the old version still work with the new class definition. - def convert_version_3_entry! - if defined?(@value) - @v = @value - remove_instance_variable(:@value) - end - if defined?(@compressed) - @c = @compressed - remove_instance_variable(:@compressed) - end - if defined?(@expires_in) && defined?(@created_at) && @expires_in && @created_at - @x = (@created_at + @expires_in).to_i - remove_instance_variable(:@created_at) - remove_instance_variable(:@expires_in) - end - end end end end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 8e265ad863..d08ecd2f7d 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -14,6 +14,7 @@ module ActiveSupport DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) + FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room EXCLUDED_DIRS = ['.', '..'].freeze def initialize(cache_path, options = nil) @@ -22,45 +23,34 @@ module ActiveSupport extend Strategy::LocalCache end + # Deletes all items from the cache. In this case it deletes all the entries in the specified + # file store directory except for .gitkeep. Be careful which directory is specified in your + # config file when using +FileStore+ because everything in that directory will be deleted. def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) - each_key(options) do |key| + search_dir(cache_path) do |fname| + key = file_path_key(fname) entry = read_entry(key, options) delete_entry(key, options) if entry && entry.expired? end end + # Increments an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. def increment(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end + # Decrements an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. def decrement(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -88,6 +78,7 @@ module ActiveSupport def write_entry(key, entry, options) file_name = key_file_path(key) + return false if options[:unless_exist] && File.exist?(file_name) ensure_cache_path(File.dirname(file_name)) File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)} true @@ -127,6 +118,10 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) + if key.size > FILEPATH_MAX_SIZE + key = Digest::MD5.hexdigest(key) + end + fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) @@ -150,9 +145,9 @@ module ActiveSupport # Delete empty directories in the cache. def delete_empty_directories(dir) - return if dir == cache_path + return if File.realpath(dir) == File.realpath(cache_path) if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty? - File.delete(dir) rescue nil + Dir.delete(dir) rescue nil delete_empty_directories(File.dirname(dir)) end end @@ -174,6 +169,22 @@ module ActiveSupport end end end + + # Modifies the amount of an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. + def modify_value(name, amount, options) + file_name = key_file_path(namespaced_key(name, options)) + + lock_file(file_name) do + options = merged_options(options) + + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + end + end + end end end end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 712db2c75a..61b4f0b8b0 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -7,6 +7,7 @@ end require 'digest/md5' require 'active_support/core_ext/marshal' +require 'active_support/core_ext/array/extract_options' module ActiveSupport module Cache @@ -40,17 +41,15 @@ module ActiveSupport # # If no addresses are specified, then MemCacheStore will connect to # localhost port 11211 (the default memcached port). - # - # Instead of addresses one can pass in a MemCache-like object. For example: - # - # require 'memcached' # gem install memcached; uses C bindings to libmemcached - # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211")) def initialize(*addresses) addresses = addresses.flatten options = addresses.extract_options! super(options) - if addresses.first.respond_to?(:get) + unless [String, Dalli::Client, NilClass].include?(addresses.first.class) + raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance." + end + if addresses.first.is_a?(Dalli::Client) @data = addresses.first else mem_cache_options = options.dup @@ -86,7 +85,7 @@ module ActiveSupport instrument(:increment, name, :amount => amount) do @data.incr(escape_key(namespaced_key(name, options)), amount) end - rescue Dalli::DalliError + rescue Dalli::DalliError => e logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -100,7 +99,7 @@ module ActiveSupport instrument(:decrement, name, :amount => amount) do @data.decr(escape_key(namespaced_key(name, options)), amount) end - rescue Dalli::DalliError + rescue Dalli::DalliError => e logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -158,7 +157,7 @@ module ActiveSupport # characters properly. def escape_key(key) key = key.to_s.dup - key = key.force_encoding("BINARY") + 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 diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 4d26fb7e42..8a0523d0e2 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -36,6 +36,7 @@ module ActiveSupport end end + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) instrument(:cleanup, :size => @data.size) do @@ -122,6 +123,13 @@ module ActiveSupport end protected + + PER_ENTRY_OVERHEAD = 240 + + def cached_size(key, entry) + key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD + end + def read_entry(key, options) # :nodoc: entry = @data[key] synchronize do @@ -139,8 +147,11 @@ module ActiveSupport synchronize do old_entry = @data[key] return false if @data.key?(key) && options[:unless_exist] - @cache_size -= old_entry.size if old_entry - @cache_size += entry.size + if old_entry + @cache_size -= (old_entry.size - entry.size) + else + @cache_size += cached_size(key, entry) + end @key_access[key] = Time.now.to_f @data[key] = entry prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size @@ -152,7 +163,7 @@ module ActiveSupport synchronize do @key_access.delete(key) entry = @data.delete(key) - @cache_size -= entry.size if entry + @cache_size -= cached_size(key, entry) if entry !!entry end end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index db5f228a70..73c6b3cb88 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/string/inflections' +require 'active_support/per_thread_registry' module ActiveSupport module Cache @@ -8,6 +9,28 @@ module ActiveSupport # duration of a block. Repeated calls to the cache for the same key will hit the # in-memory cache for faster access. module LocalCache + autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware' + + # Class for storing and registering the local caches. + class LocalCacheRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + def initialize + @registry = {} + end + + def cache_for(local_cache_key) + @registry[local_cache_key] + end + + def set_cache_for(local_cache_key, value) + @registry[local_cache_key] = value + end + + def self.set_cache_for(l, v); instance.set_cache_for l, v; end + def self.cache_for(l); instance.cache_for l; end + end + # Simple memory backed cache. This cache is not thread safe and is intended only # for serving as a temporary memory cache for a single thread. class LocalStore < Store @@ -41,46 +64,14 @@ module ActiveSupport # Use a local cache for the duration of block. def with_local_cache - save_val = Thread.current[thread_local_key] - begin - Thread.current[thread_local_key] = LocalStore.new - yield - ensure - Thread.current[thread_local_key] = save_val - end - end - - #-- - # This class wraps up local storage for middlewares. Only the middleware method should - # construct them. - class Middleware # :nodoc: - attr_reader :name, :thread_local_key - - def initialize(name, thread_local_key) - @name = name - @thread_local_key = thread_local_key - @app = nil - end - - def new(app) - @app = app - self - end - - def call(env) - Thread.current[thread_local_key] = LocalStore.new - @app.call(env) - ensure - Thread.current[thread_local_key] = nil - end + use_temporary_local_cache(LocalStore.new) { yield } end - # Middleware class can be inserted as a Rack handler to be local cache for the # duration of request. def middleware @middleware ||= Middleware.new( "ActiveSupport::Cache::Strategy::LocalCache", - thread_local_key) + local_cache_key) end def clear(options = nil) # :nodoc: @@ -95,29 +86,13 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end - end - end + set_cache_value(value, name, amount, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - if local_cache - local_cache.mute do - if value - local_cache.write(name, value, options) - else - local_cache.delete(name, options) - end - end - end + set_cache_value(value, name, amount, options) value end @@ -145,22 +120,39 @@ module ActiveSupport super end + def set_cache_value(value, name, amount, options) + if local_cache + local_cache.mute do + if value + local_cache.write(name, value, options) + else + local_cache.delete(name, options) + end + end + end + end + private - def thread_local_key - @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym + + def local_cache_key + @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym end def local_cache - Thread.current[thread_local_key] + LocalCacheRegistry.cache_for(local_cache_key) end def bypass_local_cache - save_cache = Thread.current[thread_local_key] + use_temporary_local_cache(nil) { yield } + end + + def use_temporary_local_cache(temporary_cache) + save_cache = LocalCacheRegistry.cache_for(local_cache_key) begin - Thread.current[thread_local_key] = nil + LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache) yield ensure - Thread.current[thread_local_key] = save_cache + LocalCacheRegistry.set_cache_for(local_cache_key, save_cache) end end end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb new file mode 100644 index 0000000000..a6f24b1a3c --- /dev/null +++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb @@ -0,0 +1,44 @@ +require 'rack/body_proxy' +require 'rack/utils' + +module ActiveSupport + module Cache + module Strategy + module LocalCache + + #-- + # This class wraps up local storage for middlewares. Only the middleware method should + # construct them. + class Middleware # :nodoc: + attr_reader :name, :local_cache_key + + def initialize(name, local_cache_key) + @name = name + @local_cache_key = local_cache_key + @app = nil + end + + def new(app) + @app = app + self + end + + def call(env) + LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + end + response + rescue Rack::Utils::InvalidParameterError + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + [400, {}, []] + rescue Exception + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + raise + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index e3e1845868..d2911a254c 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -1,19 +1,20 @@ -require 'thread_safe' require 'active_support/concern' require 'active_support/descendants_tracker' +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 'thread' module ActiveSupport - # Callbacks are code hooks that are run at key points in an object's lifecycle. + # Callbacks are code hooks that are run at key points in an object's life cycle. # The typical use case is to have a base class define a set of callbacks # relevant to the other functionality it supplies, so that subclasses can # install callbacks that enhance or modify the base functionality without # needing to override or redefine methods of the base class. # # Mixing in this module allows you to define the events in the object's - # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+), + # life cycle that will support callbacks (via +ClassMethods.define_callbacks+), # set the instance methods, procs, or callback objects to be called (via # +ClassMethods.set_callback+), and run the installed callbacks at the # appropriate times (via +run_callbacks+). @@ -61,6 +62,8 @@ module ActiveSupport extend ActiveSupport::DescendantsTracker end + CALLBACK_FILTER_TYPES = [:before, :after, :around] + # Runs the callbacks for the given event. # # Calls the before and around callbacks in the order they were set, yields @@ -68,268 +71,492 @@ module ActiveSupport # order. # # If the callback chain was halted, returns +false+. Otherwise returns the - # result of the block, or +true+ if no block is given. + # result of the block, +nil+ if no callbacks have been set, or +true+ + # if callbacks have been set but no block is given. # # run_callbacks :save do # save # end def run_callbacks(kind, &block) - runner_name = self.class.__define_callbacks(kind, self) - send(runner_name, &block) + send "_run_#{kind}_callbacks", &block end private - # A hook invoked everytime a before callback is halted. + def _run_callbacks(callbacks, &block) + if callbacks.empty? + block.call if block + else + runner = callbacks.compile + e = Filters::Environment.new(self, false, nil, block) + runner.call(e).value + end + end + + # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. def halted_callback_hook(filter) end - class Callback #:nodoc:# - @@_callback_sequence = 0 - - attr_accessor :chain, :filter, :kind, :options, :klass, :raw_filter + module Conditionals # :nodoc: + class Value + def initialize(&block) + @block = block + end + def call(target, value); @block.call(value); end + end + end - def initialize(chain, filter, kind, options, klass) - @chain, @kind, @klass = chain, kind, klass - deprecate_per_key_option(options) - normalize_options!(options) + module Filters + Environment = Struct.new(:target, :halted, :value, :run_block) - @raw_filter, @options = filter, options - @filter = _compile_filter(filter) - recompile_options! + class End + def call(env) + block = env.run_block + env.value = !env.halted && (!block || block.call) + env + end end + ENDING = End.new + + class Before + def self.build(callback_sequence, user_callback, user_conditions, chain_config, filter) + halted_lambda = chain_config[:terminator] - def deprecate_per_key_option(options) - if options[:per_key] - raise NotImplementedError, ":per_key option is no longer supported. Use generic :if and :unless options instead." + if chain_config.key?(:terminator) && 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 + end end - end - def clone(chain, klass) - obj = super() - obj.chain = chain - obj.klass = klass - obj.options = @options.dup - obj.options[:if] = @options[:if].dup - obj.options[:unless] = @options[:unless].dup - obj - end + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions, halted_lambda, filter) + callback_sequence.before do |env| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + result = user_callback.call target, value + env.halted = halted_lambda.call(target, result) + if env.halted + target.send :halted_callback_hook, filter + end + end - def normalize_options!(options) - options[:if] = Array(options[:if]) - options[:unless] = Array(options[:unless]) - end + env + end + end + private_class_method :halting_and_conditional + + def self.halting(callback_sequence, user_callback, halted_lambda, filter) + callback_sequence.before do |env| + target = env.target + value = env.value + halted = env.halted + + unless halted + result = user_callback.call target, value + env.halted = halted_lambda.call(target, result) + if env.halted + target.send :halted_callback_hook, filter + end + end - def name - chain.name - end + env + 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 - def next_id - @@_callback_sequence += 1 + 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? + 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 + end + else + if user_conditions.any? + conditional callback_sequence, user_callback, user_conditions + else + simple callback_sequence, user_callback + end + end + end + + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.after do |env| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + + env + end + end + private_class_method :halting_and_conditional + + def self.halting(callback_sequence, user_callback) + callback_sequence.after do |env| + unless env.halted + user_callback.call env.target, env.value + end + + env + end + end + private_class_method :halting + + def self.conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.after do |env| + target = env.target + value = env.value + + if user_conditions.all? { |c| c.call(target, value) } + user_callback.call target, value + end + + env + end + end + private_class_method :conditional + + def self.simple(callback_sequence, user_callback) + callback_sequence.after do |env| + user_callback.call env.target, env.value + + env + end + end + private_class_method :simple + end + + class Around + def self.build(callback_sequence, user_callback, user_conditions, chain_config) + if chain_config.key?(:terminator) && 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) + end + end + + def self.halting_and_conditional(callback_sequence, user_callback, user_conditions) + callback_sequence.around do |env, &run| + target = env.target + value = env.value + halted = env.halted + + if !halted && user_conditions.all? { |c| c.call(target, value) } + user_callback.call(target, value) { + env = run.call env + env.value + } + + env + else + run.call env + end + end + end + private_class_method :halting_and_conditional + + def self.halting(callback_sequence, user_callback) + callback_sequence.around do |env, &run| + target = env.target + value = env.value + + if env.halted + run.call env + else + user_callback.call(target, value) { + env = run.call env + env.value + } + env + end + 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) { + env = run.call env + env.value + } + env + else + run.call env + 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) { + env = run.call env + env.value + } + env + end + end + private_class_method :simple end + end - def matches?(_kind, _filter) - @kind == _kind && @filter == _filter + class Callback #:nodoc:# + def self.build(chain, filter, kind, options) + new chain.name, filter, kind, options, chain.config end - def duplicates?(other) - matches?(other.kind, other.filter) + attr_accessor :kind, :name + attr_reader :chain_config + + def initialize(name, filter, kind, options, chain_config) + @chain_config = chain_config + @name = name + @kind = kind + @filter = filter + @key = compute_identifier filter + @if = Array(options[:if]) + @unless = Array(options[:unless]) end - def _update_filter(filter_options, new_options) - filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless) - filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if) + def filter; @key; end + def raw_filter; @filter; end + + def merge(chain, new_options) + options = { + :if => @if.dup, + :unless => @unless.dup + } + + options[:if].concat Array(new_options.fetch(:unless, [])) + options[:unless].concat Array(new_options.fetch(:if, [])) + + self.class.build chain, @filter, @kind, options end - def recompile!(_options) - deprecate_per_key_option(_options) - _update_filter(self.options, _options) + def matches?(_kind, _filter) + @kind == _kind && filter == _filter + end - recompile_options! + def duplicates?(other) + case @filter + when Symbol, String + matches?(other.kind, other.filter) + else + false + end end # Wraps code with filter - def apply(code) - case @kind + def apply(callback_sequence) + user_conditions = conditions_lambdas + user_callback = make_lambda @filter + + case kind when :before - <<-RUBY_EVAL - if !halted && #{@compiled_options} - # This double assignment is to prevent warnings in 1.9.3 as - # the `result` variable is not always used except if the - # terminator code refers to it. - result = result = #{@filter} - halted = (#{chain.config[:terminator]}) - if halted - halted_callback_hook(#{@raw_filter.inspect.inspect}) - end - end - #{code} - RUBY_EVAL + Filters::Before.build(callback_sequence, user_callback, user_conditions, chain_config, @filter) when :after - <<-RUBY_EVAL - #{code} - if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options} - #{@filter} - end - RUBY_EVAL + Filters::After.build(callback_sequence, user_callback, user_conditions, chain_config) when :around - name = define_conditional_callback - <<-RUBY_EVAL - #{name}(halted) do - #{code} - value - end - RUBY_EVAL + Filters::Around.build(callback_sequence, user_callback, user_conditions, chain_config) end end private - # Compile around filters with conditions into proxy methods - # that contain the conditions. - # - # For `set_callback :save, :around, :filter_name, if: :condition': - # - # def _conditional_callback_save_17 - # if condition - # filter_name do - # yield self - # end - # else - # yield self - # end - # end - def define_conditional_callback - name = "_conditional_callback_#{@kind}_#{next_id}" - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{name}(halted) - if #{@compiled_options} && !halted - #{@filter} do - yield self - end - else - yield self - end - end - RUBY_EVAL - name - end - - # Options support the same options as filters themselves (and support - # symbols, string, procs, and objects), so compile a conditional - # expression based on the options. - def recompile_options! - conditions = ["true"] - - unless options[:if].empty? - conditions << Array(_compile_filter(options[:if])) - end - - unless options[:unless].empty? - conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"} - end - - @compiled_options = conditions.flatten.join(" && ") + def invert_lambda(l) + lambda { |*args, &blk| !l.call(*args, &blk) } end # Filters support: # - # Arrays:: Used in conditions. This is used to specify - # multiple conditions. Used internally to - # merge conditions from skip_* filters. # Symbols:: A method to call. # Strings:: Some content to evaluate. # Procs:: A proc to call with the object. # Objects:: An object with a <tt>before_foo</tt> method on it to call. # - # All of these objects are compiled into methods and handled - # the same after this point: - # - # Arrays:: Merged together into a single filter. - # Symbols:: Already methods. - # Strings:: class_eval'ed into methods. - # Procs:: define_method'ed into methods. - # Objects:: - # a method is created that calls the before_foo method - # on the object. - def _compile_filter(filter) - method_name = "_callback_#{@kind}_#{next_id}" + # All of these objects are converted into a lambda and handled + # the same after this point. + def make_lambda(filter) case filter - when Array - filter.map {|f| _compile_filter(f)} when Symbol - filter + lambda { |target, _, &blk| target.send filter, &blk } when String - "(#{filter})" - when Proc - @klass.send(:define_method, method_name, &filter) - return method_name if filter.arity <= 0 + l = eval "lambda { |value| #{filter} }" + lambda { |target, value| target.instance_exec(value, &l) } + when Conditionals::Value then filter + when ::Proc + if filter.arity > 1 + return lambda { |target, _, &block| + raise ArgumentError unless block + target.instance_exec(target, block, &filter) + } + end - method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ") + if filter.arity <= 0 + lambda { |target, _| target.instance_exec(&filter) } + else + lambda { |target, _| target.instance_exec(target, &filter) } + end else - @klass.send(:define_method, "#{method_name}_object") { filter } - - _normalize_legacy_filter(kind, filter) - scopes = Array(chain.config[:scope]) - method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_") + scopes = Array(chain_config[:scope]) + method_to_call = scopes.map{ |s| public_send(s) }.join("_") - @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{method_name}(&blk) - #{method_name}_object.send(:#{method_to_call}, self, &blk) - end - RUBY_EVAL + lambda { |target, _, &blk| + filter.public_send method_to_call, target, &blk + } + end + end - method_name + def compute_identifier(filter) + case filter + when String, ::Proc + filter.object_id + else + filter end end - def _normalize_legacy_filter(kind, filter) - if !filter.respond_to?(kind) && filter.respond_to?(:filter) - message = "Filter object with #filter method is deprecated. Define method corresponding " \ - "to filter type (#before, #after or #around)." - ActiveSupport::Deprecation.warn message - filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{kind}(context, &block) filter(context, &block) end - RUBY_EVAL - elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around && !filter.respond_to?(:around) - message = "Filter object with #before and #after methods is deprecated. Define #around method instead." - ActiveSupport::Deprecation.warn message - def filter.around(context) - should_continue = before(context) - yield if should_continue - after(context) - end + def conditions_lambdas + @if.map { |c| make_lambda c } + + @unless.map { |c| invert_lambda make_lambda c } + end + end + + # Execute before and after filters in a sequence instead of + # chaining them with nested lambda calls, see: + # https://github.com/rails/rails/issues/18011 + class CallbackSequence + def initialize(&call) + @call = call + @before = [] + @after = [] + end + + def before(&before) + @before.unshift(before) + self + end + + def after(&after) + @after.push(after) + self + end + + def around(&around) + CallbackSequence.new do |*args| + around.call(*args) { + self.call(*args) + } end end + + def call(*args) + @before.each { |b| b.call(*args) } + value = @call.call(*args) + @after.each { |a| a.call(*args) } + value + end end # An Array with a compile method. - class CallbackChain < Array #:nodoc:# + class CallbackChain #:nodoc:# + include Enumerable + attr_reader :name, :config def initialize(name, config) @name = name @config = { - :terminator => "false", :scope => [ :kind ] - }.merge(config) + }.merge!(config) + @chain = [] + @callbacks = nil + @mutex = Mutex.new end - def compile - method = [] - method << "value = nil" - method << "halted = false" + def each(&block); @chain.each(&block); end + def index(o); @chain.index(o); end + def empty?; @chain.empty?; end - callbacks = "value = !halted && (!block_given? || yield)" - reverse_each do |callback| - callbacks = callback.apply(callbacks) - end - method << callbacks + def insert(index, o) + @callbacks = nil + @chain.insert(index, o) + end + + def delete(o) + @callbacks = nil + @chain.delete(o) + end + + def clear + @callbacks = nil + @chain.clear + self + end - method << "value" - method.join("\n") + def initialize_copy(other) + @callbacks = nil + @chain = other.chain.dup + @mutex = Mutex.new + end + + def compile + @callbacks || @mutex.synchronize do + final_sequence = CallbackSequence.new { |env| Filters::ENDING.call(env) } + @callbacks ||= @chain.reverse.inject(final_sequence) do |callback_sequence, callback| + callback.apply callback_sequence + end + end end def append(*callbacks) @@ -340,69 +567,43 @@ module ActiveSupport callbacks.each { |c| prepend_one(c) } end + protected + def chain; @chain; end + private def append_one(callback) + @callbacks = nil remove_duplicates(callback) - push(callback) + @chain.push(callback) end def prepend_one(callback) + @callbacks = nil remove_duplicates(callback) - unshift(callback) + @chain.unshift(callback) end def remove_duplicates(callback) - delete_if { |c| callback.duplicates?(c) } + @callbacks = nil + @chain.delete_if { |c| callback.duplicates?(c) } end - end module ClassMethods - - # This method defines callback chain method for the given kind - # if it was not yet defined. - # This generated method plays caching role. - def __define_callbacks(kind, object) #:nodoc: - name = __callback_runner_name(kind) - unless object.respond_to?(name, true) - str = object.send("_#{kind}_callbacks").compile - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def #{name}() #{str} end - protected :#{name} - RUBY_EVAL - end - name - end - - def __reset_runner(symbol) - name = __callback_runner_name(symbol) - undef_method(name) if method_defined?(name) - end - - def __callback_runner_name_cache - @__callback_runner_name_cache ||= ThreadSafe::Cache.new {|cache, kind| cache[kind] = __generate_callback_runner_name(kind) } - end - - def __generate_callback_runner_name(kind) - "_run__#{self.name.hash.abs}__#{kind}__callbacks" - end - - def __callback_runner_name(kind) - __callback_runner_name_cache[kind] + def normalize_callback_params(filters, block) # :nodoc: + type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before + options = filters.extract_options! + filters.unshift(block) if block + [type, filters, options.dup] end # This is used internally to append, prepend and skip callbacks to the # CallbackChain. - def __update_callbacks(name, filters = [], block = nil) #:nodoc: - type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before - options = filters.last.is_a?(Hash) ? filters.pop : {} - filters.unshift(block) if block - - ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target| - chain = target.send("_#{name}_callbacks") - yield target, chain.dup, type, filters, options - target.__reset_runner(name) + def __update_callbacks(name) #:nodoc: + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse_each do |target| + chain = target.get_callbacks name + yield target, chain.dup end end @@ -410,7 +611,7 @@ module ActiveSupport # # set_callback :save, :before, :before_meth # set_callback :save, :after, :after_meth, if: :condition - # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # # The second arguments indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This @@ -418,10 +619,10 @@ module ActiveSupport # # set_callback :save, :before_meth # - # The callback can specified as a symbol naming an instance method; as a + # The callback can be specified as a symbol naming an instance method; as a # proc, lambda, or block; as a string to be instance evaluated; or as an # object that responds to a certain method determined by the <tt>:scope</tt> - # argument to +define_callback+. + # argument to +define_callbacks+. # # If a proc, lambda, or block is given, its body is evaluated in the context # of the current object. It can also optionally accept the current object as @@ -442,16 +643,15 @@ module ActiveSupport # * <tt>:prepend</tt> - If +true+, the callback will be prepended to the # existing chain rather than appended. def set_callback(name, *filter_list, &block) - mapped = nil - - __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| - mapped ||= filters.map do |filter| - Callback.new(chain, filter, type, options.dup, self) - end + type, filters, options = normalize_callback_params(filter_list, block) + self_chain = get_callbacks name + mapped = filters.map do |filter| + Callback.build(self_chain, filter, type, options) + end + __update_callbacks(name) do |target, chain| options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped) - - target.send("_#{name}_callbacks=", chain) + target.set_callbacks name, chain end end @@ -463,39 +663,37 @@ module ActiveSupport # skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 } # end def skip_callback(name, *filter_list, &block) - __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options| + type, filters, options = normalize_callback_params(filter_list, block) + + __update_callbacks(name) do |target, chain| filters.each do |filter| filter = chain.find {|c| c.matches?(type, filter) } if filter && options.any? - new_filter = filter.clone(chain, self) + new_filter = filter.merge(chain, options) chain.insert(chain.index(filter), new_filter) - new_filter.recompile!(options) end chain.delete(filter) end - target.send("_#{name}_callbacks=", chain) + target.set_callbacks name, chain end end # Remove all set callbacks for the given event. - def reset_callbacks(symbol) - callbacks = send("_#{symbol}_callbacks") + def reset_callbacks(name) + callbacks = get_callbacks name ActiveSupport::DescendantsTracker.descendants(self).each do |target| - chain = target.send("_#{symbol}_callbacks").dup + chain = target.get_callbacks(name).dup callbacks.each { |c| chain.delete(c) } - target.send("_#{symbol}_callbacks=", chain) - target.__reset_runner(symbol) + target.set_callbacks name, chain end - self.send("_#{symbol}_callbacks=", callbacks.dup.clear) - - __reset_runner(symbol) + self.set_callbacks name, callbacks.dup.clear end - # Define sets of events in the object lifecycle that support callbacks. + # Define sets of events in the object life cycle that support callbacks. # # define_callbacks :validate # define_callbacks :initialize, :save, :destroy @@ -503,15 +701,17 @@ module ActiveSupport # ===== Options # # * <tt>:terminator</tt> - Determines when a before filter will halt the - # callback chain, preventing following callbacks from being called and - # the event from being triggered. This is a string to be eval'ed. The - # result of the callback is available in the +result+ variable. + # callback chain, preventing following before and around callbacks from + # being called and the event from being triggered. + # This should be a lambda to be executed. + # The current object and the return result of the callback will be called + # with the lambda. # - # define_callbacks :validate, terminator: 'result == false' + # define_callbacks :validate, terminator: ->(target, result) { result == false } # # In this example, if any before validate callbacks returns +false+, - # other callbacks are not executed. Defaults to +false+, meaning no value - # halts the chain. + # any successive before and around callback is not executed. + # Defaults to +false+, meaning no value halts the chain. # # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By @@ -562,13 +762,33 @@ module ActiveSupport # define_callbacks :save, scope: [:name] # # would call <tt>Audit#save</tt>. - def define_callbacks(*callbacks) - config = callbacks.last.is_a?(Hash) ? callbacks.pop : {} - callbacks.each do |callback| - class_attribute "_#{callback}_callbacks" - send("_#{callback}_callbacks=", CallbackChain.new(callback, config)) + # + # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # `!`, `?` or `=`. + def define_callbacks(*names) + options = names.extract_options! + + 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) + send "_#{name}_callbacks" + end + + def set_callbacks(name, callbacks) + send "_#{name}_callbacks=", callbacks + end end end end diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index eeeba60839..342d3a9d52 100644 --- a/activesupport/lib/active_support/concern.rb +++ b/activesupport/lib/active_support/concern.rb @@ -26,7 +26,7 @@ module ActiveSupport # scope :disabled, -> { where(disabled: true) } # end # - # module ClassMethods + # class_methods do # ... # end # end @@ -95,32 +95,48 @@ module ActiveSupport # end # # class Host - # include Bar # works, Bar takes care now of its dependencies + # include Bar # It works, now Bar takes care of its dependencies # end module Concern + class MultipleIncludedBlocks < StandardError #:nodoc: + def initialize + super "Cannot define multiple 'included' blocks for a Concern" + end + end + def self.extended(base) #:nodoc: - base.instance_variable_set("@_dependencies", []) + base.instance_variable_set(:@_dependencies, []) end def append_features(base) - if base.instance_variable_defined?("@_dependencies") - base.instance_variable_get("@_dependencies") << self + if base.instance_variable_defined?(:@_dependencies) + base.instance_variable_get(:@_dependencies) << self return false else return false if base < self @_dependencies.each { |dep| base.send(:include, dep) } super - base.extend const_get("ClassMethods") if const_defined?("ClassMethods") - base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block") + base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods) + base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block) end end def included(base = nil, &block) if base.nil? + raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block) + @_included_block = block else super end end + + def class_methods(&class_methods_module_definition) + mod = const_defined?(:ClassMethods) ? + const_get(:ClassMethods) : + const_set(:ClassMethods, Module.new) + + mod.module_eval(&class_methods_module_definition) + end end end diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index e0d39d509f..3dd44e32d8 100644 --- a/activesupport/lib/active_support/configurable.rb +++ b/activesupport/lib/active_support/configurable.rb @@ -107,7 +107,7 @@ module ActiveSupport options = names.extract_options! names.each do |name| - raise NameError.new('invalid config attribute name') unless name =~ /^[_A-Za-z]\w*$/ + raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/ reader, reader_line = "def #{name}; config.#{name}; end", __LINE__ writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__ diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb index b48bdf08e8..199aa91020 100644 --- a/activesupport/lib/active_support/core_ext.rb +++ b/activesupport/lib/active_support/core_ext.rb @@ -1,4 +1,3 @@ -Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path| - next if File.basename(path, '.rb') == 'logger' - require "active_support/core_ext/#{File.basename(path, '.rb')}" +Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path| + require path end diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 79ba79192a..7d0c1e4c8d 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -1,6 +1,5 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/array/access' -require 'active_support/core_ext/array/uniq_by' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index a8f9dddae5..45b89d2705 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -5,6 +5,8 @@ class Array # %w( a b c d ).from(2) # => ["c", "d"] # %w( a b c d ).from(10) # => [] # %w().from(0) # => [] + # %w( a b c d ).from(-2) # => ["c", "d"] + # %w( a b c ).from(-10) # => [] def from(position) self[position, length] || [] end @@ -15,39 +17,47 @@ class Array # %w( a b c d ).to(2) # => ["a", "b", "c"] # %w( a b c d ).to(10) # => ["a", "b", "c", "d"] # %w().to(0) # => [] + # %w( a b c d ).to(-2) # => ["a", "b", "c"] + # %w( a b c ).to(-10) # => [] def to(position) - first position + 1 + if position >= 0 + first position + 1 + else + self[0..position] + end end # Equal to <tt>self[1]</tt>. # - # %w( a b c d e).second # => "b" + # %w( a b c d e ).second # => "b" def second self[1] end # Equal to <tt>self[2]</tt>. # - # %w( a b c d e).third # => "c" + # %w( a b c d e ).third # => "c" def third self[2] end # Equal to <tt>self[3]</tt>. # - # %w( a b c d e).fourth # => "d" + # %w( a b c d e ).fourth # => "d" def fourth self[3] end # Equal to <tt>self[4]</tt>. # - # %w( a b c d e).fifth # => "e" + # %w( a b c d e ).fifth # => "e" def fifth self[4] end # Equal to <tt>self[41]</tt>. Also known as accessing "the reddit". + # + # (1..42).to_a.forty_two # => 42 def forty_two self[41] end diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb index 64e9945ef5..080e3b5ef7 100644 --- a/activesupport/lib/active_support/core_ext/array/conversions.rb +++ b/activesupport/lib/active_support/core_ext/array/conversions.rb @@ -8,11 +8,11 @@ class Array # Converts the array to a comma-separated sentence where the last element is # joined by the connector word. # - # You can pass the following options to change the default behaviour. If you + # You can pass the following options to change the default behavior. If you # pass an option key that doesn't exist in the list below, it will raise an # <tt>ArgumentError</tt>. # - # Options: + # ==== Options # # * <tt>:words_connector</tt> - The sign or word used to join the elements # in arrays with two or more elements (default: ", "). @@ -24,6 +24,8 @@ class Array # the connector options defined on the 'support.array' namespace in the # corresponding dictionary file. # + # ==== Examples + # # [].to_sentence # => "" # ['one'].to_sentence # => "one" # ['one', 'two'].to_sentence # => "one and two" @@ -38,10 +40,10 @@ class Array # ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ') # # => "one or two or at least three" # - # Examples using <tt>:locale</tt> option: + # Using <tt>:locale</tt> option: # # # Given this locale dictionary: - # # + # # # # es: # # support: # # array: @@ -80,23 +82,8 @@ class Array end end - # Converts a collection of elements into a formatted string by calling - # <tt>to_s</tt> on all elements and joining them. Having this model: - # - # class Blog < ActiveRecord::Base - # def to_s - # title - # end - # end - # - # Blog.all.map(&:title) #=> ["First Post", "Second Post", "Third post"] - # - # <tt>to_formatted_s</tt> shows us: - # - # Blog.all.to_formatted_s # => "First PostSecond PostThird Post" - # - # Adding in the <tt>:db</tt> argument as the format yields a comma separated - # id list: + # 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" def to_formatted_s(format = :default) @@ -105,7 +92,7 @@ class Array if empty? 'null' else - collect { |element| element.id }.join(',') + collect(&:id).join(',') end else to_default_s diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 640e6e9328..87ae052eb0 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -18,6 +18,11 @@ class Array # ["3", "4"] # ["5"] def in_groups_of(number, fill_with = nil) + if number.to_i <= 0 + raise ArgumentError, + "Group size must be a positive integer, was #{number.inspect}" + end + if fill_with == false collection = self else @@ -25,15 +30,13 @@ class Array # subtracting from number gives how many to add; # modulo number ensures we don't add group of just fill. padding = (number - size % number) % number - collection = dup.concat([fill_with] * padding) + collection = dup.concat(Array.new(padding, fill_with)) end if block_given? collection.each_slice(number) { |slice| yield(slice) } else - groups = [] - collection.each_slice(number) { |group| groups << group } - groups + collection.each_slice(number).to_a end end @@ -55,7 +58,7 @@ class Array # ["4", "5"] # ["6", "7"] def in_groups(number, fill_with = nil) - # size / number gives minor group size; + # size.div number gives minor group size; # size % number gives how many objects need extra accommodation; # each group hold either division or division + 1 items. division = size.div number @@ -85,14 +88,28 @@ class Array # # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] - def split(value = nil, &block) - inject([[]]) do |results, element| - if block && block.call(element) || value == element - results << [] - else - results.last << element - end + def split(value = nil) + if block_given? + inject([[]]) do |results, element| + if yield(element) + results << [] + else + results.last << element + end + results + end + else + results, arr = [[]], self.dup + until arr.empty? + if (idx = arr.index(value)) + results.last.concat(arr.shift(idx)) + arr.shift + results << [] + else + results.last.concat(arr.shift(arr.size)) + end + end results end end diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb index 27718f19d4..f8d48b69df 100644 --- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb +++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb @@ -1,7 +1,7 @@ class Array - # The human way of thinking about adding stuff to the end of a list is with append + # The human way of thinking about adding stuff to the end of a list is with append. alias_method :append, :<< - # The human way of thinking about adding stuff to the beginning of a list is with prepend + # The human way of thinking about adding stuff to the beginning of a list is with prepend. alias_method :prepend, :unshift end
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/array/uniq_by.rb b/activesupport/lib/active_support/core_ext/array/uniq_by.rb deleted file mode 100644 index ca3b7748cd..0000000000 --- a/activesupport/lib/active_support/core_ext/array/uniq_by.rb +++ /dev/null @@ -1,19 +0,0 @@ -class Array - # *DEPRECATED*: Use +Array#uniq+ instead. - # - # Returns a unique array based on the criteria in the block. - # - # [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2] - def uniq_by(&block) - ActiveSupport::Deprecation.warn 'uniq_by is deprecated. Use Array#uniq instead' - uniq(&block) - end - - # *DEPRECATED*: Use +Array#uniq!+ instead. - # - # Same as +uniq_by+, but modifies +self+. - def uniq_by!(&block) - ActiveSupport::Deprecation.warn 'uniq_by! is deprecated. Use Array#uniq! instead' - uniq!(&block) - end -end diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb index 1245768870..152eb02218 100644 --- a/activesupport/lib/active_support/core_ext/array/wrap.rb +++ b/activesupport/lib/active_support/core_ext/array/wrap.rb @@ -15,12 +15,12 @@ class Array # # * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt> # moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns - # such a +nil+ right away. + # +nil+ right away. # * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt> # raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value. - # * It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array. + # * It does not call +to_a+ on the argument, but returns an empty array if argument is +nil+. # - # The last point is particularly worth comparing for some enumerables: + # The second point is easily explained with some enumerables: # # Array(foo: :bar) # => [[:foo, :bar]] # Array.wrap(foo: :bar) # => [{:foo=>:bar}] @@ -29,10 +29,10 @@ class Array # # [*object] # - # which for +nil+ returns <tt>[]</tt>, and calls to <tt>Array(object)</tt> otherwise. + # which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise. # - # Thus, in this case the behavior may be different for +nil+, and the differences with - # <tt>Kernel#Array</tt> explained above apply to the rest of <tt>object</tt>s. + # The differences with <tt>Kernel#Array</tt> explained above + # apply to the rest of <tt>object</tt>s. def self.wrap(object) if object.nil? [] diff --git a/activesupport/lib/active_support/core_ext/benchmark.rb b/activesupport/lib/active_support/core_ext/benchmark.rb index 2d110155a5..eb25b2bc44 100644 --- a/activesupport/lib/active_support/core_ext/benchmark.rb +++ b/activesupport/lib/active_support/core_ext/benchmark.rb @@ -1,6 +1,13 @@ require 'benchmark' class << Benchmark + # Benchmark realtime in milliseconds. + # + # Benchmark.realtime { User.all } + # # => 8.0e-05 + # + # Benchmark.ms { User.all } + # # => 0.074 def ms 1000 * realtime { yield } end 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 5dc5710c53..234283e792 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,30 +1,15 @@ require 'bigdecimal' -require 'yaml' +require 'bigdecimal/util' class BigDecimal - YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } - - def encode_with(coder) - string = to_s - coder.represent_scalar(nil, YAML_MAPPING[string] || string) - end - - # Backport this method if it doesn't exist - unless method_defined?(:to_d) - def to_d - self - end - end - DEFAULT_STRING_FORMAT = 'F' - def to_formatted_s(*args) - if args[0].is_a?(Symbol) - super + alias_method :to_default_s, :to_s + + def to_s(format = nil, options = nil) + if format.is_a?(Symbol) + to_formatted_s(format, options || {}) else - format = args[0] || DEFAULT_STRING_FORMAT - _original_to_s(format) + to_default_s(format || DEFAULT_STRING_FORMAT) end end - alias_method :_original_to_s, :to_s - alias_method :to_s, :to_formatted_s end diff --git a/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb new file mode 100644 index 0000000000..46ba93ead4 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb @@ -0,0 +1,14 @@ +ActiveSupport::Deprecation.warn 'core_ext/big_decimal/yaml_conversions is deprecated and will be removed in the future.' + +require 'bigdecimal' +require 'yaml' +require 'active_support/core_ext/big_decimal/conversions' + +class BigDecimal + YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } + + def encode_with(coder) + string = to_s + coder.represent_scalar(nil, YAML_MAPPING[string] || string) + end +end diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb index 86b752c2f3..c750a10bb2 100644 --- a/activesupport/lib/active_support/core_ext/class.rb +++ b/activesupport/lib/active_support/core_ext/class.rb @@ -1,4 +1,3 @@ require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/class/attribute_accessors' require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/class/subclasses' diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index 5d8d09aa69..f2a221c396 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -44,7 +44,8 @@ class Class # Base.setting # => [] # Subclass.setting # => [:foo] # - # For convenience, a query method is defined as well: + # For convenience, an instance predicate method is defined as well. + # To skip it, pass <tt>instance_predicate: false</tt>. # # Subclass.setting? # => false # @@ -69,53 +70,58 @@ class Class # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>. def class_attribute(*attrs) options = attrs.extract_options! - # double assignment is used to avoid "assigned but unused variable" warning - instance_reader = instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) + instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true) instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true) + instance_predicate = options.fetch(:instance_predicate, true) - # We use class_eval here rather than define_method because class_attribute - # may be used in a performance sensitive context therefore the overhead that - # define_method introduces may become significant. attrs.each do |name| - class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def self.#{name}() nil end - def self.#{name}?() !!#{name} end + define_singleton_method(name) { nil } + define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate - def self.#{name}=(val) - singleton_class.class_eval do - remove_possible_method(:#{name}) - define_method(:#{name}) { val } - end + ivar = "@#{name}" + + define_singleton_method("#{name}=") do |val| + singleton_class.class_eval do + remove_possible_method(name) + define_method(name) { val } + end - if singleton_class? - class_eval do - remove_possible_method(:#{name}) - def #{name} - defined?(@#{name}) ? @#{name} : singleton_class.#{name} + if singleton_class? + class_eval do + remove_possible_method(name) + define_method(name) do + if instance_variable_defined? ivar + instance_variable_get ivar + else + singleton_class.send name end end end - val end + val + end - if instance_reader - remove_possible_method :#{name} - def #{name} - defined?(@#{name}) ? @#{name} : self.class.#{name} - end - - def #{name}? - !!#{name} + if instance_reader + remove_possible_method name + define_method(name) do + if instance_variable_defined?(ivar) + instance_variable_get ivar + else + self.class.public_send name end end - RUBY + define_method("#{name}?") { !!public_send(name) } if instance_predicate + end attr_writer name if instance_writer end end private - def singleton_class? - ancestors.first != self + + unless respond_to?(:singleton_class?) + def singleton_class? + ancestors.first != self + end end end diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index fa1dbfdf06..84d5e95e7a 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -1,170 +1,4 @@ -require 'active_support/core_ext/array/extract_options' - -# Extends the class object with class and instance accessors for class attributes, -# just like the native attr* accessors for instance attributes. -class Class - # Defines a class attribute if it's not defined and creates a reader method that - # returns the attribute value. - # - # class Person - # cattr_reader :hair_colors - # end - # - # Person.class_variable_set("@@hair_colors", [:brown, :black]) - # Person.hair_colors # => [:brown, :black] - # Person.new.hair_colors # => [:brown, :black] - # - # The attribute name must be a valid method name in Ruby. - # - # class Person - # cattr_reader :"1_Badname " - # end - # # => NameError: invalid attribute name - # - # If you want to opt out the instance reader method, you can pass <tt>instance_reader: false</tt> - # or <tt>instance_accessor: false</tt>. - # - # class Person - # cattr_reader :hair_colors, instance_reader: false - # end - # - # Person.new.hair_colors # => NoMethodError - def cattr_reader(*syms) - options = syms.extract_options! - syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - EOS - - unless options[:instance_reader] == false || options[:instance_accessor] == false - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{sym} - @@#{sym} - end - EOS - end - end - end - - # Defines a class attribute if it's not defined and creates a writer method to allow - # assignment to the attribute. - # - # class Person - # cattr_writer :hair_colors - # end - # - # Person.hair_colors = [:brown, :black] - # Person.class_variable_get("@@hair_colors") # => [:brown, :black] - # Person.new.hair_colors = [:blonde, :red] - # Person.class_variable_get("@@hair_colors") # => [:blonde, :red] - # - # The attribute name must be a valid method name in Ruby. - # - # class Person - # cattr_writer :"1_Badname " - # end - # # => NameError: invalid attribute name - # - # If you want to opt out the instance writer method, pass <tt>instance_writer: false</tt> - # or <tt>instance_accessor: false</tt>. - # - # class Person - # cattr_writer :hair_colors, instance_writer: false - # end - # - # Person.new.hair_colors = [:blonde, :red] # => NoMethodError - # - # Also, you can pass a block to set up the attribute with a default value. - # - # class Person - # cattr_writer :hair_colors do - # [:brown, :black, :blonde, :red] - # end - # end - # - # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] - def cattr_writer(*syms) - options = syms.extract_options! - syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - EOS - - unless options[:instance_writer] == false || options[:instance_accessor] == false - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{sym}=(obj) - @@#{sym} = obj - end - EOS - end - send("#{sym}=", yield) if block_given? - end - end - - # Defines both class and instance accessors for class attributes. - # - # class Person - # cattr_accessor :hair_colors - # end - # - # Person.hair_colors = [:brown, :black, :blonde, :red] - # Person.hair_colors # => [:brown, :black, :blonde, :red] - # Person.new.hair_colors # => [:brown, :black, :blonde, :red] - # - # If a subclass changes the value then that would also change the value for - # parent class. Similarly if parent class changes the value then that would - # change the value of subclasses too. - # - # class Male < Person - # end - # - # Male.hair_colors << :blue - # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] - # - # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. - # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. - # - # class Person - # cattr_accessor :hair_colors, instance_writer: false, instance_reader: false - # end - # - # Person.new.hair_colors = [:brown] # => NoMethodError - # Person.new.hair_colors # => NoMethodError - # - # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. - # - # class Person - # cattr_accessor :hair_colors, instance_accessor: false - # end - # - # Person.new.hair_colors = [:brown] # => NoMethodError - # Person.new.hair_colors # => NoMethodError - # - # Also you can pass a block to set up the attribute with a default value. - # - # class Person - # cattr_accessor :hair_colors do - # [:brown, :black, :blonde, :red] - # end - # end - # - # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] - def cattr_accessor(*syms, &blk) - cattr_reader(*syms) - cattr_writer(*syms, &blk) - end -end +# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d, +# but we keep this around for libraries that directly require it knowing they +# want cattr_*. No need to deprecate. +require 'active_support/core_ext/module/attribute_accessors' diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index ff870f5fd1..1c305c5970 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -1,25 +1,30 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/deprecation' + class Class def superclass_delegating_accessor(name, options = {}) # Create private _name and _name= methods that can still be used if the public - # methods are overridden. This allows - _superclass_delegating_accessor("_#{name}") + # methods are overridden. + _superclass_delegating_accessor("_#{name}", options) - # Generate the public methods name, name=, and name? + # Generate the public methods name, name=, and name?. # These methods dispatch to the private _name, and _name= methods, making them - # overridable + # overridable. singleton_class.send(:define_method, name) { send("_#{name}") } singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - # If an instance_reader is needed, generate methods for name and name= on the - # class itself, so instances will be able to see them - define_method(name) { send("_#{name}") } if options[:instance_reader] != false - define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false + # If an instance_reader is needed, generate public instance methods name and name?. + if options[:instance_reader] != false + define_method(name) { send("_#{name}") } + define_method("#{name}?") { !!send("#{name}") } + end end + deprecate superclass_delegating_accessor: :class_attribute + private # Take the object being set and store it in a method. This gives us automatic # inheritance behavior, without having to store the object in an instance diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb index 9a2dc6e7c5..3c4bfc5f1e 100644 --- a/activesupport/lib/active_support/core_ext/class/subclasses.rb +++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb @@ -29,9 +29,9 @@ class Class # # class Foo; end # class Bar < Foo; end - # class Baz < Foo; end + # class Baz < Bar; end # - # Foo.subclasses # => [Baz, Bar] + # Foo.subclasses # => [Bar] def subclasses subclasses, chain = [], descendants chain.each do |k| diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index 421aa12100..c60e833441 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -8,8 +8,6 @@ require 'active_support/core_ext/date_and_time/calculations' class Date include DateAndTime::Calculations - @beginning_of_week_default = nil - class << self attr_accessor :beginning_of_week_default @@ -71,6 +69,16 @@ class Date alias :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day + # Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00) + def middle_of_day + in_time_zone.middle_of_day + end + alias :midday :middle_of_day + alias :noon :middle_of_day + alias :at_midday :middle_of_day + alias :at_noon :middle_of_day + alias :at_middle_of_day :middle_of_day + # Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59) def end_of_day in_time_zone.end_of_day @@ -121,4 +129,15 @@ class Date options.fetch(:day, day) ) end + + # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there. + def compare_with_coercion(other) + if other.is_a?(Time) + self.to_datetime <=> other + else + compare_without_coercion(other) + end + end + alias_method :compare_without_coercion, :<=> + alias_method :<=>, :compare_with_coercion end diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index fe08ade7e0..df419a6e63 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -13,13 +13,16 @@ class Date day_format = ActiveSupport::Inflector.ordinalize(date.day) date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007" }, - :rfc822 => '%e %b %Y' + :rfc822 => '%e %b %Y', + :iso8601 => lambda { |date| date.iso8601 } } # Ruby 1.9 has Date#to_time which converts to localtime only. - remove_possible_method :to_time + remove_method :to_time - # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. + # Ruby 1.9 has Date#xmlschema which converts to a string without the time + # component. This removal may generate an issue on FreeBSD, that's why we + # need to use remove_possible_method here remove_possible_method :xmlschema # Convert to a formatted string. See DATE_FORMATS for predefined formats. @@ -35,13 +38,14 @@ class Date # date.to_formatted_s(:long) # => "November 10, 2007" # date.to_formatted_s(:long_ordinal) # => "November 10th, 2007" # date.to_formatted_s(:rfc822) # => "10 Nov 2007" + # date.to_formatted_s(:iso8601) # => "2007-11-10" # - # == Adding your own time formats to to_formatted_s + # == Adding your own date formats to to_formatted_s # You can add your own formats to the Date::DATE_FORMATS hash. # Use the format name as the hash key and either a strftime string # or Proc instance that takes a date argument as the value. # - # # config/initializers/time_formats.rb + # # config/initializers/date_formats.rb # Date::DATE_FORMATS[:month_and_year] = '%B %Y' # Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") } def to_formatted_s(format = :default) diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb index b4548671bf..d109b430db 100644 --- a/activesupport/lib/active_support/core_ext/date/zones.rb +++ b/activesupport/lib/active_support/core_ext/date/zones.rb @@ -1,37 +1,6 @@ require 'date' -require 'active_support/core_ext/time/zones' +require 'active_support/core_ext/date_and_time/zones' class Date - # *DEPRECATED*: Use +Date#in_time_zone+ instead. - # - # Converts Date to a TimeWithZone in the current zone if <tt>Time.zone</tt> or - # <tt>Time.zone_default</tt> is set, otherwise converts Date to a Time via - # Date#to_time. - def to_time_in_current_zone - ActiveSupport::Deprecation.warn 'Date#to_time_in_current_zone is deprecated. Use Date#in_time_zone instead', caller - - if ::Time.zone - ::Time.zone.local(year, month, day) - else - to_time - end - end - - # Converts Date to a TimeWithZone in the current zone if Time.zone or Time.zone_default - # is set, otherwise converts Date to a Time via Date#to_time - # - # Time.zone = 'Hawaii' # => 'Hawaii' - # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00 - # - # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument, - # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. - # - # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00 - def in_time_zone(zone = ::Time.zone) - if zone - ::Time.find_zone!(zone).local(year, month, day) - else - to_time - end - end + include DateAndTime::Zones 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 1f78b9eb5a..b85e49aca5 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 @@ -78,7 +78,7 @@ module DateAndTime # Returns a new date/time at the start of the month. # DateTime objects will have a time set to 0:00. def beginning_of_month - first_hour{ change(:day => 1) } + first_hour(change(:day => 1)) end alias :at_beginning_of_month :beginning_of_month @@ -93,7 +93,7 @@ module DateAndTime # 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. + # DateTime objects will have a time set to 23:59:59. 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 @@ -109,11 +109,11 @@ module DateAndTime alias :at_beginning_of_year :beginning_of_year # Returns a new date/time representing the given day in the next week. - # Week is assumed to start on +start_day+, default is - # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. - # DateTime objects have their time set to 0:00. - def next_week(start_day = Date.beginning_of_week) - first_hour{ weeks_since(1).beginning_of_week.days_since(days_span(start_day)) } + # The +given_day_in_next_week+ defaults to the beginning of the week + # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+ + # when set. +DateTime+ objects have their time set to 0:00. + def next_week(given_day_in_next_week = Date.beginning_of_week) + first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) end # Short-hand for months_since(1). @@ -136,7 +136,7 @@ module DateAndTime # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. # DateTime objects have their time set to 0:00. def prev_week(start_day = Date.beginning_of_week) - first_hour{ weeks_ago(1).beginning_of_week.days_since(days_span(start_day)) } + first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day))) end alias_method :last_week, :prev_week @@ -188,7 +188,7 @@ module DateAndTime # +Date.beginning_of_week+ or +config.beginning_of_week+ when set. # DateTime objects have their time set to 23:59:59. def end_of_week(start_day = Date.beginning_of_week) - last_hour{ days_since(6 - days_to_week_start(start_day)) } + last_hour(days_since(6 - days_to_week_start(start_day))) end alias :at_end_of_week :end_of_week @@ -202,7 +202,7 @@ module DateAndTime # DateTime objects will have a time set to 23:59:59. def end_of_month last_day = ::Time.days_in_month(month, year) - last_hour{ days_since(last_day - day) } + last_hour(days_since(last_day - day)) end alias :at_end_of_month :end_of_month @@ -213,16 +213,35 @@ module DateAndTime end alias :at_end_of_year :end_of_year + # Returns a Range representing the whole week of the current date/time. + # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set. + def all_week(start_day = Date.beginning_of_week) + beginning_of_week(start_day)..end_of_week(start_day) + end + + # Returns a Range representing the whole month of the current date/time. + def all_month + beginning_of_month..end_of_month + end + + # Returns a Range representing the whole quarter of the current date/time. + def all_quarter + beginning_of_quarter..end_of_quarter + end + + # Returns a Range representing the whole year of the current date/time. + def all_year + beginning_of_year..end_of_year + end + private - def first_hour - result = yield - acts_like?(:time) ? result.change(:hour => 0) : result + def first_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time end - def last_hour - result = yield - acts_like?(:time) ? result.end_of_day : result + def last_hour(date_or_time) + date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time end def days_span(day) 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 new file mode 100644 index 0000000000..96c6df9407 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb @@ -0,0 +1,41 @@ +module DateAndTime + module Zones + # Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or + # 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 + # 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 + # instead of the operating system's time zone. + # + # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument, + # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. + # + # 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 + time = acts_like?(:time) ? self : nil + + if time_zone + time_with_zone(time, time_zone) + else + time || self.to_time + end + end + + private + + def time_with_zone(time, zone) + if time + ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone) + else + ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc)) + end + end + end +end + diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb index c79745c5aa..8fbbe0d3e9 100644 --- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb @@ -1,3 +1,4 @@ +require 'date' require 'active_support/core_ext/object/acts_like' class DateTime 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 fca5d4d679..55ad384f4f 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -1,14 +1,7 @@ -require 'active_support/deprecation' +require 'date' class DateTime class << self - # *DEPRECATED*: Use +DateTime.civil_from_format+ directly. - def local_offset - ActiveSupport::Deprecation.warn 'DateTime.local_offset is deprecated. Use DateTime.civil_from_format directly.' - - ::Time.local(2012).utc_offset.to_r / 86400 - end - # Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or # <tt>config.time_zone</tt> are set, otherwise returns # <tt>Time.now.to_datetime</tt>. @@ -17,17 +10,11 @@ class DateTime end end - # Tells whether the DateTime object's datetime lies in the past. - def past? - self < ::DateTime.current - end - - # Tells whether the DateTime object's datetime lies in the future. - def future? - self > ::DateTime.current - end - - # Seconds since midnight: DateTime.now.seconds_since_midnight. + # Returns the number of seconds since 00:00:00. + # + # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_since_midnight # => 0 + # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_since_midnight # => 45296 + # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_since_midnight # => 86399 def seconds_since_midnight sec + (min * 60) + (hour * 3600) end @@ -43,7 +30,7 @@ class DateTime # Returns a new DateTime where one or more of the elements have been changed # according to the +options+ parameter. The time options (<tt>:hour</tt>, - # <tt>:minute</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is + # <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is # passed, then minute and sec is set to 0. If the hour and minute is passed, # then sec is set to 0. The +options+ parameter takes a hash with any of these # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, @@ -59,7 +46,7 @@ class DateTime options.fetch(:day, day), options.fetch(:hour, hour), options.fetch(:min, options[:hour] ? 0 : min), - options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec), + options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction), options.fetch(:offset, offset), options.fetch(:start, start) ) @@ -70,6 +57,16 @@ class DateTime # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, # <tt>:minutes</tt>, <tt>:seconds</tt>. def advance(options) + unless options[:weeks].nil? + options[:weeks], partial_weeks = options[:weeks].divmod(1) + options[:days] = options.fetch(:days, 0) + 7 * partial_weeks + end + + unless options[:days].nil? + options[:days], partial_days = options[:days].divmod(1) + options[:hours] = options.fetch(:hours, 0) + 24 * partial_days + end + d = to_date.advance(options) datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ @@ -80,7 +77,7 @@ class DateTime if seconds_to_advance.zero? datetime_advanced_by_date else - datetime_advanced_by_date.since seconds_to_advance + datetime_advanced_by_date.since(seconds_to_advance) end end @@ -106,6 +103,16 @@ class DateTime alias :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day + # Returns a new DateTime representing the middle of the day (12:00) + def middle_of_day + change(:hour => 12) + end + alias :midday :middle_of_day + alias :noon :middle_of_day + alias :at_midday :middle_of_day + alias :at_noon :middle_of_day + alias :at_middle_of_day :middle_of_day + # Returns a new DateTime representing the end of the day (23:59:59). def end_of_day change(:hour => 23, :min => 59, :sec => 59) @@ -124,6 +131,18 @@ class DateTime end alias :at_end_of_hour :end_of_hour + # Returns a new DateTime representing the start of the minute (hh:mm:00). + def beginning_of_minute + change(:sec => 0) + end + alias :at_beginning_of_minute :beginning_of_minute + + # Returns a new DateTime representing the end of the minute (hh:mm:59). + def end_of_minute + change(:sec => 59) + end + alias :at_end_of_minute :end_of_minute + # Adjusts DateTime to UTC by adding its offset value; offset is set to 0. # # DateTime.civil(2005, 2, 21, 10, 11, 12, Rational(-6, 24)) # => Mon, 21 Feb 2005 10:11:12 -0600 @@ -146,7 +165,13 @@ class DateTime # Layers additional behavior on DateTime#<=> so that Time and # ActiveSupport::TimeWithZone instances can be compared with a DateTime. def <=>(other) - super other.to_datetime + if other.kind_of?(Infinity) + super + elsif other.respond_to? :to_datetime + super other.to_datetime + else + nil + end end end diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index b7d8414a9d..2a9c09fc29 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -1,3 +1,4 @@ +require 'date' require 'active_support/inflector/methods' require 'active_support/core_ext/time/conversions' require 'active_support/core_ext/date_time/calculations' @@ -18,6 +19,7 @@ class DateTime # datetime.to_formatted_s(:long) # => "December 04, 2007 00:00" # datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00" # datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000" + # datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00" # # == Adding your own datetime formats to to_formatted_s # DateTime formats are shared with Time. You can add your own to the @@ -69,9 +71,9 @@ class DateTime civil(year, month, day, hour, min, sec, offset) end - # Converts +self+ to a floating-point number of seconds since the Unix epoch. + # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch. def to_f - seconds_since_unix_epoch.to_f + seconds_since_unix_epoch.to_f + sec_fraction end # Converts +self+ to an integer number of seconds since the Unix epoch. @@ -79,6 +81,16 @@ class DateTime seconds_since_unix_epoch.to_i end + # Returns the fraction of a second as microseconds + def usec + (sec_fraction * 1_000_000).to_i + end + + # Returns the fraction of a second as nanoseconds + def nsec + (sec_fraction * 1_000_000_000).to_i + end + private def offset_in_seconds diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb index 6457ffbaf6..c39f358395 100644 --- a/activesupport/lib/active_support/core_ext/date_time/zones.rb +++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb @@ -1,24 +1,6 @@ -require 'active_support/core_ext/time/zones' +require 'date' +require 'active_support/core_ext/date_and_time/zones' class DateTime - # Returns the simultaneous time in <tt>Time.zone</tt>. - # - # Time.zone = 'Hawaii' # => 'Hawaii' - # DateTime.new(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 - # - # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> - # as the local zone instead of the operating system's time zone. - # - # You can also pass in a TimeZone instance or string that identifies a TimeZone - # as an argument, and the conversion will be based on that zone instead of - # <tt>Time.zone</tt>. - # - # DateTime.new(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - def in_time_zone(zone = ::Time.zone) - if zone - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) - else - self - end - end + include DateAndTime::Zones end diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb new file mode 100644 index 0000000000..593c51bba2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb @@ -0,0 +1,51 @@ +require 'securerandom' + +module Digest + module UUID + DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + + # Generates a v5 non-random UUID (Universally Unique IDentifier). + # + # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. + # uuid_from_hash always generates the same UUID for a given name and namespace combination. + # + # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt + def self.uuid_from_hash(hash_class, uuid_namespace, name) + if hash_class == Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 + version = 5 + else + raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." + end + + hash = hash_class.new + hash.update(uuid_namespace) + hash.update(name) + + ary = hash.digest.unpack('NnnnnN') + ary[2] = (ary[2] & 0x0FFF) | (version << 12) + ary[3] = (ary[3] & 0x3FFF) | 0x8000 + + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + # Convenience method for uuid_from_hash using Digest::MD5. + def self.uuid_v3(uuid_namespace, name) + self.uuid_from_hash(Digest::MD5, uuid_namespace, name) + end + + # Convenience method for uuid_from_hash using Digest::SHA1. + def self.uuid_v5(uuid_namespace, name) + self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) + end + + # Convenience method for SecureRandom.uuid. + def self.uuid_v4 + SecureRandom.uuid + end + end +end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 4501b7ff58..1343beb87a 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -35,7 +35,7 @@ module Enumerable if block_given? Hash[map { |elem| [yield(elem), elem] }] else - to_enum :index_by + to_enum(:index_by) { size if respond_to?(:size) } end end diff --git a/activesupport/lib/active_support/core_ext/exception.rb b/activesupport/lib/active_support/core_ext/exception.rb deleted file mode 100644 index ba7757ea07..0000000000 --- a/activesupport/lib/active_support/core_ext/exception.rb +++ /dev/null @@ -1,3 +0,0 @@ -module ActiveSupport - FrozenObjectError = RuntimeError -end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index c3e6124a57..38374af388 100644 --- a/activesupport/lib/active_support/core_ext/file/atomic.rb +++ b/activesupport/lib/active_support/core_ext/file/atomic.rb @@ -23,7 +23,7 @@ class File yield temp_file temp_file.close - if File.exists?(file_name) + if File.exist?(file_name) # Get original file permissions old_stat = stat(file_name) else @@ -40,7 +40,7 @@ class File chown(old_stat.uid, old_stat.gid, file_name) # This operation will affect filesystem ACL's chmod(old_stat.mode, file_name) - rescue Errno::EPERM + rescue Errno::EPERM, Errno::EACCES # Changing file ownership failed, moving on. end end diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index 501483498d..af4d1da0eb 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,8 +1,9 @@ +require 'active_support/core_ext/hash/compact' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' -require 'active_support/core_ext/hash/diff' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb new file mode 100644 index 0000000000..5dc9a05ec7 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -0,0 +1,20 @@ +class Hash + # Returns a hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil} + # hash.compact # => { a: true, b: false} + # hash # => { a: true, b: false, c: nil} + # { c: nil }.compact # => {} + def compact + self.select { |_, value| !value.nil? } + end + + # Replaces current hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil} + # hash.compact! # => { a: true, b: false} + # hash # => { a: true, b: false} + def compact! + self.reject! { |_, value| value.nil? } + end +end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index 6cb7434e5f..2149d4439d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections' class Hash # Returns a string containing an XML representation of its receiver: # - # {'foo' => 1, 'bar' => 2}.to_xml + # { foo: 1, bar: 2 }.to_xml # # => # # <?xml version="1.0" encoding="UTF-8"?> # # <hash> @@ -43,7 +43,10 @@ class Hash # end # # { foo: Foo.new }.to_xml(skip_instruct: true) - # # => "<hash><bar>fooing!</bar></hash>" + # # => + # # <hash> + # # <bar>fooing!</bar> + # # </hash> # # * Otherwise, a node with +key+ as tag is created with a string representation of # +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added. @@ -101,17 +104,33 @@ class Hash # # hash = Hash.from_xml(xml) # # => {"hash"=>{"foo"=>1, "bar"=>2}} - def from_xml(xml) - ActiveSupport::XMLConverter.new(xml).to_h + # + # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or + # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. + def from_xml(xml, disallowed_types = nil) + ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h end + # Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML. + def from_trusted_xml(xml) + from_xml xml, [] + end end end module ActiveSupport class XMLConverter # :nodoc: - def initialize(xml) + class DisallowedType < StandardError + def initialize(type) + super "Disallowed type attribute: #{type.inspect}" + end + end + + DISALLOWED_TYPES = %w(symbol yaml) + + def initialize(xml, disallowed_types = nil) @xml = normalize_keys(XmlMini.parse(xml)) + @disallowed_types = disallowed_types || DISALLOWED_TYPES end def to_h @@ -119,7 +138,6 @@ module ActiveSupport end private - def normalize_keys(params) case params when Hash @@ -145,6 +163,10 @@ module ActiveSupport end def process_hash(value) + if value.include?('type') && !value['type'].is_a?(Hash) && @disallowed_types.include?(value['type']) + raise DisallowedType, value['type'] + end + if become_array?(value) _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) }) if entries.nil? || value['__content__'].try(:empty?) @@ -182,7 +204,7 @@ module ActiveSupport end def become_empty_string?(value) - # {"string" => true} + # { "string" => true } # No tests fail when the second term is removed. value['type'] == 'string' && value['nil'] != 'true' end @@ -199,7 +221,7 @@ module ActiveSupport def garbage?(value) # If the type is the only element which makes it then # this still makes the value nil, except if type is - # a XML node(where type['value'] is a Hash) + # an XML node(where type['value'] is a Hash) value['type'] && !value['type'].is_a?(::Hash) && value.size == 1 end @@ -219,4 +241,3 @@ module ActiveSupport end end - diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb index e07db50b77..9c9faf67ea 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -1,27 +1,38 @@ class Hash # Returns a new hash with +self+ and +other_hash+ merged recursively. # - # h1 = { x: { y: [4,5,6] }, z: [7,8,9] } - # h2 = { x: { y: [7,8,9] }, z: 'xyz' } + # h1 = { a: true, b: { c: [1, 2, 3] } } + # h2 = { a: false, b: { x: [3, 4, 5] } } # - # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"} - # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]} - # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) } - # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]} + # h1.deep_merge(h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } + # + # Like with Hash#merge in the standard library, a block can be provided + # to merge values: + # + # h1 = { a: 100, b: 200, c: { c1: 100 } } + # h2 = { b: 250, c: { c1: 200 } } + # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } + # # => { a: 100, b: 450, c: { c1: 300 } } def deep_merge(other_hash, &block) dup.deep_merge!(other_hash, &block) end # Same as +deep_merge+, but modifies +self+. def deep_merge!(other_hash, &block) - other_hash.each_pair do |k,v| - tv = self[k] - if tv.is_a?(Hash) && v.is_a?(Hash) - self[k] = tv.deep_merge(v, &block) + other_hash.each_pair do |current_key, other_value| + this_value = self[current_key] + + self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash) + this_value.deep_merge(other_value, &block) else - self[k] = block && tv ? block.call(k, tv, v) : v + if block_given? && key?(current_key) + block.call(current_key, this_value, other_value) + else + other_value + end end end + self end end diff --git a/activesupport/lib/active_support/core_ext/hash/diff.rb b/activesupport/lib/active_support/core_ext/hash/diff.rb deleted file mode 100644 index 5f3868b5b0..0000000000 --- a/activesupport/lib/active_support/core_ext/hash/diff.rb +++ /dev/null @@ -1,14 +0,0 @@ -class Hash - # Returns a hash that represents the difference between two hashes. - # - # {1 => 2}.diff(1 => 2) # => {} - # {1 => 2}.diff(1 => 3) # => {1 => 2} - # {}.diff(1 => 2) # => {1 => 2} - # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4} - def diff(other) - ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead." - dup. - delete_if { |k, v| other[k] == v }. - merge!(other.dup.delete_if { |k, v| has_key?(k) }) - end -end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index 5cb00d0ebd..6e397abf51 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,13 +1,19 @@ class Hash - # Return a hash that includes everything but the given keys. This is useful for - # limiting a set of parameters to everything but a few known toggles: + # Returns a hash that includes everything but the given keys. + # hash = { a: true, b: false, c: nil} + # hash.except(:c) # => { a: true, b: false} + # hash # => { a: true, b: false, c: nil} # - # @person.update_attributes(params[:person].except(:admin)) + # This is useful for limiting a set of parameters to everything but a few known toggles: + # @person.update(params[:person].except(:admin)) def except(*keys) dup.except!(*keys) end # Replaces the hash without the given keys. + # hash = { a: true, b: false, c: nil} + # hash.except!(:c) # => { a: true, b: false} + # hash # => { a: true, b: false } def except!(*keys) keys.each { |key| delete(key) } self diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 981e8436bf..28cb3e2a3b 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -18,5 +18,6 @@ class Hash # # b = { b: 1 } # { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access + # # => {"b"=>1} alias nested_under_indifferent_access with_indifferent_access end diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index b4c451ace4..9297a59c46 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -1,12 +1,13 @@ class Hash - # Return a new hash with all keys converted using the block operation. + # Returns a new hash with all keys converted using the block operation. # # hash = { name: 'Rob', age: '28' } # # hash.transform_keys{ |key| key.to_s.upcase } - # # => { "NAME" => "Rob", "AGE" => "28" } + # # => {"NAME"=>"Rob", "AGE"=>"28"} def transform_keys - result = {} + return enum_for(:transform_keys) unless block_given? + result = self.class.new each_key do |key| result[yield(key)] = self[key] end @@ -16,35 +17,36 @@ class Hash # Destructively convert all keys using the block operations. # Same as transform_keys but modifies +self+. def transform_keys! + return enum_for(:transform_keys!) unless block_given? keys.each do |key| self[yield(key)] = delete(key) end self end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # # hash = { name: 'Rob', age: '28' } # # hash.stringify_keys - # #=> { "name" => "Rob", "age" => "28" } + # # => {"name"=>"Rob", "age"=>"28"} def stringify_keys - transform_keys{ |key| key.to_s } + transform_keys(&:to_s) end # Destructively convert all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! - transform_keys!{ |key| key.to_s } + transform_keys!(&:to_s) end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. # # hash = { 'name' => 'Rob', 'age' => '28' } # # hash.symbolize_keys - # #=> { name: "Rob", age: "28" } + # # => {:name=>"Rob", :age=>"28"} def symbolize_keys transform_keys{ |key| key.to_sym rescue key } end @@ -57,82 +59,108 @@ class Hash end alias_method :to_options!, :symbolize_keys! - # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError - # on a mismatch. Note that keys are NOT treated indifferently, meaning if you - # use strings for keys but assert symbols as keys, this will fail. + # Validate all keys in a hash match <tt>*valid_keys</tt>, raising + # ArgumentError on a mismatch. # - # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years" - # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: name" + # Note that keys are treated differently than HashWithIndifferentAccess, + # meaning that string and symbol keys will not match. + # + # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age" + # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'" # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing def assert_valid_keys(*valid_keys) valid_keys.flatten! each_key do |k| - raise ArgumentError.new("Unknown key: #{k}") unless valid_keys.include?(k) + unless valid_keys.include?(k) + raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}") + end end end - # Return a new hash with all keys converted by the block operation. + # Returns a new hash with all keys converted by the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_transform_keys{ |key| key.to_s.upcase } - # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } } + # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}} def deep_transform_keys(&block) - result = {} - each do |key, value| - result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value - end - result + _deep_transform_keys_in_object(self, &block) end # Destructively convert all keys by using the block operation. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_transform_keys!(&block) - keys.each do |key| - value = delete(key) - self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value - end - self + _deep_transform_keys_in_object!(self, &block) end - # Return a new hash with all keys converted to strings. + # Returns a new hash with all keys converted to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. # # hash = { person: { name: 'Rob', age: '28' } } # # hash.deep_stringify_keys - # # => { "person" => { "name" => "Rob", "age" => "28" } } + # # => {"person"=>{"name"=>"Rob", "age"=>"28"}} def deep_stringify_keys - deep_transform_keys{ |key| key.to_s } + deep_transform_keys(&:to_s) end # Destructively convert all keys to strings. # This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_stringify_keys! - deep_transform_keys!{ |key| key.to_s } + deep_transform_keys!(&:to_s) end - # Return a new hash with all keys converted to symbols, as long as + # Returns a new hash with all keys converted to symbols, as long as # they respond to +to_sym+. This includes the keys from the root hash - # and from all nested hashes. + # and from all nested hashes and arrays. # # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } } # # hash.deep_symbolize_keys - # # => { person: { name: "Rob", age: "28" } } + # # => {:person=>{:name=>"Rob", :age=>"28"}} def deep_symbolize_keys deep_transform_keys{ |key| key.to_sym rescue key } end # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. This includes the keys from the root hash and from all - # nested hashes. + # nested hashes and arrays. def deep_symbolize_keys! deep_transform_keys!{ |key| key.to_sym rescue key } end + + private + # support methods for deep transforming nested hashes and arrays + def _deep_transform_keys_in_object(object, &block) + case object + when Hash + object.each_with_object({}) do |(key, value), result| + result[yield(key)] = _deep_transform_keys_in_object(value, &block) + end + when Array + object.map {|e| _deep_transform_keys_in_object(e, &block) } + else + object + end + end + + def _deep_transform_keys_in_object!(object, &block) + case object + when Hash + object.keys.each do |key| + value = object.delete(key) + object[yield(key)] = _deep_transform_keys_in_object!(value, &block) + end + object + when Array + object.map! {|e| _deep_transform_keys_in_object!(e, &block)} + else + object + end + end end diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index 9fa9b3dac4..41b2279013 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -1,6 +1,12 @@ class Hash - # Slice a hash to include only the given keys. This is useful for - # limiting an options hash to valid keys before passing to a method: + # Slice a hash to include only the given keys. Returns a hash containing + # the given keys. + # + # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b) + # # => {:a=>1, :b=>2} + # + # This is useful for limiting an options hash to valid keys before + # passing to a method: # # def search(criteria = {}) # criteria.assert_valid_keys(:mass, :velocity, :time) @@ -26,6 +32,8 @@ class Hash keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) omit = slice(*self.keys - keys) hash = slice(*keys) + hash.default = default + hash.default_proc = default_proc if default_proc replace(hash) omit end diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb new file mode 100644 index 0000000000..e9bcce761f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb @@ -0,0 +1,23 @@ +class Hash + # Returns a new hash with the results of running +block+ once for every value. + # The keys are unchanged. + # + # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } + # # => { a: 2, b: 4, c: 6 } + def transform_values + return enum_for(:transform_values) unless block_given? + result = self.class.new + each do |key, value| + result[key] = yield(value) + end + result + end + + # Destructive +transform_values+ + def transform_values! + return enum_for(:transform_values!) unless block_given? + each do |key, value| + self[key] = yield(value) + end + end +end diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb index 7c6c2f1ca7..c668c7c2eb 100644 --- a/activesupport/lib/active_support/core_ext/integer/multiple.rb +++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb @@ -1,9 +1,9 @@ class Integer # Check whether the integer is evenly divisible by the argument. # - # 0.multiple_of?(0) #=> true - # 6.multiple_of?(5) #=> false - # 10.multiple_of?(2) #=> true + # 0.multiple_of?(0) # => true + # 6.multiple_of?(5) # => false + # 10.multiple_of?(2) # => true def multiple_of?(number) number != 0 ? self % number == 0 : zero? end diff --git a/activesupport/lib/active_support/core_ext/kernel.rb b/activesupport/lib/active_support/core_ext/kernel.rb index 0275f4c037..293a3b2619 100644 --- a/activesupport/lib/active_support/core_ext/kernel.rb +++ b/activesupport/lib/active_support/core_ext/kernel.rb @@ -1,4 +1,5 @@ -require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/agnostics' -require 'active_support/core_ext/kernel/debugger' +require 'active_support/core_ext/kernel/concern' +require 'active_support/core_ext/kernel/debugger' if RUBY_VERSION < '2.0.0' +require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/kernel/singleton_class' diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb new file mode 100644 index 0000000000..bf72caa058 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb @@ -0,0 +1,10 @@ +require 'active_support/core_ext/module/concerning' + +module Kernel + # A shortcut to define a toplevel concern, not within a module. + # + # See Module::Concerning for more. + def concern(topic, &module_definition) + Object.concern topic, &module_definition + end +end diff --git a/activesupport/lib/active_support/core_ext/kernel/debugger.rb b/activesupport/lib/active_support/core_ext/kernel/debugger.rb index 2073cac98d..ddf66b2022 100644 --- a/activesupport/lib/active_support/core_ext/kernel/debugger.rb +++ b/activesupport/lib/active_support/core_ext/kernel/debugger.rb @@ -1,9 +1,9 @@ module Kernel unless respond_to?(:debugger) - # Starts a debugging session if the +debugger+ gem has been loaded (call rails server --debugger to do load it). + # Starts a debugging session if the +debugger+ gem has been loaded (call rails server --debugger to load it). def debugger message = "\n***** Debugger requested, but was not available (ensure the debugger gem is listed in Gemfile/installed as gem): Start server with --debugger to enable *****\n" - defined?(Rails) ? Rails.logger.info(message) : $stderr.puts(message) + defined?(Rails.logger) ? Rails.logger.info(message) : $stderr.puts(message) end alias breakpoint debugger unless respond_to?(:breakpoint) end diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 7b518821c8..f5179552bb 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -31,9 +31,13 @@ module Kernel # For compatibility def silence_stderr #:nodoc: + ActiveSupport::Deprecation.warn( + "`#silence_stderr` is deprecated and will be removed in the next release." + ) #not thread-safe silence_stream(STDERR) { yield } end + # Deprecated : this method is not thread safe # Silences any stream for the duration of the block. # # silence_stream(STDOUT) do @@ -41,6 +45,8 @@ module Kernel # end # # puts 'But this will' + # + # This method is not thread-safe. def silence_stream(stream) old_stream = stream.dup stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null') @@ -48,6 +54,7 @@ module Kernel yield ensure stream.reopen(old_stream) + old_stream.close end # Blocks and ignores any exception passed as argument if raised within the block. @@ -59,10 +66,8 @@ module Kernel # # puts 'This code gets executed and nothing related to ZeroDivisionError was seen' def suppress(*exception_classes) - begin yield - rescue Exception => e - raise unless exception_classes.any? { |cls| e.kind_of?(cls) } - end + yield + rescue *exception_classes end # Captures the given stream and returns it: @@ -81,6 +86,9 @@ module Kernel # stream = capture(:stderr) { system('echo error 1>&2') } # stream # => "error\n" def capture(stream) + ActiveSupport::Deprecation.warn( + "`#capture(stream)` is deprecated and will be removed in the next release." + ) #not thread-safe stream = stream.to_s captured_stream = Tempfile.new(stream) stream_io = eval("$#{stream}") @@ -92,6 +100,7 @@ module Kernel stream_io.rewind return captured_stream.read ensure + captured_stream.close captured_stream.unlink stream_io.reopen(origin_stream) end @@ -100,7 +109,12 @@ module Kernel # Silences both STDOUT and STDERR, even for subprocesses. # # quietly { system 'bundle install' } + # + # This method is not thread-safe. def quietly + ActiveSupport::Deprecation.warn( + "`#quietly` is deprecated and will be removed in the next release." + ) #not thread-safe silence_stream(STDOUT) do silence_stream(STDERR) do yield diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb index fe24f3716d..768b980f21 100644 --- a/activesupport/lib/active_support/core_ext/load_error.rb +++ b/activesupport/lib/active_support/core_ext/load_error.rb @@ -7,6 +7,7 @@ class LoadError ] unless method_defined?(:path) + # Returns the path which was unable to be loaded. def path @path ||= begin REGEXPS.find do |regex| @@ -17,9 +18,11 @@ class LoadError end end + # Returns true if the given path name (except perhaps for the ".rb" + # extension) is the missing file which caused the exception to be raised. def is_missing?(location) location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '') end end -MissingSourceFile = LoadError
\ No newline at end of file +MissingSourceFile = LoadError diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb deleted file mode 100644 index 34de766331..0000000000 --- a/activesupport/lib/active_support/core_ext/logger.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/deprecation' -require 'active_support/logger_silence' - -ActiveSupport::Deprecation.warn 'this file is deprecated and will be removed' - -# Adds the 'around_level' method to Logger. -class Logger #:nodoc: - def self.define_around_helper(level) - module_eval <<-end_eval, __FILE__, __LINE__ + 1 - def around_#{level}(before_message, after_message) # def around_debug(before_message, after_message, &block) - self.#{level}(before_message) # self.debug(before_message) - return_value = yield(self) # return_value = yield(self) - self.#{level}(after_message) # self.debug(after_message) - return_value # return_value - end # end - end_eval - end - [:debug, :info, :error, :fatal].each {|level| define_around_helper(level) } -end - -require 'logger' - -# Extensions to the built-in Ruby logger. -# -# If you want to use the default log formatter as defined in the Ruby core, then you -# will need to set the formatter for the logger as in: -# -# logger.formatter = Formatter.new -# -# You can then specify the datetime format, for example: -# -# logger.datetime_format = "%Y-%m-%d" -# -# Note: This logger is deprecated in favor of ActiveSupport::Logger -class Logger - include LoggerSilence - - alias :old_datetime_format= :datetime_format= - # Logging date-time format (string passed to +strftime+). Ignored if the formatter - # does not respond to datetime_format=. - def datetime_format=(format) - formatter.datetime_format = format if formatter.respond_to?(:datetime_format=) - end - - alias :old_datetime_format :datetime_format - # Get the logging datetime format. Returns nil if the formatter does not support - # datetime formatting. - def datetime_format - formatter.datetime_format if formatter.respond_to?(:datetime_format) - end - - alias :old_initialize :initialize - # Overwrite initialize to set a default formatter. - def initialize(*args) - old_initialize(*args) - self.formatter = SimpleFormatter.new - end - - # Simple formatter which only displays the message. - class SimpleFormatter < Logger::Formatter - # This method is invoked when a log event occurs - def call(severity, timestamp, progname, msg) - "#{String === msg ? msg : msg.inspect}\n" - end - end -end diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb index fec3051c0c..56c79c04bd 100644 --- a/activesupport/lib/active_support/core_ext/marshal.rb +++ b/activesupport/lib/active_support/core_ext/marshal.rb @@ -1,21 +1,21 @@ +require 'active_support/core_ext/module/aliasing' + module Marshal class << self def load_with_autoloading(source) - begin - load_without_autoloading(source) - rescue ArgumentError, NameError => exc - 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 - source.rewind if source.respond_to?(:rewind) - retry - else - raise exc - end + load_without_autoloading(source) + rescue ArgumentError, NameError => exc + 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 + source.rewind if source.respond_to?(:rewind) + retry + else + raise exc end end alias_method_chain :load, :autoloading end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index f2d4887df6..b4efff8b24 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/reachable' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/attr_internal' +require 'active_support/core_ext/module/concerning' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/module/remove_method' diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb index 580cb80413..0a6fadf928 100644 --- a/activesupport/lib/active_support/core_ext/module/aliasing.rb +++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb @@ -19,9 +19,9 @@ class Module # alias_method :foo_without_feature?, :foo? # alias_method :foo?, :foo_with_feature? # - # so you can safely chain foo, foo?, and foo! with the same feature. + # so you can safely chain foo, foo?, foo! and/or foo= with the same feature. def alias_method_chain(target, feature) - # Strip out punctuation on predicates or bang methods since + # Strip out punctuation on predicates, bang or writer methods since # e.g. target?_without_feature is not a valid method name. aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1 yield(aliased_target, punctuation) if block_given? diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb index db07d549b0..67f0e0335d 100644 --- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb +++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb @@ -27,7 +27,8 @@ class Module def attr_internal_define(attr_name, type) internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '') - class_eval do # class_eval is necessary on 1.9 or else the methods a made private + # class_eval is necessary on 1.9 or else the methods are made private + class_eval do # use native attr_* methods as they are faster on some Ruby implementations send("attr_#{type}", internal_name) 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 672cc0256f..d4e6b5a1ac 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -1,10 +1,59 @@ require 'active_support/core_ext/array/extract_options' +# Extends the module object with class/module and instance accessors for +# class/module attributes, just like the native attr* accessors for instance +# attributes. class Module + # Defines a class attribute and creates a class and instance reader methods. + # The underlying the class variable is set to +nil+, if it is not previously + # defined. + # + # module HairColors + # mattr_reader :hair_colors + # end + # + # HairColors.hair_colors # => nil + # HairColors.class_variable_set("@@hair_colors", [:brown, :black]) + # HairColors.hair_colors # => [:brown, :black] + # + # The attribute name must be a valid method name in Ruby. + # + # module Foo + # mattr_reader :"1_Badname " + # end + # # => NameError: invalid attribute name + # + # If you want to opt out the creation on the instance reader method, pass + # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. + # + # module HairColors + # mattr_writer :hair_colors, instance_reader: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors # => NoMethodError + # + # + # Also, you can pass a block to set up the attribute with a default value. + # + # module HairColors + # cattr_reader :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # class Person + # include HairColors + # end + # + # Person.hair_colors # => [:brown, :black, :blonde, :red] def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -20,14 +69,60 @@ class Module end EOS end + class_variable_set("@@#{sym}", yield) if block_given? end end + alias :cattr_reader :mattr_reader + # Defines a class attribute and creates a class and instance writer methods to + # allow assignment to the attribute. + # + # module HairColors + # mattr_writer :hair_colors + # end + # + # class Person + # include HairColors + # end + # + # HairColors.hair_colors = [:brown, :black] + # Person.class_variable_get("@@hair_colors") # => [:brown, :black] + # Person.new.hair_colors = [:blonde, :red] + # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red] + # + # If you want to opt out the instance writer method, pass + # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. + # + # module HairColors + # mattr_writer :hair_colors, instance_writer: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors = [:blonde, :red] # => NoMethodError + # + # Also, you can pass a block to set up the attribute with a default value. + # + # class HairColors + # mattr_writer :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # class Person + # include HairColors + # end + # + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) + @@#{sym} = nil unless defined? @@#{sym} + def self.#{sym}=(obj) @@#{sym} = obj end @@ -40,27 +135,78 @@ class Module end EOS end + send("#{sym}=", yield) if block_given? end end + alias :cattr_writer :mattr_writer - # Extends the module object with module and instance accessors for class attributes, - # just like the native attr* accessors for instance attributes. + # Defines both class and instance accessors for class attributes. # - # module AppConfiguration - # mattr_accessor :google_api_key + # module HairColors + # mattr_accessor :hair_colors + # end # - # self.google_api_key = "123456789" + # class Person + # include HairColors # end # - # AppConfiguration.google_api_key # => "123456789" - # AppConfiguration.google_api_key = "overriding the api key!" - # AppConfiguration.google_api_key # => "overriding the api key!" + # Person.hair_colors = [:brown, :black, :blonde, :red] + # Person.hair_colors # => [:brown, :black, :blonde, :red] + # Person.new.hair_colors # => [:brown, :black, :blonde, :red] + # + # If a subclass changes the value then that would also change the value for + # parent class. Similarly if parent class changes the value then that would + # change the value of subclasses too. + # + # class Male < Person + # end + # + # Male.hair_colors << :blue + # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] # # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. - # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>. - def mattr_accessor(*syms) - mattr_reader(*syms) - mattr_writer(*syms) + # + # module HairColors + # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors = [:brown] # => NoMethodError + # Person.new.hair_colors # => NoMethodError + # + # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # + # module HairColors + # mattr_accessor :hair_colors, instance_accessor: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors = [:brown] # => NoMethodError + # Person.new.hair_colors # => NoMethodError + # + # Also you can pass a block to set up the attribute with a default value. + # + # module HairColors + # mattr_accessor :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # class Person + # include HairColors + # end + # + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] + def mattr_accessor(*syms, &blk) + mattr_reader(*syms, &blk) + mattr_writer(*syms, &blk) end + alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb new file mode 100644 index 0000000000..07a392404e --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -0,0 +1,135 @@ +require 'active_support/concern' + +class Module + # = Bite-sized separation of concerns + # + # We often find ourselves with a medium-sized chunk of behavior that we'd + # like to extract, but only mix in to a single class. + # + # Extracting a plain old Ruby object to encapsulate it and collaborate or + # delegate to the original object is often a good choice, but when there's + # no additional state to encapsulate or we're making DSL-style declarations + # about the parent class, introducing new collaborators can obfuscate rather + # than simplify. + # + # The typical route is to just dump everything in a monolithic class, perhaps + # with a comment, as a least-bad alternative. Using modules in separate files + # means tedious sifting to get a big-picture view. + # + # = Dissatisfying ways to separate small concerns + # + # == Using comments: + # + # class Todo + # # Other todo implementation + # # ... + # + # ## Event tracking + # has_many :events + # + # before_create :track_creation + # after_destroy :track_deletion + # + # private + # def track_creation + # # ... + # end + # end + # + # == With an inline module: + # + # Noisy syntax. + # + # class Todo + # # Other todo implementation + # # ... + # + # module EventTracking + # extend ActiveSupport::Concern + # + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # include EventTracking + # end + # + # == Mix-in noise exiled to its own file: + # + # Once our chunk of behavior starts pushing the scroll-to-understand it's + # boundary, we give in and move it to a separate file. At this size, the + # overhead feels in good proportion to the size of our extraction, despite + # diluting our at-a-glance sense of how things really work. + # + # class Todo + # # Other todo implementation + # # ... + # + # include TodoEventTracking + # end + # + # = Introducing Module#concerning + # + # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to + # separate bite-sized concerns. + # + # class Todo + # # Other todo implementation + # # ... + # + # concerning :EventTracking do + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # end + # + # Todo.ancestors + # # => Todo, Todo::EventTracking, Object + # + # This small step has some wonderful ripple effects. We can + # * grok the behavior of our class in one glance, + # * clean up monolithic junk-drawer classes by separating their concerns, and + # * stop leaning on protected/private for crude "this is internal stuff" modularity. + module Concerning + # Define a new concern and mix it in. + def concerning(topic, &block) + include concern(topic, &block) + end + + # A low-cruft shortcut to define a concern. + # + # concern :EventTracking do + # ... + # end + # + # is equivalent to + # + # module EventTracking + # extend ActiveSupport::Concern + # + # ... + # end + def concern(topic, &module_definition) + const_set topic, Module.new { + extend ::ActiveSupport::Concern + module_eval(&module_definition) + } + end + end + include Concerning +end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index e608eeaf42..24df83800b 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,8 +1,27 @@ +require 'set' + class Module - # Provides a delegate class method to easily expose contained objects' public methods - # as your own. Pass one or more methods (specified as symbols or strings) - # and the name of the target object via the <tt>:to</tt> option (also a symbol - # or string). At least one method and the <tt>:to</tt> option are required. + # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+ + # option is not used. + class DelegationError < NoMethodError; end + + RUBY_RESERVED_WORDS = Set.new( + %w(alias and BEGIN begin break case class def defined? do else elsif END + end ensure false for if in module next nil not or redo rescue retry + return self super then true undef unless until when while yield) + ).freeze + + # Provides a +delegate+ class method to easily expose contained objects' + # public methods as your own. + # + # ==== Options + # * <tt>:to</tt> - Specifies the target object + # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix + # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ from being raised + # + # The macro receives one or more method names (specified as symbols or + # strings) and the name of the target object via the <tt>:to</tt> option + # (also a symbol or string). # # Delegation is particularly useful with Active Record associations: # @@ -89,29 +108,46 @@ class Module # invoice.customer_name # => 'John Doe' # invoice.customer_address # => 'Vimmersvej 13' # - # If the delegate object is +nil+ an exception is raised, and that happens - # no matter whether +nil+ responds to the delegated method. You can get a - # +nil+ instead with the +:allow_nil+ option. + # If the target is +nil+ and does not respond to the delegated method a + # +NoMethodError+ is raised, as with any other value. Sometimes, however, it + # makes sense to be robust to that situation and that is the purpose of the + # <tt>:allow_nil</tt> option: If the target is not +nil+, or it is and + # responds to the method, everything works as usual. But if it is +nil+ and + # does not respond to the delegated method, +nil+ is returned. # - # class Foo - # attr_accessor :bar - # def initialize(bar = nil) - # @bar = bar - # end - # delegate :zoo, to: :bar + # class User < ActiveRecord::Base + # has_one :profile + # delegate :age, to: :profile + # end + # + # User.new.age # raises NoMethodError: undefined method `age' + # + # But if not having a profile yet is fine and should not be an error + # condition: + # + # class User < ActiveRecord::Base + # has_one :profile + # delegate :age, to: :profile, allow_nil: true # end # - # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo) + # User.new.age # nil + # + # Note that if the target is not +nil+ then the call is attempted regardless of the + # <tt>:allow_nil</tt> option, and thus an exception is still raised if said object + # does not respond to the method: # # class Foo - # attr_accessor :bar - # def initialize(bar = nil) + # def initialize(bar) # @bar = bar # end - # delegate :zoo, to: :bar, allow_nil: true + # + # delegate :name, to: :@bar, allow_nil: true # end # - # Foo.new.zoo # returns nil + # Foo.new("Bar").name # raises NoMethodError: undefined method `name' + # + # The target method must be public, otherwise it will raise +NoMethodError+. + # def delegate(*methods) options = methods.pop unless options.is_a?(Hash) && to = options[:to] @@ -135,36 +171,35 @@ class Module line = line.to_i to = to.to_s - to = 'self.class' if to == 'class' + to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to) methods.each do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' - if allow_nil - module_eval(<<-EOS, file, line - 2) - def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name) - #{to}.#{method}(#{definition}) # client.name(*args, &block) - end # end - end # end - EOS - else - exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + # The following generated method calls the target exactly once, storing + # the returned value in a dummy variable. + # + # Reason is twofold: On one hand doing less calls is in general better. + # On the other hand it could be that the target has side-effects, + # whereas conceptually, from the user point of view, the delegator should + # be doing one call. - module_eval(<<-EOS, file, line - 1) - def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - #{to}.#{method}(#{definition}) # client.name(*args, &block) - rescue NoMethodError # rescue NoMethodError - if #{to}.nil? # if client.nil? - #{exception} # # add helpful message to the exception - else # else - raise # raise - end # end - end # end - EOS - end + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + " _ = #{to}", + " if !_.nil? || nil.respond_to?(:#{method})", + " _.#{method}(#{definition})", + " else", + " #{exception unless allow_nil}", + " end", + "end" + ].join ';' + + module_eval(method_def, file, line) end end end diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index cc45cee5b8..56d670fbe8 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/method_wrappers' - class Module # deprecate :foo # deprecate bar: 'message' @@ -14,8 +12,8 @@ class Module # method where you can implement your custom warning behavior. # # class MyLib::Deprecator - # def deprecation_warning(deprecated_method_name, message, caller_backtrace) - # message = "#{method_name} is deprecated and will be removed from MyLibrary | #{message}" + # def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil) + # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}" # Kernel.warn message # end # end diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb index 08e5f8a5c3..f1d26ef28f 100644 --- a/activesupport/lib/active_support/core_ext/module/introspection.rb +++ b/activesupport/lib/active_support/core_ext/module/introspection.rb @@ -59,20 +59,4 @@ class Module def local_constants #:nodoc: constants(false) end - - # *DEPRECATED*: Use +local_constants+ instead. - # - # Returns the names of the constants defined locally as strings. - # - # module M - # X = 1 - # end - # M.local_constant_names # => ["X"] - # - # This method is useful for forward compatibility, since Ruby 1.8 returns - # constant names as strings, whereas 1.9 returns them as symbols. - def local_constant_names - ActiveSupport::Deprecation.warn 'Module#local_constant_names is deprecated, use Module#local_constants instead' - local_constants.map { |c| c.to_s } - end end diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb new file mode 100644 index 0000000000..b1097cc83b --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb @@ -0,0 +1,11 @@ +class Module + ### + # TODO: remove this after 1.9 support is dropped + def methods_transplantable? # :nodoc: + x = Module.new { def foo; end } + Module.new { define_method :bar, x.instance_method(:foo) } + true + rescue TypeError + false + end +end diff --git a/activesupport/lib/active_support/core_ext/numeric/bytes.rb b/activesupport/lib/active_support/core_ext/numeric/bytes.rb index deea8e9358..dfbca32474 100644 --- a/activesupport/lib/active_support/core_ext/numeric/bytes.rb +++ b/activesupport/lib/active_support/core_ext/numeric/bytes.rb @@ -7,36 +7,56 @@ class Numeric EXABYTE = PETABYTE * 1024 # Enables the use of byte calculations and declarations, like 45.bytes + 2.6.megabytes + # + # 2.bytes # => 2 def bytes self end alias :byte :bytes + # Returns the number of bytes equivalent to the kilobytes provided. + # + # 2.kilobytes # => 2048 def kilobytes self * KILOBYTE end alias :kilobyte :kilobytes + # Returns the number of bytes equivalent to the megabytes provided. + # + # 2.megabytes # => 2_097_152 def megabytes self * MEGABYTE end alias :megabyte :megabytes + # Returns the number of bytes equivalent to the gigabytes provided. + # + # 2.gigabytes # => 2_147_483_648 def gigabytes self * GIGABYTE end alias :gigabyte :gigabytes + # Returns the number of bytes equivalent to the terabytes provided. + # + # 2.terabytes # => 2_199_023_255_552 def terabytes self * TERABYTE end alias :terabyte :terabytes + # Returns the number of bytes equivalent to the petabytes provided. + # + # 2.petabytes # => 2_251_799_813_685_248 def petabytes self * PETABYTE end alias :petabyte :petabytes + # Returns the number of bytes equivalent to the exabytes provided. + # + # 2.exabytes # => 2_305_843_009_213_693_952 def exabytes self * EXABYTE end diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb index 6d3635c69a..0c8ff79237 100644 --- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb +++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb @@ -118,18 +118,28 @@ class Numeric end end - [Float, Fixnum, Bignum, BigDecimal].each do |klass| - klass.send(:alias_method, :to_default_s, :to_s) - - klass.send(:define_method, :to_s) do |*args| - if args[0].is_a?(Symbol) - format = args[0] - options = args[1] || {} + [Fixnum, Bignum].each do |klass| + klass.class_eval do + alias_method :to_default_s, :to_s + def to_s(base_or_format = 10, options = nil) + if base_or_format.is_a?(Symbol) + to_formatted_s(base_or_format, options || {}) + else + to_default_s(base_or_format) + end + end + end + end - self.to_formatted_s(format, options) + Float.class_eval do + alias_method :to_default_s, :to_s + def to_s(*args) + if args.empty? + to_default_s else - to_default_s(*args) + to_formatted_s(*args) end end end + end diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 87b9a23aef..ef32817f55 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -36,44 +36,52 @@ class Numeric end alias :second :seconds + # Returns a Duration instance matching the number of minutes provided. + # + # 2.minutes # => 120 seconds def minutes ActiveSupport::Duration.new(self * 60, [[:seconds, self * 60]]) end alias :minute :minutes + # Returns a Duration instance matching the number of hours provided. + # + # 2.hours # => 7_200 seconds def hours ActiveSupport::Duration.new(self * 3600, [[:seconds, self * 3600]]) end alias :hour :hours + # Returns a Duration instance matching the number of days provided. + # + # 2.days # => 2 days def days ActiveSupport::Duration.new(self * 24.hours, [[:days, self]]) end alias :day :days + # Returns a Duration instance matching the number of weeks provided. + # + # 2.weeks # => 14 days def weeks ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]]) end alias :week :weeks + # Returns a Duration instance matching the number of fortnights provided. + # + # 2.fortnights # => 28 days def fortnights ActiveSupport::Duration.new(self * 2.weeks, [[:days, self * 14]]) end alias :fortnight :fortnights - # Reads best without arguments: 10.minutes.ago - def ago(time = ::Time.current) - time - self - end - - # Reads best with argument: 10.minutes.until(time) - alias :until :ago - - # Reads best with argument: 10.minutes.since(time) - def since(time = ::Time.current) - time + self + # Returns the number of milliseconds equivalent to the seconds provided. + # Used with the standard time durations, like 1.hour.in_milliseconds -- + # so we can feed them to JavaScript functions like getTime(). + # + # 2.in_milliseconds # => 2_000 + def in_milliseconds + self * 1000 end - - # Reads best without arguments: 10.minutes.from_now - alias :from_now :since end diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb index ec2157221f..f4f9152d6a 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -8,7 +8,7 @@ require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/object/with_options' diff --git a/activesupport/lib/active_support/core_ext/object/acts_like.rb b/activesupport/lib/active_support/core_ext/object/acts_like.rb index fcc8e50f06..3912cc5ace 100644 --- a/activesupport/lib/active_support/core_ext/object/acts_like.rb +++ b/activesupport/lib/active_support/core_ext/object/acts_like.rb @@ -1,9 +1,9 @@ class Object # A duck-type assistant method. For example, Active Support extends Date - # to define an acts_like_date? method, and extends Time to define - # acts_like_time?. As a result, we can do "x.acts_like?(:time)" and - # "x.acts_like?(:date)" to do duck-type-safe comparisons, since classes that - # we want to act like Time simply need to define an acts_like_time? method. + # to define an <tt>acts_like_date?</tt> method, and extends Time to define + # <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and + # <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that + # we want to act like Time simply need to define an <tt>acts_like_time?</tt> method. def acts_like?(duck) respond_to? :"acts_like_#{duck}?" end diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index 8a5eb4bc93..38e43478df 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -4,36 +4,42 @@ class Object # An object is blank if it's false, empty, or a whitespace string. # For example, '', ' ', +nil+, [], and {} are all blank. # - # This simplifies: + # This simplifies # - # if address.nil? || address.empty? + # address.nil? || address.empty? # - # ...to: + # to # - # if address.blank? + # address.blank? + # + # @return [true, false] def blank? - respond_to?(:empty?) ? empty? : !self + respond_to?(:empty?) ? !!empty? : !self end - # An object is present if it's not <tt>blank?</tt>. + # An object is present if it's not blank. + # + # @return [true, false] def present? !blank? end - # Returns object if it's <tt>present?</tt> otherwise returns +nil+. - # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>. + # Returns the receiver if it's present otherwise returns +nil+. + # <tt>object.presence</tt> is equivalent to # - # This is handy for any representation of objects where blank is the same - # as not present at all. For example, this simplifies a common check for - # HTTP POST/query parameters: + # object.present? ? object : nil + # + # For example, something like # # state = params[:state] if params[:state].present? # country = params[:country] if params[:country].present? # region = state || country || 'US' # - # ...becomes: + # becomes # # region = params[:state].presence || params[:country].presence || 'US' + # + # @return [Object] def presence self if present? end @@ -43,6 +49,8 @@ class NilClass # +nil+ is blank: # # nil.blank? # => true + # + # @return [true] def blank? true end @@ -52,6 +60,8 @@ class FalseClass # +false+ is blank: # # false.blank? # => true + # + # @return [true] def blank? true end @@ -61,6 +71,8 @@ class TrueClass # +true+ is not blank: # # true.blank? # => false + # + # @return [false] def blank? false end @@ -71,6 +83,8 @@ class Array # # [].blank? # => true # [1,2,3].blank? # => false + # + # @return [true, false] alias_method :blank?, :empty? end @@ -79,18 +93,28 @@ class Hash # # {}.blank? # => true # { key: 'value' }.blank? # => false + # + # @return [true, false] alias_method :blank?, :empty? end class String + BLANK_RE = /\A[[:space:]]*\z/ + # A string is blank if it's empty or contains whitespaces only: # - # ''.blank? # => true - # ' '.blank? # => true - # ' '.blank? # => true - # ' something here '.blank? # => false + # ''.blank? # => true + # ' '.blank? # => true + # "\t\n\r".blank? # => true + # ' blah '.blank? # => false + # + # Unicode whitespace is supported: + # + # "\u00a0".blank? # => true + # + # @return [true, false] def blank? - self !~ /[^[:space:]]/ + BLANK_RE === self end end @@ -99,6 +123,8 @@ class Numeric #:nodoc: # # 1.blank? # => false # 0.blank? # => false + # + # @return [false] def blank? false end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 1d639f3af6..0191d2e973 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -8,8 +8,8 @@ class Object # dup = object.deep_dup # dup.instance_variable_set(:@a, 1) # - # object.instance_variable_defined?(:@a) #=> false - # dup.instance_variable_defined?(:@a) #=> true + # object.instance_variable_defined?(:@a) # => false + # dup.instance_variable_defined?(:@a) # => true def deep_dup duplicable? ? dup : self end @@ -22,10 +22,10 @@ class Array # dup = array.deep_dup # dup[1][2] = 4 # - # array[1][2] #=> nil - # dup[1][2] #=> 4 + # array[1][2] # => nil + # dup[1][2] # => 4 def deep_dup - map { |it| it.deep_dup } + map(&:deep_dup) end end @@ -36,8 +36,8 @@ class Hash # dup = hash.deep_dup # dup[:a][:c] = 'c' # - # hash[:a][:c] #=> nil - # dup[:a][:c] #=> "c" + # hash[:a][:c] # => nil + # dup[:a][:c] # => "c" def deep_dup each_with_object(dup) do |(key, value), hash| hash[key.deep_dup] = value.deep_dup diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb index 9cd7485e2e..665cb0f96d 100644 --- a/activesupport/lib/active_support/core_ext/object/duplicable.rb +++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb @@ -19,7 +19,7 @@ class Object # Can you safely dup this object? # - # False for +nil+, +false+, +true+, symbol, and number objects; + # False for +nil+, +false+, +true+, symbol, number and BigDecimal(in 1.9.x) objects; # true otherwise. def duplicable? true @@ -78,6 +78,9 @@ end require 'bigdecimal' class BigDecimal + # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead + # raises TypeError exception. Checking here on the runtime whether BigDecimal + # will allow dup or not. begin BigDecimal.new('4.56').dup @@ -88,3 +91,13 @@ class BigDecimal # can't dup, so use superclass implementation end end + +class Method + # Methods are not duplicable: + # + # method(:puts).duplicable? # => false + # method(:puts).dup # => TypeError: allocator undefined for Method + def duplicable? + false + end +end diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb index 3fec465ec0..55f281b213 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -1,25 +1,27 @@ class Object - # Returns true if this object is included in the argument(s). Argument must be - # any object which responds to +#include?+ or optionally, multiple arguments can be passed in. Usage: + # Returns true if this object is included in the argument. Argument must be + # any object which responds to +#include?+. Usage: # - # characters = ['Konata', 'Kagami', 'Tsukasa'] - # 'Konata'.in?(characters) # => true + # characters = ["Konata", "Kagami", "Tsukasa"] + # "Konata".in?(characters) # => true # - # character = 'Konata' - # character.in?('Konata', 'Kagami', 'Tsukasa') # => true - # - # This will throw an ArgumentError if a single argument is passed in and it doesn't respond + # This will throw an ArgumentError if the argument doesn't respond # to +#include?+. - def in?(*args) - if args.length > 1 - args.include? self - else - another_object = args.first - if another_object.respond_to? :include? - another_object.include? self - else - raise ArgumentError.new 'The single parameter passed to #in? must respond to #include?' - end - end + def in?(another_object) + another_object.include?(self) + rescue NoMethodError + raise ArgumentError.new("The parameter passed to #in? must respond to #include?") + end + + # Returns the receiver if it's included in the argument otherwise returns +nil+. + # Argument must be any object which responds to +#include?+. Usage: + # + # params[:bucket_type].presence_in %w( project calendar ) + # + # This will throw an ArgumentError if the argument doesn't respond to +#include?+. + # + # @return [Object] + def presence_in(another_object) + self.in?(another_object) ? self : nil end end diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb index 40821fd619..593a7a4940 100644 --- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb +++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb @@ -13,7 +13,7 @@ class Object Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }] end - # Returns an array of instance variable names including "@". + # Returns an array of instance variable names as strings including "@". # # class C # def initialize(x, y) @@ -23,6 +23,6 @@ class Object # # C.new(0, 1).instance_variable_names # => ["@y", "@x"] def instance_variable_names - instance_variables.map { |var| var.to_s } + instance_variables.map(&:to_s) end end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb new file mode 100644 index 0000000000..698b2d1920 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -0,0 +1,197 @@ +# Hack to load json gem first so we can overwrite its to_json. +require 'json' +require 'bigdecimal' +require 'active_support/core_ext/big_decimal/conversions' # for #to_s +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/instance_variables' +require 'time' +require 'active_support/core_ext/time/conversions' +require 'active_support/core_ext/date_time/conversions' +require 'active_support/core_ext/date/conversions' +require 'active_support/core_ext/module/aliasing' + +# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting +# their default behavior. That said, we need to define the basic to_json method in all of them, +# otherwise they will always use to_json gem implementation, which is backwards incompatible in +# several cases (for instance, the JSON implementation for Hash does not work) with inheritance +# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +# +# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the +# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always +# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the +# calls to the original to_json method. +# +# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is +# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply +# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} +# should give exactly the same results with or without active support. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass| + klass.class_eval do + def to_json_with_active_support_encoder(options = nil) + if options.is_a?(::JSON::State) + # Called from JSON.{generate,dump}, forward it to JSON gem's to_json + self.to_json_without_active_support_encoder(options) + else + # to_json is being invoked directly, use ActiveSupport's encoder + ActiveSupport::JSON.encode(self, options) + end + end + + alias_method_chain :to_json, :active_support_encoder + end +end + +class Object + def as_json(options = nil) #:nodoc: + if respond_to?(:to_hash) + to_hash.as_json(options) + else + instance_values.as_json(options) + end + end +end + +class Struct #:nodoc: + def as_json(options = nil) + Hash[members.zip(values)].as_json(options) + end +end + +class TrueClass + def as_json(options = nil) #:nodoc: + self + end +end + +class FalseClass + def as_json(options = nil) #:nodoc: + self + end +end + +class NilClass + def as_json(options = nil) #:nodoc: + self + end +end + +class String + def as_json(options = nil) #:nodoc: + self + end +end + +class Symbol + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Numeric + def as_json(options = nil) #:nodoc: + self + end +end + +class Float + # Encoding Infinity or NaN to JSON should return "null". The default returns + # "Infinity" or "NaN" which are not valid JSON. + def as_json(options = nil) #:nodoc: + finite? ? self : nil + end +end + +class BigDecimal + # A BigDecimal would be naturally represented as a JSON number. Most libraries, + # however, parse non-integer JSON numbers directly as floats. Clients using + # those libraries would get in general a wrong number and no way to recover + # other than manually inspecting the string with the JSON code itself. + # + # That's why a JSON string is returned. The JSON literal is not numeric, but + # if the other end knows by contract that the data is supposed to be a + # BigDecimal, it still has the chance to post-process the string and get the + # real value. + def as_json(options = nil) #:nodoc: + finite? ? to_s : nil + end +end + +class Regexp + def as_json(options = nil) #:nodoc: + to_s + end +end + +module Enumerable + def as_json(options = nil) #:nodoc: + to_a.as_json(options) + end +end + +class Range + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Array + def as_json(options = nil) #:nodoc: + map { |v| options ? v.as_json(options.dup) : v.as_json } + end +end + +class Hash + def as_json(options = nil) #:nodoc: + # create a subset of the hash by applying :only or :except + subset = if options + if attrs = options[:only] + slice(*Array(attrs)) + elsif attrs = options[:except] + except(*Array(attrs)) + else + self + end + else + self + end + + Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }] + end +end + +class Time + def as_json(options = nil) #:nodoc: + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + xmlschema(ActiveSupport::JSON::Encoding.time_precision) + else + %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) + end + end +end + +class Date + def as_json(options = nil) #:nodoc: + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + strftime("%Y-%m-%d") + else + strftime("%Y/%m/%d") + end + end +end + +class DateTime + def as_json(options = nil) #:nodoc: + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + xmlschema(ActiveSupport::JSON::Encoding.time_precision) + else + strftime('%Y/%m/%d %H:%M:%S %z') + end + end +end + +class Process::Status #:nodoc: + def as_json(options = nil) + { :exitstatus => exitstatus, :pid => pid } + end +end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb deleted file mode 100644 index 83cc8066e7..0000000000 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ /dev/null @@ -1,27 +0,0 @@ -# Hack to load json gem first so we can overwrite its to_json. -begin - require 'json' -rescue LoadError -end - -# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting -# their default behavior. That said, we need to define the basic to_json method in all of them, -# otherwise they will always use to_json gem implementation, which is backwards incompatible in -# several cases (for instance, the JSON implementation for Hash does not work) with inheritance -# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| - klass.class_eval do - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) - end - end -end - -module Process - class Status - def as_json(options = nil) - { :exitstatus => exitstatus, :pid => pid } - end - end -end diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 0d5f3501e5..684d4ef57e 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -1,58 +1 @@ -class Object - # Alias of <tt>to_s</tt>. - def to_param - to_s - end -end - -class NilClass - # Returns +self+. - def to_param - self - end -end - -class TrueClass - # Returns +self+. - def to_param - self - end -end - -class FalseClass - # Returns +self+. - def to_param - self - end -end - -class Array - # Calls <tt>to_param</tt> on all its elements and joins the result with - # slashes. This is used by <tt>url_for</tt> in Action Pack. - def to_param - collect { |e| e.to_param }.join '/' - end -end - -class Hash - # Returns a string representation of the receiver suitable for use as a URL - # query string: - # - # {name: 'David', nationality: 'Danish'}.to_param - # # => "name=David&nationality=Danish" - # - # An optional namespace can be passed to enclose the param names: - # - # {name: 'David', nationality: 'Danish'}.to_param('user') - # # => "user[name]=David&user[nationality]=Danish" - # - # The string pairs "key=value" that conform the query string - # are sorted lexicographically in ascending order. - # - # This method is also aliased as +to_query+. - def to_param(namespace = nil) - collect do |key, value| - value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort * '&' - end -end +require 'active_support/core_ext/object/to_query' diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 5d5fcf00e0..ec5ace4e16 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -1,27 +1,84 @@ -require 'active_support/core_ext/object/to_param' +require 'cgi' class Object - # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the - # param name. - # - # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work. + # Alias of <tt>to_s</tt>. + def to_param + to_s + end + + # Converts an object into a string suitable for use as a URL query string, + # using the given <tt>key</tt> as the param name. def to_query(key) - require 'cgi' unless defined?(CGI) && defined?(CGI::escape) "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}" end end +class NilClass + # Returns +self+. + def to_param + self + end +end + +class TrueClass + # Returns +self+. + def to_param + self + end +end + +class FalseClass + # Returns +self+. + def to_param + self + end +end + class Array + # Calls <tt>to_param</tt> on all its elements and joins the result with + # slashes. This is used by <tt>url_for</tt> in Action Pack. + def to_param + collect(&:to_param).join '/' + end + # Converts an array into a string suitable for use as a URL query string, # using the given +key+ as the param name. # # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding" def to_query(key) prefix = "#{key}[]" - collect { |value| value.to_query(prefix) }.join '&' + + if empty? + nil.to_query(prefix) + else + collect { |value| value.to_query(prefix) }.join '&' + end end end class Hash - alias_method :to_query, :to_param + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # {name: 'David', nationality: 'Danish'}.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # {name: 'David', nationality: 'Danish'}.to_query('user') + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(namespace = nil) + collect do |key, value| + unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? + value.to_query(namespace ? "#{namespace}[#{key}]" : key) + end + end.compact.sort! * '&' + end + + alias_method :to_param, :to_query end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 1079ddde98..26b8d58948 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -1,48 +1,78 @@ class Object - # Invokes the public method identified by the symbol +method+, passing it any arguments - # and/or the block specified, just like the regular Ruby <tt>Object#public_send</tt> does. + # Invokes the public method whose name goes as first argument just like + # +public_send+ does, except that if the receiver does not respond to it the + # call returns +nil+ rather than raising an exception. # - # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised - # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass. + # This method is defined to be able to write # - # This is also true if the receiving object does not implemented the tried method. It will - # return +nil+ in that case as well. + # @person.try(:name) # - # If try is called without a method to call, it will yield any given block with the object. + # instead of # - # Please also note that +try+ is defined on +Object+, therefore it won't work with - # subclasses of +BasicObject+. For example, using try with +SimpleDelegator+ will - # delegate +try+ to target instead of calling it on delegator itself. + # @person.name if @person # - # Without +try+ - # @person && @person.name - # or - # @person ? @person.name : nil + # +try+ calls can be chained: # - # With +try+ - # @person.try(:name) + # @person.try(:spouse).try(:name) + # + # instead of + # + # @person.spouse.name if @person && @person.spouse + # + # +try+ will also return +nil+ if the receiver does not respond to the method: + # + # @person.try(:non_existing_method) # => nil + # + # instead of + # + # @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil # - # +try+ also accepts arguments and/or a block, for the method it is trying - # Person.try(:find, 1) - # @people.try(:collect) {|p| p.name} + # +try+ returns +nil+ when called on +nil+ regardless of whether it responds + # to the method: # - # Without a method argument try will yield to the block unless the receiver is nil. - # @person.try { |p| "#{p.first_name} #{p.last_name}" } + # nil.try(:to_i) # => nil, rather than 0 # - # +try+ behaves like +Object#public_send+, unless called on +NilClass+. + # Arguments and blocks are forwarded to the method if invoked: + # + # @posts.try(:each_slice, 2) do |a, b| + # ... + # end + # + # The number of arguments in the signature must match. If the object responds + # to the method the call is attempted and +ArgumentError+ is still raised + # in case of argument mismatch. + # + # If +try+ is called without arguments it yields the receiver to a given + # block unless it is +nil+: + # + # @person.try do |p| + # ... + # end + # + # You can also call try with a block without accepting an argument, and the block + # will be instance_eval'ed instead: + # + # @person.try { upcase.truncate(50) } + # + # Please also note that +try+ is defined on +Object+. Therefore, it won't work + # with instances of classes that do not have +Object+ among their ancestors, + # like direct subclasses of +BasicObject+. For example, using +try+ with + # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on + # the delegator itself. def try(*a, &b) - if a.empty? && block_given? - yield self - else - public_send(*a, &b) if respond_to?(a.first) - end + try!(*a, &b) if a.empty? || respond_to?(a.first) end - # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and - # does not implemented the tried method. + # Same as #try, but will raise a NoMethodError exception if the receiver is not +nil+ and + # does not implement the tried method. + def try!(*a, &b) if a.empty? && block_given? - yield self + if b.arity.zero? + instance_eval(&b) + else + yield self + end else public_send(*a, &b) end @@ -51,12 +81,12 @@ end class NilClass # Calling +try+ on +nil+ always returns +nil+. - # It becomes specially helpful when navigating through associations that may return +nil+. + # It becomes especially helpful when navigating through associations that may return +nil+. # # nil.try(:name) # => nil # # Without +try+ - # @person && !@person.children.blank? && @person.children.first.name + # @person && @person.children.any? && @person.children.first.name # # With +try+ # @person.try(:children).try(:first).try(:name) diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 42e388b065..7d38e1d134 100644 --- a/activesupport/lib/active_support/core_ext/object/with_options.rb +++ b/activesupport/lib/active_support/core_ext/object/with_options.rb @@ -34,9 +34,36 @@ class Object # body i18n.t :body, user_name: user.name # end # + # When you don't pass an explicit receiver, it executes the whole block + # in merging options context: + # + # class Account < ActiveRecord::Base + # with_options dependent: :destroy do + # has_many :customers + # has_many :products + # has_many :invoices + # has_many :expenses + # end + # end + # # <tt>with_options</tt> can also be nested since the call is forwarded to its receiver. - # Each nesting level will merge inherited defaults in addition to their own. - def with_options(options) - yield ActiveSupport::OptionMerger.new(self, options) + # + # NOTE: Each nesting level will merge inherited defaults in addition to their own. + # + # class Post < ActiveRecord::Base + # with_options if: :persisted?, length: { minimum: 50 } do + # validates :content, if: -> { content.present? } + # end + # end + # + # The code is equivalent to: + # + # validates :content, length: { minimum: 50 }, if: -> { content.present? } + # + # Hence the inherited default for `if` key is ignored. + # + def with_options(options, &block) + option_merger = ActiveSupport::OptionMerger.new(self, options) + block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) end end diff --git a/activesupport/lib/active_support/core_ext/proc.rb b/activesupport/lib/active_support/core_ext/proc.rb deleted file mode 100644 index 166c3855a0..0000000000 --- a/activesupport/lib/active_support/core_ext/proc.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "active_support/core_ext/kernel/singleton_class" -require "active_support/deprecation" - -class Proc #:nodoc: - def bind(object) - ActiveSupport::Deprecation.warn 'Proc#bind is deprecated and will be removed in future versions' - - block, time = self, Time.now - object.class_eval do - method_name = "__bind_#{time.to_i}_#{time.usec}" - define_method(method_name, &block) - method = instance_method(method_name) - remove_method(method_name) - method - end.bind(object) - end -end diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb index 1d8b1ede5a..9368e81235 100644 --- a/activesupport/lib/active_support/core_ext/range.rb +++ b/activesupport/lib/active_support/core_ext/range.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/range/conversions' require 'active_support/core_ext/range/include_range' require 'active_support/core_ext/range/overlaps' +require 'active_support/core_ext/range/each' diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb new file mode 100644 index 0000000000..ecef78f55f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -0,0 +1,23 @@ +require 'active_support/core_ext/module/aliasing' + +class Range #:nodoc: + + def each_with_time_with_zone(&block) + ensure_iteration_allowed + each_without_time_with_zone(&block) + end + alias_method_chain :each, :time_with_zone + + def step_with_time_with_zone(n = 1, &block) + ensure_iteration_allowed + step_without_time_with_zone(n, &block) + end + alias_method_chain :step, :time_with_zone + + private + def ensure_iteration_allowed + if first.is_a?(Time) + raise TypeError, "can't iterate from #{first.class}" + end + end +end diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb index 3af66aaf2f..3a07401c8a 100644 --- a/activesupport/lib/active_support/core_ext/range/include_range.rb +++ b/activesupport/lib/active_support/core_ext/range/include_range.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/module/aliasing' + class Range # Extends the default Range#include? to support range comparisons. # (1..5).include?(1..5) # => true diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb index 5d7cb81e38..c656db2c6c 100644 --- a/activesupport/lib/active_support/core_ext/string.rb +++ b/activesupport/lib/active_support/core_ext/string.rb @@ -4,7 +4,6 @@ require 'active_support/core_ext/string/multibyte' require 'active_support/core_ext/string/starts_ends_with' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/string/access' -require 'active_support/core_ext/string/xchar' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/string/output_safety' require 'active_support/core_ext/string/exclude' diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 8fa8157d65..ebd0dd3fc7 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -8,22 +8,22 @@ class String # the beginning of the range is greater than the end of the string. # # str = "hello" - # str.at(0) #=> "h" - # str.at(1..3) #=> "ell" - # str.at(-2) #=> "l" - # str.at(-2..-1) #=> "lo" - # str.at(5) #=> nil - # str.at(5..-1) #=> "" + # str.at(0) # => "h" + # str.at(1..3) # => "ell" + # str.at(-2) # => "l" + # str.at(-2..-1) # => "lo" + # str.at(5) # => nil + # str.at(5..-1) # => "" # # If a Regexp is given, the matching portion of the string is returned. # If a String is given, that given string is returned if it occurs in # the string. In both cases, nil is returned if there is no match. # # str = "hello" - # str.at(/lo/) #=> "lo" - # str.at(/ol/) #=> nil - # str.at("lo") #=> "lo" - # str.at("ol") #=> nil + # str.at(/lo/) # => "lo" + # str.at(/ol/) # => nil + # str.at("lo") # => "lo" + # str.at("ol") # => nil def at(position) self[position] end @@ -32,15 +32,15 @@ class String # If the position is negative, it is counted from the end of the string. # # str = "hello" - # str.from(0) #=> "hello" - # str.from(3) #=> "lo" - # str.from(-2) #=> "lo" + # str.from(0) # => "hello" + # str.from(3) # => "lo" + # str.from(-2) # => "lo" # # You can mix it with +to+ method and do fun things like: # # str = "hello" - # str.from(0).to(-1) #=> "hello" - # str.from(1).to(-2) #=> "ell" + # str.from(0).to(-1) # => "hello" + # str.from(1).to(-2) # => "ell" def from(position) self[position..-1] end @@ -49,34 +49,34 @@ class String # If the position is negative, it is counted from the end of the string. # # str = "hello" - # str.to(0) #=> "h" - # str.to(3) #=> "hell" - # str.to(-2) #=> "hell" + # str.to(0) # => "h" + # str.to(3) # => "hell" + # str.to(-2) # => "hell" # # You can mix it with +from+ method and do fun things like: # # str = "hello" - # str.from(0).to(-1) #=> "hello" - # str.from(1).to(-2) #=> "ell" + # str.from(0).to(-1) # => "hello" + # str.from(1).to(-2) # => "ell" def to(position) self[0..position] end # Returns the first character. If a limit is supplied, returns a substring # from the beginning of the string until it reaches the limit value. If the - # given limit is greater than or equal to the string length, returns self. + # given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" - # str.first #=> "h" - # str.first(1) #=> "h" - # str.first(2) #=> "he" - # str.first(0) #=> "" - # str.first(6) #=> "hello" + # str.first # => "h" + # str.first(1) # => "h" + # str.first(2) # => "he" + # str.first(0) # => "" + # str.first(6) # => "hello" def first(limit = 1) if limit == 0 '' elsif limit >= size - self + self.dup else to(limit - 1) end @@ -84,19 +84,19 @@ class String # Returns the last character of the string. If a limit is supplied, returns a substring # from the end of the string until it reaches the limit value (counting backwards). If - # the given limit is greater than or equal to the string length, returns self. + # the given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" - # str.last #=> "o" - # str.last(1) #=> "o" - # str.last(2) #=> "lo" - # str.last(0) #=> "" - # str.last(6) #=> "hello" + # str.last # => "o" + # str.last(1) # => "o" + # str.last(2) # => "lo" + # str.last(0) # => "" + # str.last(6) # => "hello" def last(limit = 1) if limit == 0 '' elsif limit >= size - self + self.dup else from(-limit) end diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 9d3b81cf38..3e0cb8a7ac 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -3,56 +3,54 @@ require 'active_support/core_ext/time/calculations' class String # Converts a string to a Time value. - # The +form+ can be either :utc or :local (default :utc). + # The +form+ can be either :utc or :local (default :local). # - # The time is parsed using Date._parse method. - # If +form+ is :local, then time is formatted using Time.zone + # The time is parsed using Time.parse method. + # If +form+ is :local, then the time is in the system timezone. + # If the date part is missing then the current date is used and if + # the time part is missing then it is assumed to be 00:00:00. # - # "3-2-2012".to_time # => 2012-02-03 00:00:00 UTC - # "12:20".to_time # => ArgumentError: invalid date - # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 UTC - # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 UTC - # "2012-12-13T06:12".to_time(:local) # => 2012-12-13 06:12:00 +0100 - def to_time(form = :utc) - unless blank? - date_values = ::Date._parse(self, false). - values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset). - map! { |arg| arg || 0 } - date_values[6] *= 1000000 - offset = date_values.pop + # "13-12-2012".to_time # => 2012-12-13 00:00:00 +0100 + # "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 + # "12/13/2012".to_time # => ArgumentError: argument out of range + def to_time(form = :local) + parts = Date._parse(self, false) + return if parts.empty? - ::Time.send(form, *date_values) - offset - end + now = Time.now + time = Time.new( + parts.fetch(:year, now.year), + parts.fetch(:mon, now.month), + parts.fetch(:mday, now.day), + parts.fetch(:hour, 0), + parts.fetch(:min, 0), + parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), + parts.fetch(:offset, form == :utc ? 0 : nil) + ) + + form == :utc ? time.utc : time.getlocal end # Converts a string to a Date value. # - # "1-1-2012".to_date #=> Sun, 01 Jan 2012 - # "01/01/2012".to_date #=> Sun, 01 Jan 2012 - # "2012-12-13".to_date #=> Thu, 13 Dec 2012 - # "12/13/2012".to_date #=> ArgumentError: invalid date + # "1-1-2012".to_date # => Sun, 01 Jan 2012 + # "01/01/2012".to_date # => Sun, 01 Jan 2012 + # "2012-12-13".to_date # => Thu, 13 Dec 2012 + # "12/13/2012".to_date # => ArgumentError: invalid date def to_date - unless blank? - date_values = ::Date._parse(self, false).values_at(:year, :mon, :mday) - - ::Date.new(*date_values) - end + ::Date.parse(self, false) unless blank? end # Converts a string to a DateTime value. # - # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000 - # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000 - # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000 - # "12/13/2012".to_datetime #=> ArgumentError: invalid date + # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000 + # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000 + # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000 + # "12/13/2012".to_datetime # => ArgumentError: invalid date def to_datetime - unless blank? - date_values = ::Date._parse(self, false). - values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :sec_fraction). - map! { |arg| arg || 0 } - date_values[5] += date_values.pop - - ::DateTime.civil(*date_values) - end + ::DateTime.parse(self, false) unless blank? end end diff --git a/activesupport/lib/active_support/core_ext/string/encoding.rb b/activesupport/lib/active_support/core_ext/string/encoding.rb deleted file mode 100644 index a583b914db..0000000000 --- a/activesupport/lib/active_support/core_ext/string/encoding.rb +++ /dev/null @@ -1,8 +0,0 @@ -require 'active_support/deprecation' - -class String - def encoding_aware? - ActiveSupport::Deprecation.warn 'String#encoding_aware? is deprecated' - true - end -end diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb index 114bcb87f0..0ac684f6ee 100644 --- a/activesupport/lib/active_support/core_ext/string/exclude.rb +++ b/activesupport/lib/active_support/core_ext/string/exclude.rb @@ -2,9 +2,9 @@ class String # The inverse of <tt>String#include?</tt>. Returns true if the string # does not include the other string. # - # "hello".exclude? "lo" #=> false - # "hello".exclude? "ol" #=> true - # "hello".exclude? ?h #=> false + # "hello".exclude? "lo" # => false + # "hello".exclude? "ol" # => true + # "hello".exclude? ?h # => false def exclude?(string) !include?(string) end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index e05447439a..096292dc58 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -3,6 +3,8 @@ class String # the string, and then changing remaining consecutive whitespace # groups into one space each. # + # Note that it handles both ASCII and Unicode whitespace. + # # %{ Multi-line # string }.squish # => "Multi-line string" # " foo bar \n \t boo".squish # => "foo bar boo" @@ -11,9 +13,33 @@ class String end # Performs a destructive squish. See String#squish. + # str = " foo bar \n \t boo" + # str.squish! # => "foo bar boo" + # str # => "foo bar boo" def squish! - strip! - gsub!(/\s+/, ' ') + gsub!(/\A[[:space:]]+/, '') + gsub!(/[[:space:]]+\z/, '') + gsub!(/[[:space:]]+/, ' ') + self + end + + # Returns a new string with all occurrences of the patterns removed. + # str = "foo bar test" + # str.remove(" test") # => "foo bar" + # str # => "foo bar test" + def remove(*patterns) + dup.remove!(*patterns) + end + + # Alters the string by removing all occurrences of the patterns. + # str = "foo bar test" + # str.remove!(" test") # => "foo bar" + # str # => "foo bar" + def remove!(*patterns) + patterns.each do |pattern| + gsub! pattern, "" + end + self end @@ -38,8 +64,8 @@ class String def truncate(truncate_at, options = {}) return dup unless length > truncate_at - options[:omission] ||= '...' - length_with_room_for_omission = truncate_at - options[:omission].length + omission = options[:omission] || '...' + length_with_room_for_omission = truncate_at - omission.length stop = \ if options[:separator] rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission @@ -47,6 +73,30 @@ class String length_with_room_for_omission end - self[0...stop] + options[:omission] + "#{self[0, stop]}#{omission}" + end + + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): + # + # 'Once upon a time in a world far far away'.truncate_words(4) + # # => "Once upon a time..." + # + # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words: + # + # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>') + # # => "Once<br>upon<br>a<br>time<br>in..." + # + # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."): + # + # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)') + # # => "And they found that many... (continued)" + def truncate_words(words_count, options = {}) + sep = options[:separator] || /\s+/ + sep = Regexp.escape(sep.to_s) unless Regexp === sep + if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m + $1 + (options[:omission] || '...') + else + dup + end end end diff --git a/activesupport/lib/active_support/core_ext/string/indent.rb b/activesupport/lib/active_support/core_ext/string/indent.rb index afc3032272..ce3a69cf5f 100644 --- a/activesupport/lib/active_support/core_ext/string/indent.rb +++ b/activesupport/lib/active_support/core_ext/string/indent.rb @@ -29,7 +29,7 @@ class String # "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar" # "foo".indent(2, "\t") # => "\t\tfoo" # - # While +indent_string+ is tipically one space or tab, it may be any string. + # While +indent_string+ is typically one space or tab, it may be any string. # # The third argument, +indent_empty_lines+, is a flag that says whether # empty lines should be indented. Default is false. diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 6522145572..38d567c014 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -31,7 +31,7 @@ class String def pluralize(count = nil, locale = :en) locale = count if count.is_a?(Symbol) if count == 1 - self + self.dup else ActiveSupport::Inflector.pluralize(self, locale) end @@ -41,7 +41,7 @@ class String # # If the optional parameter +locale+ is specified, # the word will be singularized as a word of that language. - # By default, this paramter is set to <tt>:en</tt>. + # By default, this parameter is set to <tt>:en</tt>. # You must define your own inflection rules for languages other than English. # # 'posts'.singularize # => "post" @@ -130,6 +130,8 @@ class String # # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => '' # # See also +deconstantize+. def demodulize @@ -182,21 +184,24 @@ class String # # 'egg_and_hams'.classify # => "EggAndHam" # 'posts'.classify # => "Post" - # - # Singular names are not handled correctly. - # - # 'business'.classify # => "Busines" def classify ActiveSupport::Inflector.classify(self) end - # Capitalizes the first word, turns underscores into spaces, and strips '_id'. + # Capitalizes the first word, turns underscores into spaces, and strips a + # trailing '_id' if present. # Like +titleize+, this is meant for creating pretty output. # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - def humanize - ActiveSupport::Inflector.humanize(self) + # The capitalization of the first word can be turned off by setting the + # optional parameter +capitalize+ to false. + # By default, this parameter is true. + # + # 'employee_salary'.humanize # => "Employee salary" + # 'author_id'.humanize # => "Author" + # 'author_id'.humanize(capitalize: false) # => "author" + # '_id'.humanize # => "Id" + def humanize(options = {}) + ActiveSupport::Inflector.humanize(self, options) end # Creates a foreign key name from a class name. diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb index 5f85cedcf5..231eaedbba 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -1,12 +1,14 @@ require 'erb' require 'active_support/core_ext/kernel/singleton_class' +require 'active_support/deprecation' class ERB module Util HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' } - HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ - JSON_ESCAPE_REGEXP = /[&"><]/ + JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' } + HTML_ESCAPE_REGEXP = /[&"'><]/ + HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/ + JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u # A utility method for escaping HTML tag characters. # This method is also aliased as <tt>h</tt>. @@ -17,12 +19,7 @@ class ERB # puts html_escape('is a > 0 & a < 10?') # # => is a > 0 & a < 10? def html_escape(s) - s = s.to_s - if s.html_safe? - s - else - s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe - end + unwrapped_html_escape(s).html_safe end # Aliasing twice issues a warning "discarding old...". Remove first to avoid it. @@ -34,6 +31,18 @@ class ERB singleton_class.send(:remove_method, :html_escape) module_function :html_escape + # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer. + # This method is not for public consumption! Seriously! + def unwrapped_html_escape(s) # :nodoc: + s = s.to_s + if s.html_safe? + s + else + s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE) + end + end + module_function :unwrapped_html_escape + # A utility method for escaping HTML without affecting existing escaped entities. # # html_escape_once('1 < 2 & 3') @@ -42,25 +51,64 @@ class ERB # html_escape_once('<< Accept & Checkout') # # => "<< Accept & Checkout" def html_escape_once(s) - result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP) { |special| HTML_ESCAPE[special] } + result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) s.html_safe? ? result.html_safe : result end module_function :html_escape_once - # A utility method for escaping HTML entities in JSON strings - # using \uXXXX JavaScript escape sequences for string literals: + # A utility method for escaping HTML entities in JSON strings. Specifically, the + # &, > and < characters are replaced with their equivalent unicode escaped form - + # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also + # escaped as they are treated as newline characters in some JavaScript engines. + # These sequences have identical meaning as the original characters inside the + # context of a JSON string, so assuming the input is a valid and well-formed + # JSON value, the output will have equivalent meaning when parsed: + # + # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"}) + # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}" + # + # json_escape(json) + # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}" + # + # JSON.parse(json) == JSON.parse(json_escape(json)) + # # => true + # + # The intended use case for this method is to escape JSON strings before including + # them inside a script tag to avoid XSS vulnerability: + # + # <script> + # var currentUser = <%= raw json_escape(current_user.to_json) %>; + # </script> # - # json_escape('is a > 0 & a < 10?') - # # => is a \u003E 0 \u0026 a \u003C 10? + # It is necessary to +raw+ the result of +json_escape+, so that quotation marks + # don't get converted to <tt>"</tt> entities. +json_escape+ doesn't + # automatically flag the result as HTML safe, since the raw value is unsafe to + # use inside HTML attributes. # - # Note that after this operation is performed the output is not - # valid JSON. In particular double quotes are removed: + # If you need to output JSON elsewhere in your HTML, you can just do something + # like this, as any unsafe characters (including quotation marks) will be + # automatically escaped for you: # - # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}') - # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1} + # <div data-user-info="<%= current_user.to_json %>">...</div> + # + # WARNING: this helper only works with valid JSON. Using this on non-JSON values + # will open up serious XSS vulnerabilities. For example, if you replace the + # +current_user.to_json+ in the example above with user input instead, the browser + # will happily eval() that string as JavaScript. + # + # The escaping performed in this method is identical to those performed in the + # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is + # set to true. Because this transformation is idempotent, this helper can be + # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true. + # + # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+ + # is enabled, or if you are unsure where your JSON string originated from, it + # is recommended that you always apply this helper (other libraries, such as the + # JSON gem, do not provide this kind of protection by default; also some gems + # might override +to_json+ to bypass Active Support's encoder). def json_escape(s) - result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] } + result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE) s.html_safe? ? result.html_safe : result end @@ -84,7 +132,7 @@ module ActiveSupport #:nodoc: class SafeBuffer < String UNSAFE_STRING_METHODS = %w( capitalize chomp chop delete downcase gsub lstrip next reverse rstrip - slice squeeze strip sub succ swapcase tr tr_s upcase prepend + slice squeeze strip sub succ swapcase tr tr_s upcase ) alias_method :original_concat, :concat @@ -102,7 +150,11 @@ module ActiveSupport #:nodoc: else if html_safe? new_safe_buffer = super - new_safe_buffer.instance_eval { @html_safe = true } + + if new_safe_buffer + new_safe_buffer.instance_variable_set :@html_safe, true + end + new_safe_buffer else to_str[*args] @@ -130,28 +182,32 @@ module ActiveSupport #:nodoc: end def concat(value) - if !html_safe? || value.html_safe? - super(value) - else - super(ERB::Util.h(value)) - end + super(html_escape_interpolated_argument(value)) end alias << concat + def prepend(value) + super(html_escape_interpolated_argument(value)) + end + + def prepend!(value) + ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend + prepend value + end + def +(other) dup.concat(other) end def %(args) - args = Array(args).map do |arg| - if !html_safe? || arg.html_safe? - arg - else - ERB::Util.h(arg) - end + case args + when Hash + escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }] + else + escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) } end - self.class.new(super(args)) + self.class.new(super(escaped_args)) end def html_safe? @@ -171,7 +227,7 @@ module ActiveSupport #:nodoc: end UNSAFE_STRING_METHODS.each do |unsafe_method| - if 'String'.respond_to?(unsafe_method) + if unsafe_method.respond_to?(unsafe_method) class_eval <<-EOT, __FILE__, __LINE__ + 1 def #{unsafe_method}(*args, &block) # def capitalize(*args, &block) to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block) @@ -184,10 +240,22 @@ module ActiveSupport #:nodoc: EOT end end + + private + + def html_escape_interpolated_argument(arg) + (!html_safe? || arg.html_safe?) ? arg : + arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE) + end end end class String + # Marks a string as trusted safe. It will be inserted into HTML with no + # additional escaping performed. It is your responsibilty to ensure that the + # string contains no malicious content. This method is equivalent to the + # `raw` helper in views. It is recommended that you use `sanitize` instead of + # this method. It should never be called on user input. def html_safe ActiveSupport::SafeBuffer.new(self) end diff --git a/activesupport/lib/active_support/core_ext/string/xchar.rb b/activesupport/lib/active_support/core_ext/string/xchar.rb deleted file mode 100644 index f9a5b4fb64..0000000000 --- a/activesupport/lib/active_support/core_ext/string/xchar.rb +++ /dev/null @@ -1,18 +0,0 @@ -begin - # See http://fast-xs.rubyforge.org/ by Eric Wong. - # Also included with hpricot. - require 'fast_xs' -rescue LoadError - # fast_xs extension unavailable -else - begin - require 'builder' - rescue LoadError - # builder demands the first shot at defining String#to_xs - end - - class String - alias_method :original_xs, :to_xs if method_defined?(:to_xs) - alias_method :to_xs, :fast_xs - end -end diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb index e3f20eee29..510c884c18 100644 --- a/activesupport/lib/active_support/core_ext/string/zones.rb +++ b/activesupport/lib/active_support/core_ext/string/zones.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/string/conversions' require 'active_support/core_ext/time/zones' class String diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb deleted file mode 100644 index 5481766f10..0000000000 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ /dev/null @@ -1,74 +0,0 @@ -class Thread - LOCK = Mutex.new # :nodoc: - - # Returns the value of a thread local variable that has been set. Note that - # these are different than fiber local values. - # - # Thread local values are carried along with threads, and do not respect - # fibers. For example: - # - # Thread.new { - # Thread.current.thread_variable_set("foo", "bar") # set a thread local - # Thread.current["foo"] = "bar" # set a fiber local - # - # Fiber.new { - # Fiber.yield [ - # Thread.current.thread_variable_get("foo"), # get the thread local - # Thread.current["foo"], # get the fiber local - # ] - # }.resume - # }.join.value # => ['bar', nil] - # - # The value <tt>"bar"</tt> is returned for the thread local, where +nil+ is returned - # for the fiber local. The fiber is executed in the same thread, so the - # thread local values are available. - def thread_variable_get(key) - locals[key.to_sym] - end - - # Sets a thread local with +key+ to +value+. Note that these are local to - # threads, and not to fibers. Please see Thread#thread_variable_get for - # more information. - def thread_variable_set(key, value) - locals[key.to_sym] = value - end - - # Returns an an array of the names of the thread-local variables (as Symbols). - # - # thr = Thread.new do - # Thread.current.thread_variable_set(:cat, 'meow') - # Thread.current.thread_variable_set("dog", 'woof') - # end - # thr.join #=> #<Thread:0x401b3f10 dead> - # thr.thread_variables #=> [:dog, :cat] - # - # Note that these are not fiber local variables. Please see Thread#thread_variable_get - # for more details. - def thread_variables - locals.keys - end - - # Returns <tt>true</tt> if the given string (or symbol) exists as a - # thread-local variable. - # - # me = Thread.current - # me.thread_variable_set(:oliver, "a") - # me.thread_variable?(:oliver) #=> true - # me.thread_variable?(:stanley) #=> false - # - # Note that these are not fiber local variables. Please see Thread#thread_variable_get - # for more details. - def thread_variable?(key) - locals.has_key?(key.to_sym) - end - - private - - def locals - if defined?(@locals) - @locals - else - LOCK.synchronize { @locals ||= {} } - end - end -end unless Thread.instance_methods.include?(:thread_variable_set) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 1f95f62229..ab8307429a 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -3,7 +3,6 @@ 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/deprecation' class Time include DateAndTime::Calculations @@ -26,45 +25,27 @@ class Time end end - # *DEPRECATED*: Use +Time#utc+ or +Time#local+ instead. - # - # Returns a new Time if requested year can be accommodated by Ruby's Time class - # (i.e., if year is within either 1970..2038 or 1902..2038, depending on system architecture); - # otherwise returns a DateTime. - def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0) - ActiveSupport::Deprecation.warn 'time_with_datetime_fallback is deprecated. Use Time#utc or Time#local instead', caller - time = ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec) - - # This check is needed because Time.utc(y) returns a time object in the 2000s for 0 <= y <= 138. - if time.year == year - time - else - ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) - end - rescue - ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec) + # 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 end - # *DEPRECATED*: Use +Time#utc+ instead. - # - # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:utc</tt>. - def utc_time(*args) - ActiveSupport::Deprecation.warn 'utc_time is deprecated. Use Time#utc instead', caller - time_with_datetime_fallback(:utc, *args) - end + # Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime + # instances can be used when called with a single argument + def at_with_coercion(*args) + return at_without_coercion(*args) if args.size != 1 - # *DEPRECATED*: Use +Time#local+ instead. - # - # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:local</tt>. - def local_time(*args) - ActiveSupport::Deprecation.warn 'local_time is deprecated. Use Time#local instead', caller - time_with_datetime_fallback(:local, *args) - end + # Time.at can be called with a time or numerical value + time_or_number = args.first - # 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 + if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime) + at_without_coercion(time_or_number.to_f).getlocal + else + at_without_coercion(time_or_number) + end end + alias_method :at_without_coercion, :at + alias_method :at, :at_with_coercion end # Seconds since midnight: Time.now.seconds_since_midnight @@ -83,11 +64,12 @@ class Time # Returns a new Time where one or more of the elements have been changed according # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>) reset cascadingly, so if only the hour is passed, - # then minute, sec, and usec is set to 0. If the hour and minute is passed, then - # sec and usec is set to 0. The +options+ parameter takes a hash with any of these - # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>. + # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only + # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> + # <tt>:nsec</tt>. Path either <tt>:usec</tt> or <tt>:nsec</tt>, not both. # # Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0) # Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0) @@ -99,21 +81,29 @@ class Time new_hour = options.fetch(:hour, hour) new_min = options.fetch(:min, options[:hour] ? 0 : min) new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) - new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + + if new_nsec = options[:nsec] + raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] + new_usec = Rational(new_nsec, 1000) + else + new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + end if utc? ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) elsif zone ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) else + raise ArgumentError, 'argument out of range' if new_usec > 999999 ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) end end - # Uses Date to provide precise Time calculations for years, months, and days. - # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>, - # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, - # <tt>:minutes</tt>, <tt>:seconds</tt>. + # Uses Date to provide precise Time calculations for years, months, and days + # according to the proleptic Gregorian calendar. The +options+ parameter + # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>, + # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>, + # <tt>:seconds</tt>. def advance(options) unless options[:weeks].nil? options[:weeks], partial_weeks = options[:weeks].divmod(1) @@ -126,6 +116,7 @@ class Time end d = to_date.advance(options) + d = d.gregorian if d.julian? time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ options.fetch(:seconds, 0) + @@ -161,6 +152,16 @@ class Time alias :at_midnight :beginning_of_day alias :at_beginning_of_day :beginning_of_day + # Returns a new Time representing the middle of the day (12:00) + def middle_of_day + change(:hour => 12) + end + alias :midday :middle_of_day + alias :noon :middle_of_day + alias :at_midday :middle_of_day + alias :at_noon :middle_of_day + alias :at_middle_of_day :middle_of_day + # Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9) def end_of_day change( @@ -188,30 +189,24 @@ class Time end alias :at_end_of_hour :end_of_hour - # Returns a Range representing the whole day of the current time. - def all_day - beginning_of_day..end_of_day - end - - # Returns a Range representing the whole week of the current time. - # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set. - def all_week(start_day = Date.beginning_of_week) - beginning_of_week(start_day)..end_of_week(start_day) - end - - # Returns a Range representing the whole month of the current time. - def all_month - beginning_of_month..end_of_month + # Returns a new Time representing the start of the minute (x:xx:00) + def beginning_of_minute + change(:sec => 0) end + alias :at_beginning_of_minute :beginning_of_minute - # Returns a Range representing the whole quarter of the current time. - def all_quarter - beginning_of_quarter..end_of_quarter + # Returns a new Time representing the end of the minute, x:xx:59.999999 (.999999999 in ruby1.9) + def end_of_minute + change( + :sec => 59, + :usec => Rational(999999999, 1000) + ) end + alias :at_end_of_minute :end_of_minute - # Returns a Range representing the whole year of the current time. - def all_year - beginning_of_year..end_of_year + # Returns a Range representing the whole day of the current time. + def all_day + beginning_of_day..end_of_day end def plus_with_duration(other) #:nodoc: diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 48654eb1cc..dbf1f2f373 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -16,10 +16,11 @@ class Time :rfc822 => lambda { |time| offset_format = time.formatted_offset(false) time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}") - } + }, + :iso8601 => lambda { |time| time.iso8601 } } - # Converts to a formatted string. See DATE_FORMATS for builtin formats. + # Converts to a formatted string. See DATE_FORMATS for built-in formats. # # This method is aliased to <tt>to_s</tt>. # @@ -34,6 +35,7 @@ class Time # time.to_formatted_s(:long) # => "January 18, 2007 06:10" # time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10" # time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600" + # time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00" # # == Adding your own time formats to +to_formatted_s+ # You can add your own formats to the Time::DATE_FORMATS hash. diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb index 796c5f9805..0668eadb1e 100644 --- a/activesupport/lib/active_support/core_ext/time/zones.rb +++ b/activesupport/lib/active_support/core_ext/time/zones.rb @@ -1,8 +1,9 @@ require 'active_support/time_with_zone' +require 'active_support/core_ext/time/acts_like' +require 'active_support/core_ext/date_and_time/zones' class Time - @zone_default = nil - + include DateAndTime::Zones class << self attr_accessor :zone_default @@ -50,7 +51,16 @@ class Time end end - # Returns a TimeZone instance or nil, or raises an ArgumentError for invalid timezones. + # Returns a TimeZone instance matching the time zone provided. + # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. + # Raises an ArgumentError for invalid time zones. + # + # Time.find_zone! "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> + # Time.find_zone! "EST" # => #<ActiveSupport::TimeZone @name="EST" ...> + # Time.find_zone! -5.hours # => #<ActiveSupport::TimeZone @name="Bogota" ...> + # Time.find_zone! nil # => nil + # Time.find_zone! false # => false + # Time.find_zone! "NOT-A-TIMEZONE" # => ArgumentError: Invalid Timezone: NOT-A-TIMEZONE def find_zone!(time_zone) if !time_zone || time_zone.is_a?(ActiveSupport::TimeZone) time_zone @@ -71,28 +81,14 @@ class Time raise ArgumentError, "Invalid Timezone: #{time_zone}" end + # Returns a TimeZone instance matching the time zone provided. + # Accepts the time zone in any format supported by <tt>Time.zone=</tt>. + # Returns +nil+ for invalid time zones. + # + # Time.find_zone "America/New_York" # => #<ActiveSupport::TimeZone @name="America/New_York" ...> + # Time.find_zone "NOT-A-TIMEZONE" # => nil def find_zone(time_zone) find_zone!(time_zone) rescue nil end end - - # Returns the simultaneous time in <tt>Time.zone</tt>. - # - # Time.zone = 'Hawaii' # => 'Hawaii' - # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00 - # - # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone - # instead of the operating system's time zone. - # - # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument, - # and the conversion will be based on that zone instead of <tt>Time.zone</tt>. - # - # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00 - def in_time_zone(zone = ::Time.zone) - if zone - ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone)) - else - self - end - end end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index fff4c776a9..ff8c0fd310 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -8,6 +8,7 @@ require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/module/qualified_const' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/load_error' require 'active_support/core_ext/name_error' require 'active_support/core_ext/string/starts_ends_with' @@ -29,6 +30,10 @@ module ActiveSupport #:nodoc: mattr_accessor :loaded self.loaded = Set.new + # Stack of files being loaded. + mattr_accessor :loading + self.loading = [] + # Should we load files or require them? mattr_accessor :mechanism self.mechanism = ENV['NO_RELOAD'] ? :require : :load @@ -175,14 +180,23 @@ module ActiveSupport #:nodoc: end def const_missing(const_name) - # The interpreter does not pass nesting information, and in the - # case of anonymous modules we cannot even make the trade-off of - # assuming their name reflects the nesting. Resort to Object as - # the only meaningful guess we can make. - from_mod = anonymous? ? ::Object : self + from_mod = anonymous? ? guess_for_anonymous(const_name) : self Dependencies.load_missing_constant(from_mod, const_name) end + # We assume that the name of the module reflects the nesting + # (unless it can be proven that is not the case) and the path to the file + # that defines the constant. Anonymous modules cannot follow these + # conventions and therefore we assume that the user wants to refer to a + # top-level constant. + def guess_for_anonymous(const_name) + if Object.const_defined?(const_name) + raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name + else + Object + end + end + def unloadable(const_desc = self) super(const_desc) end @@ -191,16 +205,29 @@ module ActiveSupport #:nodoc: # Object includes this module. module Loadable #:nodoc: def self.exclude_from(base) - base.class_eval { define_method(:load, Kernel.instance_method(:load)) } + base.class_eval do + define_method(:load, Kernel.instance_method(:load)) + private :load + end end def require_or_load(file_name) Dependencies.require_or_load(file_name) end + # Interprets a file using <tt>mechanism</tt> and marks its defined + # constants as autoloaded. <tt>file_name</tt> can be either a string or + # respond to <tt>to_path</tt>. + # + # Use this method in code that absolutely needs a certain constant to be + # defined at that point. A typical use case is to make constant name + # resolution deterministic for constants with the same relative name in + # different namespaces whose evaluation would depend on load order + # otherwise. def require_dependency(file_name, message = "No such file to load -- %s") + file_name = file_name.to_path if file_name.respond_to?(:to_path) unless file_name.is_a?(String) - raise ArgumentError, "the file name must be a String -- you passed #{file_name.inspect}" + raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}" end Dependencies.depend_on(file_name, message) @@ -213,22 +240,10 @@ module ActiveSupport #:nodoc: yield end rescue Exception => exception # errors from loading file - exception.blame_file! file + exception.blame_file! file if exception.respond_to? :blame_file! raise end - def load(file, wrap = false) - result = false - load_dependency(file) { result = super } - result - end - - def require(file) - result = false - load_dependency(file) { result = super } - result - end - # Mark the given constant as unloadable. Unloadable constants are removed # each time dependencies are cleared. # @@ -245,6 +260,20 @@ module ActiveSupport #:nodoc: def unloadable(const_desc) Dependencies.mark_for_unload const_desc end + + private + + def load(file, wrap = false) + result = false + load_dependency(file) { result = super } + result + end + + def require(file) + result = false + load_dependency(file) { result = super } + result + end end # Exception file-blaming. @@ -297,6 +326,7 @@ module ActiveSupport #:nodoc: def clear log_call loaded.clear + loading.clear remove_unloadable_constants! end @@ -309,6 +339,7 @@ module ActiveSupport #:nodoc: # Record that we've seen this file *before* loading it to avoid an # infinite loop with mutual dependencies. loaded << expanded + loading << expanded begin if load? @@ -331,6 +362,8 @@ module ActiveSupport #:nodoc: rescue Exception loaded.delete expanded raise + ensure + loading.pop end # Record history *after* loading so first load gets warnings. @@ -388,7 +421,8 @@ module ActiveSupport #:nodoc: end def load_once_path?(path) - # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false + # to_s works around a ruby1.9 issue where String#starts_with?(Pathname) + # will raise a TypeError: no implicit conversion of Pathname into String autoload_once_paths.any? { |base| path.starts_with? base.to_s } end @@ -416,7 +450,7 @@ module ActiveSupport #:nodoc: def load_file(path, const_paths = loadable_constants_for_path(path)) log_call path, const_paths const_paths = [const_paths].compact unless const_paths.is_a? Array - parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || :Object } + parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object } result = nil newly_defined_paths = new_constants_in(*parent_paths) do @@ -445,8 +479,6 @@ module ActiveSupport #:nodoc: raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!" end - raise NameError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name, false) - qualified_name = qualified_name_for from_mod, const_name path_suffix = qualified_name.underscore @@ -456,10 +488,10 @@ module ActiveSupport #:nodoc: expanded = File.expand_path(file_path) expanded.sub!(/\.rb\z/, '') - if loaded.include?(expanded) + if loading.include?(expanded) raise "Circular dependency detected while autoloading constant #{qualified_name}" else - require_or_load(expanded) + require_or_load(expanded, qualified_name) raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false) return from_mod.const_get(const_name) end @@ -497,9 +529,9 @@ module ActiveSupport #:nodoc: end end - raise NameError, - "uninitialized constant #{qualified_name}", - caller.reject { |l| l.starts_with? __FILE__ } + name_error = NameError.new("uninitialized constant #{qualified_name}", const_name) + name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ }) + raise name_error end # Remove the constants that have been autoloaded, and those that have been @@ -634,7 +666,7 @@ module ActiveSupport #:nodoc: when String then desc.sub(/^::/, '') when Symbol then desc.to_s when Module - desc.name.presence || + desc.name || raise(ArgumentError, "Anonymous modules have no name to be referenced by") else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}" end @@ -648,6 +680,14 @@ module ActiveSupport #:nodoc: constants = normalized.split('::') to_remove = constants.pop + # Remove the file path from the loaded list. + file_path = search_for_file(const.underscore) + if file_path + expanded = File.expand_path(file_path) + expanded.sub!(/\.rb\z/, '') + self.loaded.delete(expanded) + end + if constants.empty? parent = Object else @@ -702,7 +742,7 @@ module ActiveSupport #:nodoc: protected def log_call(*args) if log_activity? - arg_str = args.collect { |arg| arg.inspect } * ', ' + arg_str = args.collect(&:inspect) * ', ' /in `([a-z_\?\!]+)'/ =~ caller(1).first selector = $1 || '<unknown>' log "called #{selector}(#{arg_str})" diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb index c0dba5f7fd..13036d521d 100644 --- a/activesupport/lib/active_support/dependencies/autoload.rb +++ b/activesupport/lib/active_support/dependencies/autoload.rb @@ -67,7 +67,7 @@ module ActiveSupport end def eager_load! - @_autoloads.values.each { |file| require file } + @_autoloads.each_value { |file| require file } end def autoloads diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 6c15fffc0f..46e9996d59 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -25,14 +25,14 @@ module ActiveSupport include Reporting include MethodWrapper - # The version the deprecated behavior will be removed, by default. + # The version number in which the deprecated behavior will be removed, by default. attr_accessor :deprecation_horizon - # It accepts two parameters on initialization. The first is an version of library - # and the second is an library name + # It accepts two parameters on initialization. The first is a version of library + # and the second is a library name # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = '4.1', gem_name = 'Rails') + def initialize(deprecation_horizon = '5.0', gem_name = 'Rails') self.gem_name = gem_name self.deprecation_horizon = deprecation_horizon # By default, warnings are not silenced and debugging is off. diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 90db180124..9f9dca8453 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,16 +1,26 @@ require "active_support/notifications" module ActiveSupport + class DeprecationException < StandardError + end + class Deprecation # Default warning behaviors per Rails.env. DEFAULT_BEHAVIORS = { - :stderr => Proc.new { |message, callstack| + raise: ->(message, callstack) { + e = DeprecationException.new(message) + e.set_backtrace(callstack) + raise e + }, + + stderr: ->(message, callstack) { $stderr.puts(message) $stderr.puts callstack.join("\n ") if debug }, - :log => Proc.new { |message, callstack| + + log: ->(message, callstack) { logger = - if defined?(Rails) && Rails.logger + if defined?(Rails.logger) && Rails.logger Rails.logger else require 'active_support/logger' @@ -19,11 +29,13 @@ module ActiveSupport logger.warn message logger.debug callstack.join("\n ") if debug }, - :notify => Proc.new { |message, callstack| + + notify: ->(message, callstack) { ActiveSupport::Notifications.instrument("deprecation.rails", :message => message, :callstack => callstack) }, - :silence => Proc.new { |message, callstack| } + + silence: ->(message, callstack) {}, } module Behavior @@ -40,6 +52,7 @@ module ActiveSupport # # Available behaviors: # + # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>. # [+stderr+] Log all deprecation warnings to +$stderr+. # [+log+] Log all deprecation warnings to +Rails.logger+. # [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+. @@ -52,7 +65,7 @@ module ActiveSupport # ActiveSupport::Deprecation.behavior = :stderr # ActiveSupport::Deprecation.behavior = [:stderr, :log] # ActiveSupport::Deprecation.behavior = MyCustomHandler - # ActiveSupport::Deprecation.behavior = proc { |message, callstack| + # ActiveSupport::Deprecation.behavior = ->(message, callstack) { # # custom stuff # } def behavior=(behavior) diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index 485dc91063..a03a66b96b 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -25,7 +25,7 @@ module ActiveSupport end end - # This DeprecatedObjectProxy transforms object to depracated object. + # This DeprecatedObjectProxy transforms object to deprecated object. # # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!") # @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance) @@ -52,7 +52,7 @@ module ActiveSupport end # This DeprecatedInstanceVariableProxy transforms instance variable to - # depracated instance variable. + # deprecated instance variable. # # class Example # def initialize(deprecator) @@ -93,7 +93,7 @@ module ActiveSupport end end - # This DeprecatedConstantProxy transforms constant to depracated constant. + # This DeprecatedConstantProxy transforms constant to deprecated constant. # # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST') # OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance) diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 2cb1f408b6..bcb415f6d3 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -1,4 +1,3 @@ -require 'active_support/proxy_object' require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/object/acts_like' @@ -7,7 +6,7 @@ module ActiveSupport # Time#advance, respectively. It mainly supports the methods on Numeric. # # 1.month.ago # equivalent to Time.now.advance(months: -1) - class Duration < ProxyObject + class Duration attr_accessor :value, :parts def initialize(value, parts) #:nodoc: @@ -39,6 +38,10 @@ module ActiveSupport end alias :kind_of? :is_a? + def instance_of?(klass) # :nodoc: + Duration == klass || value.instance_of?(klass) + end + # Returns +true+ if +other+ is also a Duration instance with the # same +value+, or if <tt>other == value</tt>. def ==(other) @@ -49,6 +52,20 @@ module ActiveSupport end end + def to_s + @value.to_s + end + + # Returns +true+ if +other+ is also a Duration instance, which has the + # same parts as this one. + def eql?(other) + Duration === other && other.value.eql?(value) + end + + def hash + @value.hash + end + def self.===(other) #:nodoc: other.is_a?(Duration) rescue ::NoMethodError @@ -70,19 +87,23 @@ module ActiveSupport alias :until :ago def inspect #:nodoc: - consolidated = parts.inject(::Hash.new(0)) { |h,(l,r)| h[l] += r; h } - parts = [:years, :months, :days, :minutes, :seconds].map do |length| - n = consolidated[length] - "#{n} #{n == 1 ? length.to_s.singularize : length.to_s}" if n.nonzero? - end.compact - parts = ["0 seconds"] if parts.empty? - parts.to_sentence(:locale => :en) + parts. + reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }. + sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}. + map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}. + to_sentence(:locale => :en) end def as_json(options = nil) #:nodoc: to_i end + def respond_to_missing?(method, include_private=false) #:nodoc: + @value.respond_to?(method, include_private) + end + + delegate :<=>, to: :value + protected def sum(sign, time = ::Time.current) #:nodoc: @@ -101,6 +122,13 @@ module ActiveSupport private + # We define it as a workaround to Ruby 2.0.0-p353 bug. + # For more information, check rails/rails#13055. + # Remove it when we drop support for 2.0.0-p353. + def ===(other) #:nodoc: + value === other + end + def method_missing(method, *args, &block) #:nodoc: value.send(method, *args, &block) end diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 20136dd1b0..78b627c286 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -92,7 +92,7 @@ module ActiveSupport def watched @watched || begin - all = @files.select { |f| File.exists?(f) } + all = @files.select { |f| File.exist?(f) } all.concat(Dir[@glob]) if @glob all end @@ -115,7 +115,7 @@ module ActiveSupport end def compile_glob(hash) - hash.freeze # Freeze so changes aren't accidently pushed + hash.freeze # Freeze so changes aren't accidentally pushed return if hash.empty? globs = hash.map do |key, value| diff --git a/activesupport/lib/active_support/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb deleted file mode 100644 index 81e63e76a7..0000000000 --- a/activesupport/lib/active_support/file_watcher.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveSupport - class FileWatcher - class Backend - def initialize(path, watcher) - @watcher = watcher - @path = path - end - - def trigger(files) - @watcher.trigger(files) - end - end - - def initialize - @regex_matchers = {} - end - - def watch(pattern, &block) - @regex_matchers[pattern] = block - end - - def trigger(files) - trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } } - - files.each do |file, state| - @regex_matchers.each do |pattern, block| - trigger_files[block][state] << file if pattern === file - end - end - - trigger_files.each do |block, payload| - block.call payload - end - end - end -end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb new file mode 100644 index 0000000000..7068f09d87 --- /dev/null +++ b/activesupport/lib/active_support/gem_version.rb @@ -0,0 +1,15 @@ +module ActiveSupport + # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt> + def self.gem_version + Gem::Version.new VERSION::STRING + end + + module VERSION + MAJOR = 5 + MINOR = 0 + TINY = 0 + PRE = "alpha" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb index 6ef33ab683..b837c879bb 100644 --- a/activesupport/lib/active_support/gzip.rb +++ b/activesupport/lib/active_support/gzip.rb @@ -25,9 +25,9 @@ module ActiveSupport end # Compresses a string using gzip. - def self.compress(source) + def self.compress(source, level=Zlib::DEFAULT_COMPRESSION, strategy=Zlib::DEFAULT_STRATEGY) output = Stream.new - gz = Zlib::GzipWriter.new(output) + gz = Zlib::GzipWriter.new(output, level, strategy) gz.write(source) gz.close output.string diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 306d80b2df..1468c62151 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/hash/reverse_merge' module ActiveSupport # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered @@ -55,7 +56,7 @@ module ActiveSupport end def initialize(constructor = {}) - if constructor.is_a?(Hash) + if constructor.respond_to?(:to_hash) super() update(constructor) else @@ -72,13 +73,15 @@ module ActiveSupport end def self.new_from_hash_copying_default(hash) + hash = hash.to_hash new(hash).tap do |new_hash| new_hash.default = hash.default + new_hash.default_proc = hash.default_proc if hash.default_proc end end def self.[](*args) - new.merge(Hash[*args]) + new.merge!(Hash[*args]) end alias_method :regular_writer, :[]= unless method_defined?(:regular_writer) @@ -91,7 +94,7 @@ module ActiveSupport # # This value can be later fetched using either +:key+ or +'key'+. def []=(key, value) - regular_writer(convert_key(key), convert_value(value)) + regular_writer(convert_key(key), convert_value(value, for: :assignment)) end alias_method :store, :[]= @@ -125,7 +128,7 @@ module ActiveSupport if other_hash.is_a? HashWithIndifferentAccess super(other_hash) else - other_hash.each_pair do |key, value| + other_hash.to_hash.each_pair do |key, value| if block_given? && key?(key) value = yield(convert_key(key), self[key], value) end @@ -159,7 +162,7 @@ module ActiveSupport # # counters.fetch('foo') # => 1 # counters.fetch(:bar, 0) # => 0 - # counters.fetch(:bar) {|key| 0} # => 0 + # counters.fetch(:bar) { |key| 0 } # => 0 # counters.fetch(:zoo) # => KeyError: key not found: "zoo" def fetch(key, *extras) super(convert_key(key), *extras) @@ -172,10 +175,17 @@ module ActiveSupport # hash[:b] = 'y' # hash.values_at('a', 'b') # => ["x", "y"] def values_at(*indices) - indices.collect {|key| self[convert_key(key)]} + indices.collect { |key| self[convert_key(key)] } end - # Returns an exact copy of the hash. + # Returns a shallow copy of the hash. + # + # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } }) + # dup = hash.dup + # dup[:a][:c] = 'c' + # + # hash[:a][:c] # => nil + # dup[:a][:c] # => "c" def dup self.class.new(self).tap do |new_hash| new_hash.default = default @@ -207,7 +217,7 @@ module ActiveSupport # Replaces the contents of this hash with other_hash. # # h = { "a" => 100, "b" => 200 } - # h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400} + # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400} def replace(other_hash) super(self.class.new_from_hash_copying_default(other_hash)) end @@ -223,13 +233,25 @@ module ActiveSupport def deep_stringify_keys; dup end undef :symbolize_keys! undef :deep_symbolize_keys! - def symbolize_keys; to_hash.symbolize_keys end - def deep_symbolize_keys; to_hash.deep_symbolize_keys end + def symbolize_keys; to_hash.symbolize_keys! end + def deep_symbolize_keys; to_hash.deep_symbolize_keys! end def to_options!; self end + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + # Convert to a regular hash with string keys. def to_hash - Hash.new(default).merge!(self) + _new_hash = Hash.new(default) + each do |key, value| + _new_hash[key] = convert_value(value, for: :to_hash) + end + _new_hash end protected @@ -237,12 +259,18 @@ module ActiveSupport key.kind_of?(Symbol) ? key.to_s : key end - def convert_value(value) + def convert_value(value, options = {}) if value.is_a? Hash - value.nested_under_indifferent_access + if options[:for] == :to_hash + value.to_hash + else + value.nested_under_indifferent_access + end elsif value.is_a?(Array) - value = value.dup if value.frozen? - value.map! { |e| convert_value(e) } + unless options[:for] == :assignment + value = value.dup + end + value.map! { |e| convert_value(e, options) } else value end diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb index 188653bd9b..6cc98191d4 100644 --- a/activesupport/lib/active_support/i18n.rb +++ b/activesupport/lib/active_support/i18n.rb @@ -1,10 +1,13 @@ +require 'active_support/core_ext/hash/deep_merge' +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' begin require 'i18n' - require 'active_support/lazy_load_hooks' rescue LoadError => e $stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install" raise e end +require 'active_support/lazy_load_hooks' ActiveSupport.run_load_hooks(:i18n) I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml" diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 890dd9380b..affcfb7398 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -31,6 +31,12 @@ module I18n fallbacks = app.config.i18n.delete(:fallbacks) + # Avoid issues with setting the default_locale by disabling available locales + # check while configuring. + enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) + enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil? + I18n.enforce_available_locales = false + app.config.i18n.each do |setting, value| case setting when :railties_load_path @@ -44,6 +50,9 @@ module I18n init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks) + # Restore available locales check so it will take place from now on. + I18n.enforce_available_locales = enforce_available_locales + reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! } app.reloaders << reloader ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated } diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index ef882ebd09..2ca1124e76 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -1,5 +1,11 @@ require 'active_support/inflector/inflections' +#-- +# Defines the standard inflection rules. These are the starting point for +# new projects and are not considered complete. The current set of inflection +# rules is frozen. This means, we do not change them to become more complete. +# This is a safety measure to keep existing applications from breaking. +#++ module ActiveSupport Inflector.inflections(:en) do |inflect| inflect.plural(/$/, 's') @@ -57,7 +63,6 @@ module ActiveSupport inflect.irregular('child', 'children') inflect.irregular('sex', 'sexes') inflect.irregular('move', 'moves') - inflect.irregular('cow', 'kine') inflect.irregular('zombie', 'zombies') inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police)) diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index 9cf4b2b2ba..486838bd15 100644 --- a/activesupport/lib/active_support/inflector/inflections.rb +++ b/activesupport/lib/active_support/inflector/inflections.rb @@ -52,21 +52,21 @@ module ActiveSupport # into a non-delimited single lowercase word when passed to +underscore+. # # acronym 'HTML' - # titleize 'html' #=> 'HTML' - # camelize 'html' #=> 'HTML' - # underscore 'MyHTML' #=> 'my_html' + # titleize 'html' # => 'HTML' + # camelize 'html' # => 'HTML' + # underscore 'MyHTML' # => 'my_html' # # The acronym, however, must occur as a delimited unit and not be part of # another word for conversions to recognize it: # # acronym 'HTTP' - # camelize 'my_http_delimited' #=> 'MyHTTPDelimited' - # camelize 'https' #=> 'Https', not 'HTTPs' - # underscore 'HTTPS' #=> 'http_s', not 'https' + # camelize 'my_http_delimited' # => 'MyHTTPDelimited' + # camelize 'https' # => 'Https', not 'HTTPs' + # underscore 'HTTPS' # => 'http_s', not 'https' # # acronym 'HTTPS' - # camelize 'https' #=> 'HTTPS' - # underscore 'HTTPS' #=> 'https' + # camelize 'https' # => 'HTTPS' + # underscore 'HTTPS' # => 'https' # # Note: Acronyms that are passed to +pluralize+ will no longer be # recognized, since the acronym will not occur as a delimited unit in the @@ -74,25 +74,25 @@ module ActiveSupport # form as an acronym as well: # # acronym 'API' - # camelize(pluralize('api')) #=> 'Apis' + # camelize(pluralize('api')) # => 'Apis' # # acronym 'APIs' - # camelize(pluralize('api')) #=> 'APIs' + # camelize(pluralize('api')) # => 'APIs' # # +acronym+ may be used to specify any word that contains an acronym or # otherwise needs to maintain a non-standard capitalization. The only # restriction is that the word must begin with a capital letter. # # acronym 'RESTful' - # underscore 'RESTful' #=> 'restful' - # underscore 'RESTfulController' #=> 'restful_controller' - # titleize 'RESTfulController' #=> 'RESTful Controller' - # camelize 'restful' #=> 'RESTful' - # camelize 'restful_controller' #=> 'RESTfulController' + # underscore 'RESTful' # => 'restful' + # underscore 'RESTfulController' # => 'restful_controller' + # titleize 'RESTfulController' # => 'RESTful Controller' + # camelize 'restful' # => 'RESTful' + # camelize 'restful_controller' # => 'RESTfulController' # # acronym 'McDonald' - # underscore 'McDonald' #=> 'mcdonald' - # camelize 'mcdonald' #=> 'McDonald' + # underscore 'McDonald' # => 'mcdonald' + # camelize 'mcdonald' # => 'McDonald' def acronym(word) @acronyms[word.downcase] = word @acronym_regex = /#{@acronyms.values.join("|")}/ @@ -128,27 +128,39 @@ module ActiveSupport def irregular(singular, plural) @uncountables.delete(singular) @uncountables.delete(plural) - if singular[0,1].upcase == plural[0,1].upcase - plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1]) - plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1]) - singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1]) + + s0 = singular[0] + srest = singular[1..-1] + + p0 = plural[0] + prest = plural[1..-1] + + if s0.upcase == p0.upcase + plural(/(#{s0})#{srest}$/i, '\1' + prest) + plural(/(#{p0})#{prest}$/i, '\1' + prest) + + singular(/(#{s0})#{srest}$/i, '\1' + srest) + singular(/(#{p0})#{prest}$/i, '\1' + srest) else - plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1]) - plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1]) - plural(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), plural[0,1].upcase + plural[1..-1]) - plural(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), plural[0,1].downcase + plural[1..-1]) - singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1]) - singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1]) + plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest) + plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest) + plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest) + plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest) + + singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest) + singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest) + singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest) + singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest) end end - # Add uncountable words that shouldn't be attempted inflected. + # Specifies words that are uncountable and should not be inflected. # # uncountable 'money' # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - (@uncountables << words).flatten! + @uncountables += words.flatten.map(&:downcase) end # Specifies a humanized form of a string by a regular expression rule or diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 1eb2b4212b..74b3a7c2a9 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -1,6 +1,5 @@ # encoding: utf-8 -require 'active_support/inflector/inflections' require 'active_support/inflections' module ActiveSupport @@ -37,7 +36,7 @@ module ActiveSupport # string. # # If passed an optional +locale+ parameter, the word will be - # pluralized using rules defined for that language. By default, + # singularized using rules defined for that language. By default, # this parameter is set to <tt>:en</tt>. # # 'posts'.singularize # => "post" @@ -73,7 +72,9 @@ module ActiveSupport else string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase } end - string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::') + string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" } + string.gsub!(/\//, '::') + string end # Makes an underscored, lowercase form from the expression in the string. @@ -88,9 +89,9 @@ module ActiveSupport # # 'SSLError'.underscore.camelize # => "SslError" def underscore(camel_cased_word) - word = camel_cased_word.to_s.dup - word.gsub!('::', '/') - word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } + return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ + word = camel_cased_word.to_s.gsub(/::/, '/') + word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" } word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2') word.gsub!(/([a-z\d])([A-Z])/,'\1_\2') word.tr!("-", "_") @@ -98,20 +99,47 @@ module ActiveSupport word end - # Capitalizes the first word and turns underscores into spaces and strips a - # trailing "_id", if any. Like +titleize+, this is meant for creating pretty - # output. + # Tweaks an attribute name for display to end users. + # + # Specifically, +humanize+ performs these transformations: + # + # * Applies human inflection rules to the argument. + # * Deletes leading underscores, if any. + # * Removes a "_id" suffix if present. + # * Replaces underscores with spaces, if any. + # * Downcases all words except acronyms. + # * Capitalizes the first word. + # + # The capitalization of the first word can be turned off by setting the + # +:capitalize+ option to false (default is true). + # + # humanize('employee_salary') # => "Employee salary" + # humanize('author_id') # => "Author" + # humanize('author_id', capitalize: false) # => "author" + # humanize('_id') # => "Id" # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - def humanize(lower_case_and_underscored_word) + # If "SSL" was defined to be an acronym: + # + # humanize('ssl_error') # => "SSL error" + # + def humanize(lower_case_and_underscored_word, options = {}) result = lower_case_and_underscored_word.to_s.dup + inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.gsub!(/_id$/, "") + + result.sub!(/\A_+/, '') + result.sub!(/_id\z/, '') result.tr!('_', ' ') - result.gsub(/([a-z\d]*)/i) { |match| + + result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" - }.gsub(/^\w/) { $&.upcase } + end + + if options.fetch(:capitalize, true) + result.sub!(/\A\w/) { |match| match.upcase } + end + + result end # Capitalizes all the words and replaces some characters in the string to @@ -147,7 +175,7 @@ module ActiveSupport # # Singular names are not handled correctly: # - # 'business'.classify # => "Busines" + # 'calculus'.classify # => "Calculu" def classify(table_name) # strip out any leading schema name camelize(singularize(table_name.to_s.sub(/.*\./, ''))) @@ -164,6 +192,8 @@ module ActiveSupport # # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => "" # # See also +deconstantize+. def demodulize(path) @@ -185,7 +215,7 @@ module ActiveSupport # # See also +demodulize+. def deconstantize(path) - path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename + path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename end # Creates a foreign key name from a class name. @@ -219,7 +249,12 @@ module ActiveSupport # unknown. def constantize(camel_cased_word) names = camel_cased_word.split('::') - names.shift if names.empty? || names.first.empty? + + # Trigger a built-in NameError exception including the ill-formed constant in the message. + Object.const_get(camel_cased_word) if names.empty? + + # Remove the first blank element in case of '::ClassName' notation. + names.shift if names.size > 1 && names.first.empty? names.inject(Object) do |constant, name| if constant == Object @@ -229,8 +264,8 @@ module ActiveSupport next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) - # Go down the ancestors to check it it's owned - # directly before we reach Object or the end of ancestors. + # Go down the ancestors to check if it is owned directly. The check + # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) @@ -266,14 +301,12 @@ module ActiveSupport # 'UnknownModule'.safe_constantize # => nil # 'UnknownModule::Foo::Bar'.safe_constantize # => nil def safe_constantize(camel_cased_word) - begin - constantize(camel_cased_word) - rescue NameError => e - raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ || - e.name.to_s == camel_cased_word.to_s - rescue ArgumentError => e - raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ - end + constantize(camel_cased_word) + rescue NameError => e + raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) || + e.name.to_s == camel_cased_word.to_s) + rescue ArgumentError => e + raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ end # Returns the suffix that should be added to a number to denote the position @@ -315,10 +348,16 @@ module ActiveSupport private - # Mount a regular expression that will match part by part of the constant. - # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)? + # Mounts a regular expression, returned as a string to ease interpolation, + # that will match part by part the given constant. + # + # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" + # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: parts = camel_cased_word.split("::") + + return Regexp.escape(camel_cased_word) if parts.blank? + last = parts.pop parts.reverse.inject(last) do |acc, part| diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index a4a32b2ad0..35548f3f56 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -1,20 +1,30 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/delegation' -require 'multi_json' +require 'json' module ActiveSupport # Look for and parse json strings that look like ISO 8601 times. mattr_accessor :parse_json_times module JSON + # matches YAML-formatted dates + DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ + class << self # Parses a JSON string (JavaScript Object Notation) into a hash. - # See www.json.org for more info. + # See http://www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, options ={}) - data = MultiJson.load(json, options) + def decode(json, options = {}) + if options.present? + raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \ + "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \ + "and has been removed." + end + + data = ::JSON.parse(json, quirks_mode: true) + if ActiveSupport.parse_json_times convert_dates_from(data) else @@ -22,23 +32,6 @@ module ActiveSupport end end - def engine - MultiJson.adapter - end - alias :backend :engine - - def engine=(name) - MultiJson.use(name) - end - alias :backend= :engine= - - def with_backend(name) - old_backend, self.backend = backend, name - yield - ensure - self.backend = old_backend - end - # Returns the class of the error that will be raised when there is an # error in decoding JSON. Using this method means you won't directly # depend on the ActiveSupport's JSON implementation, in case it changes @@ -50,7 +43,7 @@ module ActiveSupport # Rails.logger.warn("Attempted to decode invalid JSON: #{some_string}") # end def parse_error - MultiJson::DecodeError + ::JSON::ParserError end private diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 832d1ce6d5..c0ac5af153 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,338 +1,172 @@ -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/module/delegation' -require 'active_support/json/variable' - -require 'bigdecimal' -require 'active_support/core_ext/big_decimal/conversions' # for #to_s -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/instance_variables' -require 'time' -require 'active_support/core_ext/time/conversions' -require 'active_support/core_ext/date_time/conversions' -require 'active_support/core_ext/date/conversions' -require 'set' module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, + :time_precision, :time_precision=, :escape_html_entities_in_json, :escape_html_entities_in_json=, :encode_big_decimal_as_string, :encode_big_decimal_as_string=, + :json_encoder, :json_encoder=, :to => :'ActiveSupport::JSON::Encoding' end module JSON - # matches YAML-formatted dates - DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ - # Dumps objects in JSON (JavaScript Object Notation). - # See www.json.org for more info. + # See http://www.json.org for more info. # # ActiveSupport::JSON.encode({ team: 'rails', players: '36' }) # # => "{\"team\":\"rails\",\"players\":\"36\"}" def self.encode(value, options = nil) - Encoding::Encoder.new(options).encode(value) + Encoding.json_encoder.new(options).encode(value) end module Encoding #:nodoc: - class CircularReferenceError < StandardError; end - - class Encoder + class JSONGemEncoder #:nodoc: attr_reader :options def initialize(options = nil) @options = options || {} - @seen = Set.new end - def encode(value, use_options = true) - check_for_circular_references(value) do - jsonified = use_options ? value.as_json(options_for(value)) : value.as_json - jsonified.encode_json(self) - end + # Encode the given object into a JSON string + def encode(value) + stringify jsonify value.as_json(options.dup) end - # like encode, but only calls as_json, without encoding to string. - def as_json(value, use_options = true) - check_for_circular_references(value) do - use_options ? value.as_json(options_for(value)) : value.as_json + private + # Rails does more escaping than the JSON gem natively does (we + # escape \u2028 and \u2029 and optionally >, <, & to work around + # certain browser problems). + ESCAPED_CHARS = { + "\u2028" => '\u2028', + "\u2029" => '\u2029', + '>' => '\u003e', + '<' => '\u003c', + '&' => '\u0026', + } + + ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u + ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u + + # This class wraps all the strings we see and does the extra escaping + class EscapedString < String #:nodoc: + def to_json(*) + if Encoding.escape_html_entities_in_json + super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS + else + super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS + end + end end - end - def options_for(value) - if value.is_a?(Array) || value.is_a?(Hash) - # hashes and arrays need to get encoder in the options, so that - # they can detect circular references. - options.merge(:encoder => self) - else - options.dup + # Mark these as private so we don't leak encoding-specific constructs + private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, + :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString + + # Convert an object into a "JSON-ready" representation composed of + # primitives like Hash, Array, String, Numeric, and true/false/nil. + # Recursively calls #as_json to the object to recursively build a + # fully JSON-ready object. + # + # This allows developers to implement #as_json without having to + # worry about what base types of objects they are allowed to return + # or having to remember to call #as_json recursively. + # + # Note: the +options+ hash passed to +object.to_json+ is only passed + # to +object.as_json+, not any of this method's recursive +#as_json+ + # calls. + def jsonify(value) + case value + when String + EscapedString.new(value) + when Numeric, NilClass, TrueClass, FalseClass + value + when Hash + Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] + when Array + value.map { |v| jsonify(v) } + else + jsonify value.as_json + end end - end - - def escape(string) - Encoding.escape(string) - end - private - def check_for_circular_references(value) - unless @seen.add?(value.__id__) - raise CircularReferenceError, 'object references itself' - end - yield - ensure - @seen.delete(value.__id__) + # Encode a "jsonified" Ruby data structure using the JSON gem + def stringify(jsonified) + ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false) end end - - ESCAPED_CHARS = { - "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002', - "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005', - "\x06" => '\u0006', "\x07" => '\u0007', "\x0B" => '\u000B', - "\x0E" => '\u000E', "\x0F" => '\u000F', "\x10" => '\u0010', - "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013', - "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016', - "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019', - "\x1A" => '\u001A', "\x1B" => '\u001B', "\x1C" => '\u001C', - "\x1D" => '\u001D', "\x1E" => '\u001E', "\x1F" => '\u001F', - "\010" => '\b', - "\f" => '\f', - "\n" => '\n', - "\r" => '\r', - "\t" => '\t', - '"' => '\"', - '\\' => '\\\\', - '>' => '\u003E', - '<' => '\u003C', - '&' => '\u0026' } - class << self # If true, use ISO 8601 format for dates and times. Otherwise, fall back # to the Active Support legacy format. attr_accessor :use_standard_json_time_format - # If false, serializes BigDecimal objects as numeric instead of wrapping - # them in a string. - attr_accessor :encode_big_decimal_as_string - - attr_accessor :escape_regex - attr_reader :escape_html_entities_in_json - - def escape_html_entities_in_json=(value) - self.escape_regex = \ - if @escape_html_entities_in_json = value - /[\x00-\x1F"\\><&]/ - else - /[\x00-\x1F"\\]/ - end - end - - def escape(string) - string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY) - json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] } - json = %("#{json}") - json.force_encoding(::Encoding::UTF_8) - json - end - end - - self.use_standard_json_time_format = true - self.escape_html_entities_in_json = true - self.encode_big_decimal_as_string = true - end - end -end - -class Object - def as_json(options = nil) #:nodoc: - if respond_to?(:to_hash) - to_hash - else - instance_values - end - end -end - -class Struct #:nodoc: - def as_json(options = nil) - Hash[members.zip(values)] - end -end - -class TrueClass - def as_json(options = nil) #:nodoc: - self - end + # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e) + # as a safety measure. + attr_accessor :escape_html_entities_in_json - def encode_json(encoder) #:nodoc: - to_s - end -end + # Sets the precision of encoded time values. + # Defaults to 3 (equivalent to millisecond precision) + attr_accessor :time_precision -class FalseClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end + # Sets the encoder used by Rails to encode Ruby objects into JSON strings + # in +Object#to_json+ and +ActiveSupport::JSON.encode+. + attr_accessor :json_encoder -class NilClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - 'null' - end -end - -class String - def as_json(options = nil) #:nodoc: - self - end + def encode_big_decimal_as_string=(as_string) + message = \ + "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \ + "the new encoder will always encode them as strings.\n\n" \ + "You are seeing this error because you have 'active_support.encode_big_decimal_as_string' in " \ + "your configuration file. If you have been setting this to true, you can safely remove it from " \ + "your configuration. Otherwise, you should add the 'activesupport-json_encoder' gem to your " \ + "Gemfile in order to restore this functionality." - def encode_json(encoder) #:nodoc: - encoder.escape(self) - end -end - -class Symbol - def as_json(options = nil) #:nodoc: - to_s - end -end - -class Numeric - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class Float - # Encoding Infinity or NaN to JSON should return "null". The default returns - # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]'). - def as_json(options = nil) #:nodoc: - finite? ? self : nil - end -end - -class BigDecimal - # A BigDecimal would be naturally represented as a JSON number. Most libraries, - # however, parse non-integer JSON numbers directly as floats. Clients using - # those libraries would get in general a wrong number and no way to recover - # other than manually inspecting the string with the JSON code itself. - # - # That's why a JSON string is returned. The JSON literal is not numeric, but - # if the other end knows by contract that the data is supposed to be a - # BigDecimal, it still has the chance to post-process the string and get the - # real value. - # - # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to - # override this behaviour. - def as_json(options = nil) #:nodoc: - if finite? - ActiveSupport.encode_big_decimal_as_string ? to_s : self - else - nil - end - end -end - -class Regexp - def as_json(options = nil) #:nodoc: - to_s - end -end + raise NotImplementedError, message + end -module Enumerable - def as_json(options = nil) #:nodoc: - to_a.as_json(options) - end -end + def encode_big_decimal_as_string + message = \ + "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \ + "the new encoder will always encode them as strings.\n\n" \ + "You are seeing this error because you are trying to check the value of the related configuration, " \ + "`active_support.encode_big_decimal_as_string`. If your application depends on this option, you should " \ + "add the 'activesupport-json_encoder' gem to your Gemfile. For now, this option will always be true. " \ + "In the future, it will be removed from Rails, so you should stop checking its value." -class Range - def as_json(options = nil) #:nodoc: - to_s - end -end + ActiveSupport::Deprecation.warn message -class Array - def as_json(options = nil) #:nodoc: - # use encoder as a proxy to call as_json on all elements, to protect from circular references - encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) - map { |v| encoder.as_json(v, options) } - end - - def encode_json(encoder) #:nodoc: - # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly - "[#{map { |v| v.encode_json(encoder) } * ','}]" - end -end + true + end -class Hash - def as_json(options = nil) #:nodoc: - # create a subset of the hash by applying :only or :except - subset = if options - if attrs = options[:only] - slice(*Array(attrs)) - elsif attrs = options[:except] - except(*Array(attrs)) - else - self + # Deprecate CircularReferenceError + def const_missing(name) + if name == :CircularReferenceError + message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \ + "You are seeing this warning because you are rescuing from (or otherwise referencing) " \ + "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \ + "removed from Rails. You should remove these rescue blocks from your code and ensure " \ + "that your data structures are free of circular references so they can be properly " \ + "serialized into JSON.\n\n" \ + "For example, the following Hash contains a circular reference to itself:\n" \ + " h = {}\n" \ + " h['circular'] = h\n" \ + "In this case, calling h.to_json would not work properly." + + ActiveSupport::Deprecation.warn message + + SystemStackError + else + super + end + end end - else - self - end - - # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references - encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options) - Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }] - end - def encode_json(encoder) #:nodoc: - # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be - # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields); - - # on the other hand, we need to run as_json on the elements, because the model representation may contain fields - # like Time/Date in their original (not jsonified) form, etc. - - "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}" - end -end - -class Time - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema - else - %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) - end - end -end - -class Date - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - strftime("%Y-%m-%d") - else - strftime("%Y/%m/%d") - end - end -end - -class DateTime - def as_json(options = nil) #:nodoc: - if ActiveSupport.use_standard_json_time_format - xmlschema - else - strftime('%Y/%m/%d %H:%M:%S %z') + self.use_standard_json_time_format = true + self.escape_html_entities_in_json = true + self.json_encoder = JSONGemEncoder + self.time_precision = 3 end end end diff --git a/activesupport/lib/active_support/json/variable.rb b/activesupport/lib/active_support/json/variable.rb deleted file mode 100644 index d69dab6408..0000000000 --- a/activesupport/lib/active_support/json/variable.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'active_support/deprecation' - -module ActiveSupport - module JSON - # Deprecated: A string that returns itself as its JSON-encoded form. - class Variable < String - def initialize(*args) - message = 'ActiveSupport::JSON::Variable is deprecated and will be removed in Rails 4.1. ' \ - 'For your own custom JSON literals, define #as_json and #encode_json yourself.' - ActiveSupport::Deprecation.warn message - super - end - - def as_json(options = nil) self end #:nodoc: - def encode_json(encoder) self end #:nodoc: - end - end -end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 71654dbb87..51d2da3a79 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -4,7 +4,7 @@ require 'openssl' module ActiveSupport # 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 + # This lets Rails applications have a single secure secret, but avoid reusing that # key in multiple incompatible contexts. class KeyGenerator def initialize(secret, options = {}) @@ -39,7 +39,7 @@ module ActiveSupport end end - class DummyKeyGenerator # :nodoc: + class LegacyKeyGenerator # :nodoc: SECRET_MIN_LENGTH = 30 # Characters def initialize(secret) @@ -57,18 +57,16 @@ module ActiveSupport # secret they've provided is at least 30 characters in length. def ensure_secret_secure(secret) if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_key_base = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/initializers/secret_token.rb" + raise ArgumentError, "A secret is required to generate an integrity hash " \ + "for cookie session data. Set a secret_key_base of at least " \ + "#{SECRET_MIN_LENGTH} characters in config/secrets.yml." end if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" + raise ArgumentError, "Secret should be something secure, " \ + "like \"#{SecureRandom.hex(16)}\". The value you " \ + "provided, \"#{secret}\", is shorter than the minimum length " \ + "of #{SECRET_MIN_LENGTH} characters." end end end diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb index e489512531..e2b8f0f648 100644 --- a/activesupport/lib/active_support/lazy_load_hooks.rb +++ b/activesupport/lib/active_support/lazy_load_hooks.rb @@ -1,5 +1,5 @@ module ActiveSupport - # lazy_load_hooks allows rails to lazily load a lot of components and thus + # lazy_load_hooks allows Rails to lazily load a lot of components and thus # making the app boot faster. Because of this feature now there is no need to # require <tt>ActiveRecord::Base</tt> at boot time purely to apply # configuration. Instead a hook is registered that applies configuration once diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml index f4900dc935..a4563ace8f 100644 --- a/activesupport/lib/active_support/locale/en.yml +++ b/activesupport/lib/active_support/locale/en.yml @@ -16,9 +16,9 @@ en: abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] # Used in date_select and datetime_select. order: - - :year - - :month - - :day + - year + - month + - day time: formats: diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb index a58afc6b9d..e95dc5a866 100644 --- a/activesupport/lib/active_support/log_subscriber.rb +++ b/activesupport/lib/active_support/log_subscriber.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/class/attribute' +require 'active_support/subscriber' module ActiveSupport # ActiveSupport::LogSubscriber is an object set to consume @@ -33,7 +34,7 @@ module ActiveSupport # Log subscriber also has some helpers to deal with logging and automatically # flushes all logs when the request finishes (via action_dispatch.callback # notification) in a Rails environment. - class LogSubscriber + class LogSubscriber < Subscriber # Embed in a String to clear all previous ANSI sequences. CLEAR = "\e[0m" BOLD = "\e[1m" @@ -53,24 +54,15 @@ module ActiveSupport class << self def logger - @logger ||= Rails.logger if defined?(Rails) - @logger + @logger ||= if defined?(Rails) && Rails.respond_to?(:logger) + Rails.logger + end end attr_writer :logger - def attach_to(namespace, log_subscriber=new, notifier=ActiveSupport::Notifications) - log_subscribers << log_subscriber - - log_subscriber.public_methods(false).each do |event| - next if %w{ start finish }.include?(event.to_s) - - notifier.subscribe("#{event}.#{namespace}", log_subscriber) - end - end - def log_subscribers - @@log_subscribers ||= [] + subscribers end # Flush all log_subscribers' logger. @@ -79,39 +71,18 @@ module ActiveSupport end end - def initialize - @queue_key = [self.class.name, object_id].join "-" - super - end - def logger LogSubscriber.logger end def start(name, id, payload) - return unless logger - - e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload) - parent = event_stack.last - parent << e if parent - - event_stack.push e + super if logger end def finish(name, id, payload) - return unless logger - - finished = Time.now - event = event_stack.pop - event.end = finished - event.payload.merge!(payload) - - method = name.split('.').first - begin - send(method, event) - rescue Exception => e - logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" - end + super if logger + rescue Exception => e + logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}" end protected @@ -134,11 +105,5 @@ module ActiveSupport bold = bold ? BOLD : "" "#{bold}#{color}#{text}#{CLEAR}" end - - private - - def event_stack - Thread.current[@queue_key] ||= [] - end end end diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 63dad7e01a..75f353f62c 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -1,5 +1,5 @@ require 'active_support/log_subscriber' -require 'active_support/buffered_logger' +require 'active_support/logger' require 'active_support/notifications' module ActiveSupport @@ -15,7 +15,7 @@ module ActiveSupport # end # # def test_basic_query_logging - # Developer.all + # Developer.all.to_a # wait # assert_equal 1, @logger.logged(:debug).size # assert_match(/Developer Load/, @logger.logged(:debug).last) diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 4a55bbb350..33fccdcf95 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/logger_silence' require 'logger' diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index b7dc0689b0..92ab6fe648 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -1,5 +1,6 @@ require 'openssl' require 'base64' +require 'active_support/core_ext/array/extract_options' module ActiveSupport # MessageEncryptor is a simple way to encrypt values which get stored @@ -11,10 +12,11 @@ module ActiveSupport # This can be used in situations similar to the <tt>MessageVerifier</tt>, but # where you don't want users to be able to determine the value of the payload. # - # key = OpenSSL::Digest::SHA256.new('password').digest # => "\x89\xE0\x156\xAC..." - # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> - # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." - # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" + # salt = SecureRandom.random_bytes(64) + # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." + # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> + # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." + # crypt.decrypt_and_verify(encrypted_data) # => "my secret data" class MessageEncryptor module NullSerializer #:nodoc: def self.load(value) @@ -27,7 +29,7 @@ module ActiveSupport end class InvalidMessage < StandardError; end - OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError + OpenSSLCipherError = OpenSSL::Cipher::CipherError # 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 @@ -38,6 +40,7 @@ module ActiveSupport # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! @@ -45,7 +48,7 @@ module ActiveSupport @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' - @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer) + @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end @@ -65,22 +68,21 @@ module ActiveSupport def _encrypt(value) cipher = new_cipher - # Rely on OpenSSL for the initialization vector - iv = cipher.random_iv - cipher.encrypt cipher.key = @secret - cipher.iv = iv + + # Rely on OpenSSL for the initialization vector + iv = cipher.random_iv encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final - [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--") + "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.decode64(v)} + encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret @@ -90,7 +92,7 @@ module ActiveSupport decrypted_data << cipher.final @serializer.load(decrypted_data) - rescue OpenSSLCipherError, TypeError + rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index a87383fe99..eee9bbaead 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -1,5 +1,6 @@ require 'base64' require 'active_support/core_ext/object/blank' +require 'active_support/security_utils' module ActiveSupport # +MessageVerifier+ makes it easy to generate and verify messages which are @@ -19,45 +20,104 @@ module ActiveSupport # end # # By default it uses Marshal to serialize the message. If you want to use - # another serialization method, you can set the serializer attribute to - # something that responds to dump and load, e.g.: + # another serialization method, you can set the serializer in the options + # hash upon initialization: # - # @verifier.serializer = YAML + # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML) class MessageVerifier class InvalidSignature < StandardError; end def initialize(secret, options = {}) + raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal end - def verify(signed_message) - raise InvalidSignature if signed_message.blank? + # Checks if a signed message could have been generated by signing an object + # with the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # signed_message = verifier.generate 'a private message' + # verifier.valid_message?(signed_message) # => true + # + # tampered_message = signed_message.chop # editing the message invalidates the signature + # verifier.valid_message?(tampered_message) # => false + def valid_message?(signed_message) + return if signed_message.blank? data, digest = signed_message.split("--") - if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) - @serializer.load(::Base64.decode64(data)) - else - raise InvalidSignature + data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data)) + end + + # Decodes the signed message using the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # + # signed_message = verifier.generate 'a private message' + # verifier.verified(signed_message) # => 'a private message' + # + # Returns +nil+ if the message was not signed with the same secret. + # + # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier.verified(signed_message) # => nil + # + # Returns +nil+ if the message is not Base64-encoded. + # + # invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d" + # verifier.verified(invalid_message) # => nil + # + # Raises any error raised while decoding the signed message. + # + # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff" + # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format + def verified(signed_message) + if valid_message?(signed_message) + begin + data = signed_message.split("--")[0] + @serializer.load(decode(data)) + rescue ArgumentError => argument_error + return if argument_error.message =~ %r{invalid base64} + raise + end end end + # Decodes the signed message using the +MessageVerifier+'s secret. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # signed_message = verifier.generate 'a private message' + # + # verifier.verify(signed_message) # => 'a private message' + # + # Raises +InvalidSignature+ if the message was not signed with the same + # secret or was not Base64-encoded. + # + # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit' + # other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature + def verify(signed_message) + verified(signed_message) || raise(InvalidSignature) + end + + # Generates a signed message for the provided value. + # + # The message is signed with the +MessageVerifier+'s secret. Without knowing + # the secret, the original value cannot be extracted from the message. + # + # verifier = ActiveSupport::MessageVerifier.new 's3Krit' + # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772" def generate(value) - data = ::Base64.strict_encode64(@serializer.dump(value)) + data = encode(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end private - # constant-time comparison algorithm to prevent timing attacks - def secure_compare(a, b) - return false unless a.bytesize == b.bytesize - - l = a.unpack "C#{a.bytesize}" + def encode(data) + ::Base64.strict_encode64(data) + end - res = 0 - b.each_byte { |byte| res |= byte ^ l.shift } - res == 0 + def decode(data) + ::Base64.strict_decode64(data) end def generate_digest(data) diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index a42e7f6542..3c0cf9f137 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -56,11 +56,10 @@ module ActiveSupport #:nodoc: # Forward all undefined methods to the wrapped string. def method_missing(method, *args, &block) + result = @wrapped_string.__send__(method, *args, &block) if method.to_s =~ /!$/ - result = @wrapped_string.__send__(method, *args, &block) self if result else - result = @wrapped_string.__send__(method, *args, &block) result.kind_of?(String) ? chars(result) : result end end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index f49ca47f14..7ab6293b60 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -11,7 +11,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '6.1.0' + UNICODE_VERSION = '7.0.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -42,7 +42,6 @@ module ActiveSupport 0x0085, # White_Space # Cc <control-0085> 0x00A0, # White_Space # Zs NO-BREAK SPACE 0x1680, # White_Space # Zs OGHAM SPACE MARK - 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE 0x2028, # White_Space # Zl LINE SEPARATOR 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR @@ -145,7 +144,7 @@ module ActiveSupport ncp << (HANGUL_TBASE + tindex) unless tindex == 0 decomposed.concat ncp # if the codepoint is decomposable in with the current decomposition type - elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatability) + elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatibility) decomposed.concat decompose(type, ncp.dup) else decomposed << cp @@ -212,57 +211,44 @@ module ActiveSupport codepoints end - # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent - # resulting in a valid UTF-8 string. - # - # Passing +true+ will forcibly tidy all bytes, assuming that the string's - # encoding is entirely CP1252 or ISO-8859-1. - def tidy_bytes(string, force = false) - if force - return string.unpack("C*").map do |b| - tidy_byte(b) - end.flatten.compact.pack("C*").unpack("U*").pack("U*") + # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1. + # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars. + if '<3'.respond_to?(:scrub) && !defined?(Rubinius) + # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent + # resulting in a valid UTF-8 string. + # + # Passing +true+ will forcibly tidy all bytes, assuming that the string's + # encoding is entirely CP1252 or ISO-8859-1. + def tidy_bytes(string, force = false) + return string if string.empty? + return recode_windows1252_chars(string) if force + string.scrub { |bad| recode_windows1252_chars(bad) } end + else + def tidy_bytes(string, force = false) + return string if string.empty? + return recode_windows1252_chars(string) if force + + # We can't transcode to the same format, so we choose a nearly-identical encoding. + # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to + # CP1252 when we get errors. The final string will be 'converted' back to UTF-8 + # before returning. + reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE) + + source = string.dup + out = ''.force_encoding(Encoding::UTF_16LE) + + loop do + reader.primitive_convert(source, out) + _, _, _, error_bytes, _ = reader.primitive_errinfo + break if error_bytes.nil? + out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace) + end - bytes = string.unpack("C*") - conts_expected = 0 - last_lead = 0 - - bytes.each_index do |i| - - byte = bytes[i] - is_cont = byte > 127 && byte < 192 - is_lead = byte > 191 && byte < 245 - is_unused = byte > 240 - is_restricted = byte > 244 + reader.finish - # Impossible or highly unlikely byte? Clean it. - if is_unused || is_restricted - bytes[i] = tidy_byte(byte) - elsif is_cont - # Not expecting continuation byte? Clean up. Otherwise, now expect one less. - conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1 - else - if conts_expected > 0 - # Expected continuation, but got ASCII or leading? Clean backwards up to - # the leading byte. - (1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])} - conts_expected = 0 - end - if is_lead - # Final byte is leading? Clean it. - if i == bytes.length - 1 - bytes[i] = tidy_byte(bytes.last) - else - # Valid leading byte? Expect continuations determined by position of - # first zero bit, with max of 3. - conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3 - last_lead = i - end - end - end + out.encode!(Encoding::UTF_8) end - bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*") end # Returns the KC normalization of the string by default. NFKC is @@ -283,9 +269,9 @@ module ActiveSupport when :c compose(reorder_characters(decompose(:canonical, codepoints))) when :kd - reorder_characters(decompose(:compatability, codepoints)) + reorder_characters(decompose(:compatibility, codepoints)) when :kc - compose(reorder_characters(decompose(:compatability, codepoints))) + compose(reorder_characters(decompose(:compatibility, codepoints))) else raise ArgumentError, "#{form} is not a valid normalization variant", caller end.pack('U*') @@ -307,6 +293,13 @@ module ActiveSupport class Codepoint attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping + # Initializing Codepoint object with default values + def initialize + @combining_class = 0 + @uppercase_mapping = 0 + @lowercase_mapping = 0 + end + def swapcase_mapping uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping end @@ -342,7 +335,7 @@ module ActiveSupport begin @codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read } rescue => e - raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") + raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable") end # Redefine the === method so we can write shorter rules for grapheme cluster breaks @@ -374,6 +367,7 @@ module ActiveSupport private def apply_mapping(string, mapping) #:nodoc: + database.codepoints string.each_codepoint.map do |codepoint| cp = database.codepoints[codepoint] if cp and (ncp = cp.send(mapping)) and ncp > 0 @@ -384,20 +378,13 @@ module ActiveSupport end.pack('U*') end - def tidy_byte(byte) - if byte < 160 - [database.cp1252[byte] || byte].pack("U").unpack("C*") - elsif byte < 192 - [194, byte] - else - [195, byte - 64] - end + def recode_windows1252_chars(string) + string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) end def database @database ||= UnicodeDatabase.new end - end end end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 705a4693b7..b9f8e1ab2c 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -1,5 +1,6 @@ require 'active_support/notifications/instrumenter' require 'active_support/notifications/fanout' +require 'active_support/per_thread_registry' module ActiveSupport # = Notifications @@ -15,7 +16,7 @@ module ActiveSupport # render text: 'Foo' # end # - # That executes the block first and notifies all subscribers once done. + # That first executes the block and then notifies all subscribers once done. # # In the example above +render+ is the name of the event, and the rest is called # the _payload_. The payload is a mechanism that allows instrumenters to pass @@ -140,10 +141,15 @@ module ActiveSupport # # ActiveSupport::Notifications.unsubscribe(subscriber) # + # You can also unsubscribe by passing the name of the subscriber object. Note + # that this will unsubscribe all subscriptions with the given name: + # + # ActiveSupport::Notifications.unsubscribe("render") + # # == Default Queue # - # Notifications ships with a queue implementation that consumes and publish events - # to log subscribers in a thread. You can use any queue implementation you want. + # Notifications ships with a queue implementation that consumes and publishes events + # to all log subscribers. You can use any queue implementation you want. # module Notifications class << self @@ -172,12 +178,32 @@ module ActiveSupport unsubscribe(subscriber) end - def unsubscribe(args) - notifier.unsubscribe(args) + def unsubscribe(subscriber_or_name) + notifier.unsubscribe(subscriber_or_name) end def instrumenter - Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier) + InstrumentationRegistry.instance.instrumenter_for(notifier) + end + end + + # This class is a registry which holds all of the +Instrumenter+ objects + # in a particular thread local. To access the +Instrumenter+ object for a + # particular +notifier+, you can call the following method: + # + # InstrumentationRegistry.instrumenter_for(notifier) + # + # The instrumenters for multiple notifiers are held in a single instance of + # this class. + class InstrumentationRegistry # :nodoc: + extend ActiveSupport::PerThreadRegistry + + def initialize + @registry = {} + end + + def instrumenter_for(notifier) + @registry[notifier] ||= Instrumenter.new(notifier) end end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 7588fdb67c..6bf8c7d5de 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -25,9 +25,15 @@ module ActiveSupport subscriber end - def unsubscribe(subscriber) + def unsubscribe(subscriber_or_name) synchronize do - @subscribers.reject! { |s| s.matches?(subscriber) } + case subscriber_or_name + when String + @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + else + @subscribers.delete(subscriber_or_name) + end + @listeners_for.clear end end @@ -79,6 +85,13 @@ module ActiveSupport def initialize(pattern, delegate) @pattern = pattern @delegate = delegate + @can_publish = delegate.respond_to?(:publish) + end + + def publish(name, *args) + if @can_publish + @delegate.publish name, *args + end end def start(name, id, payload) @@ -90,31 +103,27 @@ module ActiveSupport end def subscribed_to?(name) - @pattern === name.to_s + @pattern === name end - def matches?(subscriber_or_name) - self === subscriber_or_name || - @pattern && @pattern === subscriber_or_name + def matches?(name) + @pattern && @pattern === name end end class Timed < Evented - def initialize(pattern, delegate) - @timestack = [] - super - end - def publish(name, *args) @delegate.call name, *args end def start(name, id, payload) - @timestack.push Time.now + timestack = Thread.current[:_timestack] ||= [] + timestack.push Time.now end def finish(name, id, payload) - started = @timestack.pop + timestack = Thread.current[:_timestack] + started = timestack.pop @delegate.call(name, started, Time.now, id, payload) end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index ab0b162ee0..075ddc2382 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -2,12 +2,12 @@ require 'securerandom' module ActiveSupport module Notifications - # Instrumentors are stored in a thread local. + # Instrumenters are stored in a thread local. class Instrumenter attr_reader :id def initialize(notifier) - @id = unique_id + @id = unique_id @notifier = notifier end @@ -15,21 +15,32 @@ module ActiveSupport # and publish it. Notice that events get sent even if an error occurs # in the passed-in block. def instrument(name, payload={}) - @notifier.start(name, @id, payload) + start name, payload begin - yield + yield payload rescue Exception => e payload[:exception] = [e.class.name, e.message] raise e ensure - @notifier.finish(name, @id, payload) + finish name, payload end end + # Send a start notification with +name+ and +payload+. + def start(name, payload) + @notifier.start name, @id, payload + end + + # Send a finish notification with +name+ and +payload+. + def finish(name, payload) + @notifier.finish name, @id, payload + end + private - def unique_id - SecureRandom.hex(10) - end + + def unique_id + SecureRandom.hex(10) + end end class Event @@ -43,10 +54,23 @@ module ActiveSupport @transaction_id = transaction_id @end = ending @children = [] + @duration = nil end + # Returns the difference in milliseconds between when the execution of the + # event started and when it ended. + # + # ActiveSupport::Notifications.subscribe('wait') do |*args| + # @event = ActiveSupport::Notifications::Event.new(*args) + # end + # + # ActiveSupport::Notifications.instrument('wait') do + # sleep 1 + # end + # + # @event.duration # => 1000.138 def duration - 1000.0 * (self.end - time) + @duration ||= 1000.0 * (self.end - time) end def <<(event) diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index 2191471daa..34439ee8be 100644 --- a/activesupport/lib/active_support/number_helper.rb +++ b/activesupport/lib/active_support/number_helper.rb @@ -1,115 +1,19 @@ -require 'active_support/core_ext/big_decimal/conversions' -require 'active_support/core_ext/object/blank' -require 'active_support/core_ext/hash/keys' -require 'active_support/i18n' - module ActiveSupport module NumberHelper - extend self - - DEFAULTS = { - # Used in number_to_delimited - # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' - format: { - # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) - separator: ".", - # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) - delimiter: ",", - # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) - precision: 3, - # If set to true, precision will mean the number of significant digits instead - # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) - significant: false, - # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) - strip_insignificant_zeros: false - }, - - # Used in number_to_currency - currency: { - format: { - format: "%u%n", - negative_format: "-%u%n", - unit: "$", - # These five are to override number.format and are optional - separator: ".", - delimiter: ",", - precision: 2, - significant: false, - strip_insignificant_zeros: false - } - }, - - # Used in number_to_percentage - percentage: { - format: { - delimiter: "", - format: "%n%" - } - }, - - # Used in number_to_rounded - precision: { - format: { - delimiter: "" - } - }, - - # Used in number_to_human_size and number_to_human - human: { - format: { - # These five are to override number.format and are optional - delimiter: "", - precision: 3, - significant: true, - strip_insignificant_zeros: true - }, - # Used in number_to_human_size - storage_units: { - # Storage units output formatting. - # %u is the storage unit, %n is the number (default: 2 MB) - format: "%n %u", - units: { - byte: "Bytes", - kb: "KB", - mb: "MB", - gb: "GB", - tb: "TB" - } - }, - # Used in number_to_human - decimal_units: { - format: "%n %u", - # Decimal units output formatting - # By default we will only quantify some of the exponents - # but the commented ones might be defined or overridden - # by the user. - units: { - # femto: Quadrillionth - # pico: Trillionth - # nano: Billionth - # micro: Millionth - # mili: Thousandth - # centi: Hundredth - # deci: Tenth - unit: "", - # ten: - # one: Ten - # other: Tens - # hundred: Hundred - thousand: "Thousand", - million: "Million", - billion: "Billion", - trillion: "Trillion", - quadrillion: "Quadrillion" - } - } - } - } - - DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, - -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto } + extend ActiveSupport::Autoload + + eager_autoload do + autoload :NumberConverter + autoload :NumberToRoundedConverter + autoload :NumberToDelimitedConverter + autoload :NumberToHumanConverter + autoload :NumberToHumanSizeConverter + autoload :NumberToPhoneConverter + autoload :NumberToCurrencyConverter + autoload :NumberToPercentageConverter + end - STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + extend self # Formats a +number+ into a US phone number (e.g., (555) # 123-9876). You can customize the format in the +options+ hash. @@ -137,27 +41,7 @@ module ActiveSupport # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.') # # => +1.123.555.1234 x 1343 def number_to_phone(number, options = {}) - return unless number - options = options.symbolize_keys - - number = number.to_s.strip - area_code = options[:area_code] - delimiter = options[:delimiter] || "-" - extension = options[:extension] - country_code = options[:country_code] - - if area_code - number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") - else - number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") - number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank? - end - - str = '' - str << "+#{country_code}#{delimiter}" unless country_code.blank? - str << number - str << " x #{extension}" unless extension.blank? - str + NumberToPhoneConverter.convert(number, options) end # Formats a +number+ into a currency string (e.g., $13.65). You @@ -199,25 +83,7 @@ module ActiveSupport # number_to_currency(1234567890.50, unit: '£', separator: ',', delimiter: '', format: '%n %u') # # => 1234567890,50 £ def number_to_currency(number, options = {}) - return unless number - options = options.symbolize_keys - - currency = i18n_format_options(options[:locale], :currency) - currency[:negative_format] ||= "-" + currency[:format] if currency[:format] - - defaults = default_format_options(:currency).merge!(currency) - defaults[:negative_format] = "-" + options[:format] if options[:format] - options = defaults.merge!(options) - - unit = options.delete(:unit) - format = options.delete(:format) - - if number.to_f.phase != 0 - format = options.delete(:negative_format) - number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '') - end - - format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit) + NumberToCurrencyConverter.convert(number, options) end # Formats a +number+ as a percentage string (e.g., 65%). You can @@ -244,23 +110,16 @@ module ActiveSupport # # ==== Examples # - # number_to_percentage(100) # => 100.000% - # number_to_percentage('98') # => 98.000% - # 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('98a') # => 98a% - # number_to_percentage(100, format: '%n %') # => 100 % + # number_to_percentage(100) # => 100.000% + # number_to_percentage('98') # => 98.000% + # 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('98a') # => 98a% + # number_to_percentage(100, format: '%n %') # => 100 % def number_to_percentage(number, options = {}) - return unless number - options = options.symbolize_keys - - defaults = format_options(options[:locale], :percentage) - options = defaults.merge!(options) - - format = options[:format] || "%n%" - format.gsub('%n', self.number_to_rounded(number, options)) + NumberToPercentageConverter.convert(number, options) end # Formats a +number+ with grouped thousands using +delimiter+ @@ -289,15 +148,7 @@ module ActiveSupport # number_to_delimited(98765432.98, delimiter: ' ', separator: ',') # # => 98 765 432,98 def number_to_delimited(number, options = {}) - options = options.symbolize_keys - - return number unless valid_float?(number) - - options = format_options(options[:locale]).merge!(options) - - parts = number.to_s.to_str.split('.') - parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}") - parts.join(options[:separator]) + NumberToDelimitedConverter.convert(number, options) end # Formats a +number+ with the specified level of @@ -340,38 +191,7 @@ module ActiveSupport # number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.') # # => 1.111,23 def number_to_rounded(number, options = {}) - return number unless valid_float?(number) - number = Float(number) - options = options.symbolize_keys - - defaults = format_options(options[:locale], :precision) - options = defaults.merge!(options) - - precision = options.delete :precision - significant = options.delete :significant - strip_insignificant_zeros = options.delete :strip_insignificant_zeros - - if significant && precision > 0 - if number == 0 - digits, rounded_number = 1, 0 - else - digits = (Math.log10(number.abs) + 1).floor - rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision) - digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed - end - precision -= digits - precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros - end - formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options) - if strip_insignificant_zeros - escaped_separator = Regexp.escape(options[:separator]) - formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') - else - formatted_number - end + NumberToRoundedConverter.convert(number, options) end # Formats the bytes in +number+ into a more understandable @@ -412,43 +232,10 @@ module ActiveSupport # number_to_human_size(1234567, precision: 2) # => 1.2 MB # number_to_human_size(483989, precision: 2) # => 470 KB # number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB - # - # Non-significant zeros after the fractional separator are stripped out by - # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that): - # - # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB" - # number_to_human_size(524288000, precision: 5) # => "500 MB" + # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB" + # number_to_human_size(524288000, precision: 5) # => "500 MB" def number_to_human_size(number, options = {}) - options = options.symbolize_keys - - return number unless valid_float?(number) - number = Float(number) - - defaults = format_options(options[:locale], :human) - options = defaults.merge!(options) - - #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files - options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - - storage_units_format = translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true) - - base = options[:prefix] == :si ? 1000 : 1024 - - if number.to_i < base - unit = translate_number_value_with_default('human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true) - storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit) - else - max_exp = STORAGE_UNITS.size - 1 - exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base - exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit - number /= base ** exponent - - unit_key = STORAGE_UNITS[exponent] - unit = translate_number_value_with_default("human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true) - - formatted_number = self.number_to_rounded(number, options) - storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit) - end + NumberToHumanSizeConverter.convert(number, options) end # Pretty prints (formats and approximates) a number in a way it @@ -459,7 +246,7 @@ module ActiveSupport # See <tt>number_to_human_size</tt> if you want to print a file # size. # - # You can also define you own unit-quantifier names if you want + # You can also define your own unit-quantifier names if you want # to use other decimal units (eg.: 1500 becomes "1.5 # kilometers", 0.150 becomes "150 milliliters", etc). You may # define a wide range of unit quantifiers, even fractional ones @@ -485,12 +272,12 @@ module ActiveSupport # string containing an i18n scope where to find this hash. It # might have the following keys: # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>, - # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, - # *<tt>:billion</tt>, <tt>:trillion</tt>, - # *<tt>:quadrillion</tt> + # <tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>, + # <tt>:billion</tt>, <tt>:trillion</tt>, + # <tt>:quadrillion</tt> # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>, - # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, - # *<tt>:pico</tt>, <tt>:femto</tt> + # <tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>, + # <tt>:pico</tt>, <tt>:femto</tt> # * <tt>:format</tt> - Sets the format of the output string # (defaults to "%n %u"). The field types are: # * %u - The quantifier (ex.: 'thousand') @@ -514,12 +301,15 @@ module ActiveSupport # separator: ',', # significant: false) # => "1,2 Million" # + # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12345012345, significant: false) # => "12.345 Billion" + # # Non-significant zeros after the decimal separator are stripped # out by default (set <tt>:strip_insignificant_zeros</tt> to # +false+ to change that): # - # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion" - # number_to_human(500000000, precision: 5) # => "500 Million" + # number_to_human(12.00001) # => "12" + # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0" # # ==== Custom Unit Quantifiers # @@ -549,88 +339,7 @@ module ActiveSupport # number_to_human(1, units: :distance) # => "1 meter" # number_to_human(0.34, units: :distance) # => "34 centimeters" def number_to_human(number, options = {}) - options = options.symbolize_keys - - return number unless valid_float?(number) - number = Float(number) - - defaults = format_options(options[:locale], :human) - options = defaults.merge!(options) - - #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files - options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros) - - inverted_du = DECIMAL_UNITS.invert - - units = options.delete :units - unit_exponents = case units - when Hash - units - when String, Symbol - I18n.translate(:"#{units}", :locale => options[:locale], :raise => true) - when nil - translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true) - else - raise ArgumentError, ":units must be a Hash or String translation scope." - end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e} - - number_exponent = number != 0 ? Math.log10(number.abs).floor : 0 - display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0 - number /= 10 ** display_exponent - - unit = case units - when Hash - units[DECIMAL_UNITS[display_exponent]] - when String, Symbol - I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) - else - translate_number_value_with_default("human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i) - end - - decimal_format = options[:format] || translate_number_value_with_default('human.decimal_units.format', :locale => options[:locale]) - formatted_number = self.number_to_rounded(number, options) - decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip - end - - def self.private_module_and_instance_method(method_name) #:nodoc: - private method_name - private_class_method method_name - end - private_class_method :private_module_and_instance_method - - def format_options(locale, namespace = nil) #:nodoc: - default_format_options(namespace).merge!(i18n_format_options(locale, namespace)) - end - private_module_and_instance_method :format_options - - def default_format_options(namespace = nil) #:nodoc: - options = DEFAULTS[:format].dup - options.merge!(DEFAULTS[namespace][:format]) if namespace - options - end - private_module_and_instance_method :default_format_options - - def i18n_format_options(locale, namespace = nil) #:nodoc: - options = I18n.translate(:'number.format', locale: locale, default: {}).dup - if namespace - options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {})) - end - options - end - private_module_and_instance_method :i18n_format_options - - def translate_number_value_with_default(key, i18n_options = {}) #:nodoc: - default = key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] } - - I18n.translate(key, { default: default, scope: :number }.merge!(i18n_options)) - end - private_module_and_instance_method :translate_number_value_with_default - - def valid_float?(number) #:nodoc: - Float(number) - rescue ArgumentError, TypeError - false + NumberToHumanConverter.convert(number, options) end - private_module_and_instance_method :valid_float? end end diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb new file mode 100644 index 0000000000..9d976f1831 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_converter.rb @@ -0,0 +1,182 @@ +require 'active_support/core_ext/big_decimal/conversions' +require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/hash/keys' +require 'active_support/i18n' +require 'active_support/core_ext/class/attribute' + +module ActiveSupport + module NumberHelper + class NumberConverter # :nodoc: + # Default and i18n option namespace per class + class_attribute :namespace + + # Does the object need a number that is a valid float? + class_attribute :validate_float + + attr_reader :number, :opts + + DEFAULTS = { + # Used in number_to_delimited + # These are also the defaults for 'currency', 'percentage', 'precision', and 'human' + format: { + # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5) + separator: ".", + # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three) + delimiter: ",", + # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00) + precision: 3, + # If set to true, precision will mean the number of significant digits instead + # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2) + significant: false, + # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2) + strip_insignificant_zeros: false + }, + + # Used in number_to_currency + currency: { + format: { + format: "%u%n", + negative_format: "-%u%n", + unit: "$", + # These five are to override number.format and are optional + separator: ".", + delimiter: ",", + precision: 2, + significant: false, + strip_insignificant_zeros: false + } + }, + + # Used in number_to_percentage + percentage: { + format: { + delimiter: "", + format: "%n%" + } + }, + + # Used in number_to_rounded + precision: { + format: { + delimiter: "" + } + }, + + # Used in number_to_human_size and number_to_human + human: { + format: { + # These five are to override number.format and are optional + delimiter: "", + precision: 3, + significant: true, + strip_insignificant_zeros: true + }, + # Used in number_to_human_size + storage_units: { + # Storage units output formatting. + # %u is the storage unit, %n is the number (default: 2 MB) + format: "%n %u", + units: { + byte: "Bytes", + kb: "KB", + mb: "MB", + gb: "GB", + tb: "TB" + } + }, + # Used in number_to_human + decimal_units: { + format: "%n %u", + # Decimal units output formatting + # By default we will only quantify some of the exponents + # but the commented ones might be defined or overridden + # by the user. + units: { + # femto: Quadrillionth + # pico: Trillionth + # nano: Billionth + # micro: Millionth + # mili: Thousandth + # centi: Hundredth + # deci: Tenth + unit: "", + # ten: + # one: Ten + # other: Tens + # hundred: Hundred + thousand: "Thousand", + million: "Million", + billion: "Billion", + trillion: "Trillion", + quadrillion: "Quadrillion" + } + } + } + } + + def self.convert(number, options) + new(number, options).execute + end + + def initialize(number, options) + @number = number + @opts = options.symbolize_keys + end + + def execute + if !number + nil + elsif validate_float? && !valid_float? + number + else + convert + end + end + + private + + def options + @options ||= format_options.merge(opts) + end + + def format_options #:nodoc: + default_format_options.merge!(i18n_format_options) + end + + def default_format_options #:nodoc: + options = DEFAULTS[:format].dup + options.merge!(DEFAULTS[namespace][:format]) if namespace + options + end + + def i18n_format_options #:nodoc: + locale = opts[:locale] + options = I18n.translate(:'number.format', locale: locale, default: {}).dup + + if namespace + options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {})) + end + + options + end + + def translate_number_value_with_default(key, i18n_options = {}) #:nodoc: + I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options)) + end + + def translate_in_locale(key, i18n_options = {}) + translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options)) + end + + def default_value(key) + key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] } + end + + def valid_float? #:nodoc: + Float(number) + rescue ArgumentError, TypeError + false + end + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb new file mode 100644 index 0000000000..fb5adb574a --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb @@ -0,0 +1,46 @@ +module ActiveSupport + module NumberHelper + class NumberToCurrencyConverter < NumberConverter # :nodoc: + self.namespace = :currency + + def convert + number = self.number.to_s.strip + format = options[:format] + + if is_negative?(number) + format = options[:negative_format] + number = absolute_value(number) + end + + rounded_number = NumberToRoundedConverter.convert(number, options) + format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit]) + end + + private + + def is_negative?(number) + number.to_f.phase != 0 + end + + def absolute_value(number) + number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '') + end + + def options + @options ||= begin + defaults = default_format_options.merge(i18n_opts) + # Override negative format if format options is given + defaults[:negative_format] = "-#{opts[:format]}" if opts[:format] + defaults.merge!(opts) + end + end + + def i18n_opts + # Set International negative format if not exists + i18n = i18n_format_options + i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format] + i18n + end + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb new file mode 100644 index 0000000000..d85cc086d7 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -0,0 +1,23 @@ +module ActiveSupport + module NumberHelper + class NumberToDelimitedConverter < NumberConverter #:nodoc: + self.validate_float = true + + DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/ + + def convert + parts.join(options[:separator]) + end + + private + + def parts + left, right = number.to_s.split('.') + left.gsub!(DELIMITED_REGEX) do |digit_to_delimit| + "#{digit_to_delimit}#{options[:delimiter]}" + end + [left, right].compact + end + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb new file mode 100644 index 0000000000..6940beb318 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb @@ -0,0 +1,66 @@ +module ActiveSupport + module NumberHelper + class NumberToHumanConverter < NumberConverter # :nodoc: + DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion, + -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto } + INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert + + self.namespace = :human + self.validate_float = true + + def convert # :nodoc: + @number = Float(number) + + # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + unless options.key?(:strip_insignificant_zeros) + options[:strip_insignificant_zeros] = true + end + + units = opts[:units] + exponent = calculate_exponent(units) + @number = number / (10 ** exponent) + + unit = determine_unit(units, exponent) + + rounded_number = NumberToRoundedConverter.convert(number, options) + format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip + end + + private + + def format + options[:format] || translate_in_locale('human.decimal_units.format') + end + + def determine_unit(units, exponent) + exp = DECIMAL_UNITS[exponent] + case units + when Hash + units[exp] || '' + when String, Symbol + I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i) + else + translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i) + end + end + + def calculate_exponent(units) + exponent = number != 0 ? Math.log10(number.abs).floor : 0 + unit_exponents(units).find { |e| exponent >= e } || 0 + end + + def unit_exponents(units) + case units + when Hash + units + when String, Symbol + I18n.translate(units.to_s, :locale => options[:locale], :raise => true) + when nil + translate_in_locale("human.decimal_units.units", raise: true) + else + raise ArgumentError, ":units must be a Hash or String translation scope." + end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@) + end + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb new file mode 100644 index 0000000000..78d2c9ae6e --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb @@ -0,0 +1,58 @@ +module ActiveSupport + module NumberHelper + class NumberToHumanSizeConverter < NumberConverter #:nodoc: + STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb] + + self.namespace = :human + self.validate_float = true + + def convert + @number = Float(number) + + # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files + unless options.key?(:strip_insignificant_zeros) + options[:strip_insignificant_zeros] = true + end + + if smaller_than_base? + number_to_format = number.to_i.to_s + else + human_size = number / (base ** exponent) + number_to_format = NumberToRoundedConverter.convert(human_size, options) + end + conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit) + end + + private + + def conversion_format + translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true) + end + + def unit + translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => number.to_i, :raise => true) + end + + def storage_unit_key + key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent] + "human.storage_units.units.#{key_end}" + end + + def exponent + max = STORAGE_UNITS.size - 1 + exp = (Math.log(number) / Math.log(base)).to_i + exp = max if exp > max # avoid overflow for the highest unit + exp + end + + def smaller_than_base? + number.to_i < base + end + + def base + opts[:prefix] == :si ? 1000 : 1024 + end + end + end +end + diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb new file mode 100644 index 0000000000..1af294a03e --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb @@ -0,0 +1,12 @@ +module ActiveSupport + module NumberHelper + class NumberToPercentageConverter < NumberConverter # :nodoc: + self.namespace = :percentage + + def convert + rounded_number = NumberToRoundedConverter.convert(number, options) + options[:format].gsub(/%n/, rounded_number) + end + end + end +end diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb new file mode 100644 index 0000000000..af2ee56d91 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb @@ -0,0 +1,49 @@ +module ActiveSupport + module NumberHelper + class NumberToPhoneConverter < NumberConverter #:nodoc: + def convert + str = country_code(opts[:country_code]) + str << convert_to_phone_number(number.to_s.strip) + str << phone_ext(opts[:extension]) + end + + private + + def convert_to_phone_number(number) + if opts[:area_code] + convert_with_area_code(number) + else + convert_without_area_code(number) + end + end + + def convert_with_area_code(number) + number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3") + number + end + + def convert_without_area_code(number) + number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3") + number.slice!(0, 1) if start_with_delimiter?(number) + number + end + + def start_with_delimiter?(number) + delimiter.present? && number.start_with?(delimiter) + end + + def delimiter + opts[:delimiter] || "-" + end + + def country_code(code) + code.blank? ? "" : "+#{code}#{delimiter}" + end + + def phone_ext(ext) + ext.blank? ? "" : " x #{ext}" + end + end + end +end + diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb new file mode 100644 index 0000000000..dcf9a567e8 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -0,0 +1,87 @@ +module ActiveSupport + module NumberHelper + class NumberToRoundedConverter < NumberConverter # :nodoc: + self.namespace = :precision + self.validate_float = true + + def convert + precision = options.delete :precision + significant = options.delete :significant + + case number + when Float, String + @number = BigDecimal(number.to_s) + when Rational + @number = BigDecimal(number, digit_count(number.to_i) + precision) + else + @number = number.to_d + end + + if significant && precision > 0 + digits, rounded_number = digits_and_rounded_number(precision) + precision -= digits + precision = 0 if precision < 0 # don't let it be negative + else + rounded_number = number.round(precision) + rounded_number = rounded_number.to_i if precision == 0 + rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros + end + + formatted_string = + if BigDecimal === rounded_number && rounded_number.finite? + s = rounded_number.to_s('F') + '0'*precision + a, b = s.split('.', 2) + a + '.' + b[0, precision] + else + "%00.#{precision}f" % rounded_number + end + + delimited_number = NumberToDelimitedConverter.convert(formatted_string, options) + format_number(delimited_number) + end + + private + + def digits_and_rounded_number(precision) + if zero? + [1, 0] + else + digits = digit_count(number) + multiplier = 10 ** (digits - precision) + rounded_number = calculate_rounded_number(multiplier) + digits = digit_count(rounded_number) # After rounding, the number of digits may have changed + [digits, rounded_number] + end + end + + def calculate_rounded_number(multiplier) + (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier + end + + def digit_count(number) + number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor + end + + def strip_insignificant_zeros + options[:strip_insignificant_zeros] + end + + def format_number(number) + if strip_insignificant_zeros + escaped_separator = Regexp.escape(options[:separator]) + number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '') + else + number + end + end + + def absolute_number(number) + number.respond_to?(:abs) ? number.abs : number.to_d.abs + end + + def zero? + number.respond_to?(:zero?) ? number.zero? : number.to_d.zero? + end + end + end +end diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb index e55ffd12c3..dea84e437f 100644 --- a/activesupport/lib/active_support/option_merger.rb +++ b/activesupport/lib/active_support/option_merger.rb @@ -12,7 +12,7 @@ module ActiveSupport private def method_missing(method, *arguments, &block) - if arguments.last.is_a?(Proc) + if arguments.first.is_a?(Proc) proc = arguments.pop arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) } else diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 1a3693f766..4680d5acb7 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -28,6 +28,14 @@ module ActiveSupport coder.represent_seq '!omap', map { |k,v| { k => v } } end + def select(*args, &block) + dup.tap { |hash| hash.select!(*args, &block) } + end + + def reject(*args, &block) + dup.tap { |hash| hash.reject!(*args, &block) } + end + def nested_under_indifferent_access self end diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index c9518bda79..a33e2c58a9 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -40,6 +40,14 @@ module ActiveSupport end end + # +InheritableOptions+ provides a constructor to build an +OrderedOptions+ + # hash inherited from another hash. + # + # Use this if you already have some hash and you want to create a new one based on it. + # + # h = ActiveSupport::InheritableOptions.new({ girl: 'Mary', boy: 'John' }) + # h.girl # => 'Mary' + # h.boy # => 'John' class InheritableOptions < OrderedOptions def initialize(parent = nil) if parent.kind_of?(OrderedOptions) diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb new file mode 100644 index 0000000000..ca2e4d5625 --- /dev/null +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -0,0 +1,53 @@ +module ActiveSupport + # This module is used to encapsulate access to thread local variables. + # + # Instead of polluting the thread locals namespace: + # + # Thread.current[:connection_handler] + # + # you define a class that extends this module: + # + # module ActiveRecord + # class RuntimeRegistry + # extend ActiveSupport::PerThreadRegistry + # + # attr_accessor :connection_handler + # end + # end + # + # and invoke the declared instance accessors as class methods. So + # + # ActiveRecord::RuntimeRegistry.connection_handler = connection_handler + # + # sets a connection handler local to the current thread, and + # + # ActiveRecord::RuntimeRegistry.connection_handler + # + # returns a connection handler local to the current thread. + # + # This feature is accomplished by instantiating the class and storing the + # instance as a thread local keyed by the class name. In the example above + # a key "ActiveRecord::RuntimeRegistry" is stored in <tt>Thread.current</tt>. + # The class methods proxy to said thread local instance. + # + # If the class has an initializer, it must accept no arguments. + module PerThreadRegistry + def self.extended(object) + object.instance_variable_set '@per_thread_registry_key', object.name.freeze + end + + def instance + Thread.current[@per_thread_registry_key] ||= new + end + + protected + def method_missing(name, *args, &block) # :nodoc: + # Caches the method definition as a singleton method of the receiver. + define_singleton_method(name) do |*a, &b| + instance.public_send(name, *a, &b) + end + + send(name, *args, &block) + end + end +end diff --git a/activesupport/lib/active_support/proxy_object.rb b/activesupport/lib/active_support/proxy_object.rb index a2bdf1d790..20a0fd8e62 100644 --- a/activesupport/lib/active_support/proxy_object.rb +++ b/activesupport/lib/active_support/proxy_object.rb @@ -5,7 +5,7 @@ module ActiveSupport undef_method :== undef_method :equal? - # Let ActiveSupport::BasicObject at least raise exceptions. + # Let ActiveSupport::ProxyObject at least raise exceptions. def raise(*args) ::Object.send(:raise, *args) end diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb index 9a038dfbca..1a02acd5b1 100644 --- a/activesupport/lib/active_support/rescuable.rb +++ b/activesupport/lib/active_support/rescuable.rb @@ -1,6 +1,5 @@ require 'active_support/concern' require 'active_support/core_ext/class/attribute' -require 'active_support/core_ext/proc' require 'active_support/core_ext/string/inflections' require 'active_support/core_ext/array/extract_options' @@ -61,7 +60,7 @@ module ActiveSupport end klasses.each do |klass| - key = if klass.is_a?(Class) && klass <= Exception + key = if klass.is_a?(Module) && klass.respond_to?(:===) klass.name elsif klass.is_a?(String) klass @@ -102,7 +101,7 @@ module ActiveSupport # itself when rescue_from CONSTANT is executed. klass = self.class.const_get(klass_name) rescue nil klass ||= klass_name.constantize rescue nil - exception.is_a?(klass) if klass + klass === exception if klass end case rescuer diff --git a/activesupport/lib/active_support/security_utils.rb b/activesupport/lib/active_support/security_utils.rb new file mode 100644 index 0000000000..64c4801179 --- /dev/null +++ b/activesupport/lib/active_support/security_utils.rb @@ -0,0 +1,20 @@ +module ActiveSupport + module SecurityUtils + # Constant time string comparison. + # + # The values compared should be of fixed length, such as strings + # that have already been processed by HMAC. This should not be used + # on variable length plaintext strings because it could leak length info + # via timing attacks. + def secure_compare(a, b) + return false unless a.bytesize == b.bytesize + + l = a.unpack "C#{a.bytesize}" + + res = 0 + b.each_byte { |byte| res |= byte ^ l.shift } + res == 0 + end + module_function :secure_compare + end +end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb new file mode 100644 index 0000000000..98be78b41b --- /dev/null +++ b/activesupport/lib/active_support/subscriber.rb @@ -0,0 +1,125 @@ +require 'active_support/per_thread_registry' + +module ActiveSupport + # ActiveSupport::Subscriber is an object set to consume + # ActiveSupport::Notifications. The subscriber dispatches notifications to + # a registered object based on its given namespace. + # + # An example would be Active Record subscriber responsible for collecting + # statistics about queries: + # + # module ActiveRecord + # class StatsSubscriber < ActiveSupport::Subscriber + # def sql(event) + # Statsd.timing("sql.#{event.payload[:name]}", event.duration) + # end + # end + # end + # + # And it's finally registered as: + # + # ActiveRecord::StatsSubscriber.attach_to :active_record + # + # Since we need to know all instance methods before attaching the log + # subscriber, the line above should be called after your subscriber definition. + # + # After configured, whenever a "sql.active_record" notification is published, + # it will properly dispatch the event (ActiveSupport::Notifications::Event) to + # the +sql+ method. + class Subscriber + class << self + + # Attach the subscriber to a namespace. + def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications) + @namespace = namespace + @subscriber = subscriber + @notifier = notifier + + subscribers << subscriber + + # Add event subscribers for all existing methods on the class. + subscriber.public_methods(false).each do |event| + add_event_subscriber(event) + end + end + + # Adds event subscribers for all new methods added to the class. + def method_added(event) + # Only public methods are added as subscribers, and only if a notifier + # has been set up. This means that subscribers will only be set up for + # classes that call #attach_to. + if public_method_defined?(event) && notifier + add_event_subscriber(event) + end + end + + def subscribers + @@subscribers ||= [] + end + + protected + + attr_reader :subscriber, :notifier, :namespace + + def add_event_subscriber(event) + return if %w{ start finish }.include?(event.to_s) + + pattern = "#{event}.#{namespace}" + + # don't add multiple subscribers (eg. if methods are redefined) + return if subscriber.patterns.include?(pattern) + + subscriber.patterns << pattern + notifier.subscribe(pattern, subscriber) + end + end + + attr_reader :patterns # :nodoc: + + def initialize + @queue_key = [self.class.name, object_id].join "-" + @patterns = [] + super + end + + def start(name, id, payload) + e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload) + parent = event_stack.last + parent << e if parent + + event_stack.push e + end + + def finish(name, id, payload) + finished = Time.now + event = event_stack.pop + event.end = finished + event.payload.merge!(payload) + + method = name.split('.').first + send(method, event) + end + + private + + def event_stack + SubscriberQueueRegistry.instance.get_queue(@queue_key) + end + end + + # This is a registry for all the event stacks kept for subscribers. + # + # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt> + # for further details. + class SubscriberQueueRegistry # :nodoc: + extend PerThreadRegistry + + def initialize + @registry = {} + end + + def get_queue(queue_key) + @registry[queue_key] ||= [] + end + end +end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index 18bc919734..d5c2222d2e 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'logger' require 'active_support/logger' diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 8b392c36d0..98b68455ab 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -1,42 +1,75 @@ gem 'minitest' # make sure we get the gem, not stdlib -require 'minitest/unit' +require 'minitest' require 'active_support/testing/tagged_logging' require 'active_support/testing/setup_and_teardown' require 'active_support/testing/assertions' require 'active_support/testing/deprecation' -require 'active_support/testing/pending' require 'active_support/testing/declarative' require 'active_support/testing/isolation' require 'active_support/testing/constant_lookup' +require 'active_support/testing/time_helpers' require 'active_support/core_ext/kernel/reporting' require 'active_support/deprecation' -begin - silence_warnings { require 'mocha/setup' } -rescue LoadError -end - module ActiveSupport - class TestCase < ::MiniTest::Unit::TestCase - Assertion = MiniTest::Assertion - alias_method :method_name, :__name__ + class TestCase < ::Minitest::Test + Assertion = Minitest::Assertion - $tags = {} - def self.for_tag(tag) - yield if $tags[tag] - end + class << self + # Sets the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order = :random # => :random + # + # Valid values are: + # * +:random+ (to run tests in random order) + # * +:parallel+ (to run tests in parallel) + # * +:sorted+ (to run tests alphabetically by method name) + # * +:alpha+ (equivalent to +:sorted+) + def test_order=(new_order) + ActiveSupport.test_order = new_order + end + + # Returns the order in which test cases are run. + # + # ActiveSupport::TestCase.test_order # => :sorted + # + # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. + # Defaults to +:sorted+. + def test_order + test_order = ActiveSupport.test_order - # FIXME: we have tests that depend on run order, we should fix that and - # remove this method. - def self.test_order # :nodoc: - :sorted + if test_order.nil? + ActiveSupport::Deprecation.warn "You did not specify a value for the " \ + "configuration option `active_support.test_order`. In Rails 5, " \ + "the default value of this option will change from `:sorted` to " \ + "`:random`.\n" \ + "To disable this warning and keep the current behavior, you can add " \ + "the following line to your `config/environments/test.rb`:\n" \ + "\n" \ + " Rails.application.configure do\n" \ + " config.active_support.test_order = :sorted\n" \ + " end\n" \ + "\n" \ + "Alternatively, you can opt into the future behavior by setting this " \ + "option to `:random`." + + test_order = :sorted + self.test_order = test_order + end + + test_order + end + + alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end + alias_method :method_name, :name + include ActiveSupport::Testing::TaggedLogging include ActiveSupport::Testing::SetupAndTeardown include ActiveSupport::Testing::Assertions include ActiveSupport::Testing::Deprecation - include ActiveSupport::Testing::Pending + include ActiveSupport::Testing::TimeHelpers extend ActiveSupport::Testing::Declarative # test/unit backwards compatibility methods diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 88aebba5c5..8b649c193f 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -9,7 +9,7 @@ module ActiveSupport # # assert_not nil # => true # assert_not false # => true - # assert_not 'foo' # => 'foo' is not nil or false + # assert_not 'foo' # => Expected "foo" to be nil or false # # An error message can be specified. # @@ -66,7 +66,7 @@ module ActiveSupport exps = expressions.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } - before = exps.map { |e| e.call } + before = exps.map(&:call) yield @@ -92,34 +92,6 @@ module ActiveSupport def assert_no_difference(expression, message = nil, &block) assert_difference expression, 0, message, &block end - - # Test if an expression is blank. Passes if <tt>object.blank?</tt> - # is +true+. - # - # assert_blank [] # => true - # assert_blank [[]] # => [[]] is not blank - # - # An error message can be specified. - # - # assert_blank [], 'this should be blank' - def assert_blank(object, message=nil) - message ||= "#{object.inspect} is not blank" - assert object.blank?, message - end - - # Test if an expression is not blank. Passes if <tt>object.present?</tt> - # is +true+. - # - # assert_present({ data: 'x' }) # => true - # assert_present({}) # => {} is blank - # - # An error message can be specified. - # - # assert_present({ data: 'x' }, 'this should not be blank') - def assert_present(object, message=nil) - message ||= "#{object.inspect} is blank" - assert object.present?, message - end end end end diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb index c446adc16d..5aa5f46310 100644 --- a/activesupport/lib/active_support/testing/autorun.rb +++ b/activesupport/lib/active_support/testing/autorun.rb @@ -1,5 +1,5 @@ gem 'minitest' -require 'minitest/unit' +require 'minitest' -MiniTest::Unit.autorun +Minitest.autorun diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb index 52bfeb7179..07d477c0db 100644 --- a/activesupport/lib/active_support/testing/constant_lookup.rb +++ b/activesupport/lib/active_support/testing/constant_lookup.rb @@ -36,10 +36,8 @@ module ActiveSupport while names.size > 0 do names.last.sub!(/Test$/, "") begin - constant = names.join("::").constantize + constant = names.join("::").safe_constantize break(constant) if yield(constant) - rescue NameError - # Constant wasn't found, move on ensure names.pop end diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index 508e37254a..0bf3643a56 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -1,30 +1,16 @@ module ActiveSupport module Testing module Declarative - - def self.extended(klass) #:nodoc: - klass.class_eval do - - unless method_defined?(:describe) - def self.describe(text) - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.name - "#{text}" - end - RUBY_EVAL - end - end - - end - end - unless defined?(Spec) - # test "verify something" do - # ... - # end + # Helper to define a test method using a String. Under the hood, it replaces + # spaces with underscores and defines the test method. + # + # test "verify something" do + # ... + # end def test(name, &block) test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym - defined = instance_method(test_name) rescue false + defined = method_defined? test_name raise "#{test_name} is already defined in #{self}" if defined if block_given? define_method(test_name, &block) diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index a8342904dc..6c94c611b6 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -19,18 +19,17 @@ module ActiveSupport result end - private - def collect_deprecations - old_behavior = ActiveSupport::Deprecation.behavior - deprecations = [] - ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| - deprecations << message - end - result = yield - [result, deprecations] - ensure - ActiveSupport::Deprecation.behavior = old_behavior + def collect_deprecations + old_behavior = ActiveSupport::Deprecation.behavior + deprecations = [] + ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| + deprecations << message end + result = yield + [result, deprecations] + ensure + ActiveSupport::Deprecation.behavior = old_behavior + end end end end diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index 27d444fd91..68bda35980 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,109 +1,49 @@ require 'rbconfig' + module ActiveSupport module Testing - class RemoteError < StandardError - - attr_reader :message, :backtrace - - def initialize(exception) - @message = "caught #{exception.class.name}: #{exception.message}" - @backtrace = exception.backtrace - end - end - - class ProxyTestResult - def initialize - @calls = [] - end - - def add_error(e) - e = Test::Unit::Error.new(e.test_name, RemoteError.new(e.exception)) - @calls << [:add_error, e] - end - - def __replay__(result) - @calls.each do |name, args| - result.send(name, *args) - end - end - - def method_missing(name, *args) - @calls << [name, args] - end - end - module Isolation require 'thread' - class ParallelEach - include Enumerable - - # default to 2 cores - CORES = (ENV['TEST_CORES'] || 2).to_i - - def initialize list - @list = list - @queue = SizedQueue.new CORES - end - - def grep pattern - self.class.new super - end - - def each - threads = CORES.times.map { - Thread.new { - while job = @queue.pop - yield job - end - } - } - @list.each { |i| @queue << i } - CORES.times { @queue << nil } - threads.each(&:join) - end - end - def self.included(klass) #:nodoc: - klass.extend(Module.new { - def test_methods - ParallelEach.new super - end - }) + klass.class_eval do + parallelize_me! + end end def self.forking_env? !ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/)) end + @@class_setup_mutex = Mutex.new + def _run_class_setup # class setup method should only happen in parent - unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true + @@class_setup_mutex.synchronize do + unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] + self.class.setup if self.class.respond_to?(:setup) + @@ran_class_setup = true + end end end - def run(runner) - _run_class_setup - - serialized = run_in_isolation do |isolated_runner| - super(isolated_runner) + def run + serialized = run_in_isolation do + super end - retval, proxy = Marshal.load(serialized) - proxy.__replay__(runner) - retval + Marshal.load(serialized) end module Forking def run_in_isolation(&blk) read, write = IO.pipe + read.binmode + write.binmode pid = fork do read.close - proxy = ProxyTestResult.new - retval = yield proxy - write.puts [Marshal.dump([retval, proxy])].pack("m") + yield + write.puts [Marshal.dump(self.dup)].pack("m") exit! end @@ -123,22 +63,31 @@ module ActiveSupport require "tempfile" if ENV["ISOLATION_TEST"] - proxy = ProxyTestResult.new - retval = yield proxy + yield File.open(ENV["ISOLATION_OUTPUT"], "w") do |file| - file.puts [Marshal.dump([retval, proxy])].pack("m") + file.puts [Marshal.dump(self.dup)].pack("m") end exit! else Tempfile.open("isolation") do |tmpfile| - ENV["ISOLATION_TEST"] = @method_name - ENV["ISOLATION_OUTPUT"] = tmpfile.path + env = { + ISOLATION_TEST: self.class.name, + ISOLATION_OUTPUT: tmpfile.path + } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")} -t\"#{self.class}\"` + orig_args = ORIG_ARGV.join(" ") + test_opts = "-n#{self.class.name}##{self.name}" + command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" - ENV.delete("ISOLATION_TEST") - ENV.delete("ISOLATION_OUTPUT") + # IO.popen lets us pass env in a cross-platform way + child = IO.popen([env, command]) + + begin + Process.wait(child.pid) + rescue Errno::ECHILD # The child process may exit before we wait + nil + end return tmpfile.read.unpack("m")[0] end diff --git a/activesupport/lib/active_support/testing/pending.rb b/activesupport/lib/active_support/testing/pending.rb deleted file mode 100644 index b04bbbbaea..0000000000 --- a/activesupport/lib/active_support/testing/pending.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'active_support/deprecation' - -module ActiveSupport - module Testing - module Pending # :nodoc: - unless defined?(Spec) - def pending(description = "", &block) - ActiveSupport::Deprecation.warn("#pending is deprecated and will be removed in Rails 4.1, please use #skip instead.") - skip(description.blank? ? nil : description) - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb deleted file mode 100644 index 7102ffe2ed..0000000000 --- a/activesupport/lib/active_support/testing/performance.rb +++ /dev/null @@ -1,271 +0,0 @@ -require 'fileutils' -require 'active_support/concern' -require 'active_support/core_ext/class/delegating_attributes' -require 'active_support/core_ext/string/inflections' -require 'active_support/core_ext/module/delegation' -require 'active_support/number_helper' - -module ActiveSupport - module Testing - module Performance - extend ActiveSupport::Concern - - included do - superclass_delegating_accessor :profile_options - self.profile_options = {} - end - - # each implementation should define metrics and freeze the defaults - DEFAULTS = - if ARGV.include?('--benchmark') # HAX for rake test - { :runs => 4, - :output => 'tmp/performance', - :benchmark => true } - else - { :runs => 1, - :output => 'tmp/performance', - :benchmark => false } - end - - def full_profile_options - DEFAULTS.merge(profile_options) - end - - def full_test_name - "#{self.class.name}##{method_name}" - end - - def run(runner) - @runner = runner - - run_warmup - if full_profile_options && metrics = full_profile_options[:metrics] - metrics.each do |metric_name| - if klass = Metrics[metric_name.to_sym] - run_profile(klass.new) - end - end - end - - return - end - - def run_test(metric, mode) - result = '.' - begin - run_callbacks :setup - setup - metric.send(mode) { __send__ method_name } - rescue Exception => e - result = @runner.puke(self.class, method_name, e) - ensure - begin - teardown - run_callbacks :teardown - rescue Exception => e - result = @runner.puke(self.class, method_name, e) - end - end - result - end - - protected - # overridden by each implementation. - def run_gc; end - - def run_warmup - run_gc - - time = Metrics::Time.new - run_test(time, :benchmark) - puts "%s (%s warmup)" % [full_test_name, time.format(time.total)] - - run_gc - end - - def run_profile(metric) - klass = full_profile_options[:benchmark] ? Benchmarker : Profiler - performer = klass.new(self, metric) - - performer.run - puts performer.report - performer.record - end - - class Performer - delegate :run_test, :full_profile_options, :full_test_name, :to => :@harness - - def initialize(harness, metric) - @harness, @metric, @supported = harness, metric, false - end - - def report - if @supported - rate = @total / full_profile_options[:runs] - '%20s: %s' % [@metric.name, @metric.format(rate)] - else - '%20s: unsupported' % @metric.name - end - end - - protected - def output_filename - "#{full_profile_options[:output]}/#{full_test_name}_#{@metric.name}" - end - end - - # overridden by each implementation. - class Profiler < Performer - def time_with_block - before = Time.now - yield - Time.now - before - end - - def run; end - def record; end - end - - class Benchmarker < Performer - def initialize(*args) - super - @supported = @metric.respond_to?('measure') - end - - def run - return unless @supported - - full_profile_options[:runs].to_i.times { run_test(@metric, :benchmark) } - @total = @metric.total - end - - def record - avg = @metric.total / full_profile_options[:runs].to_i - now = Time.now.utc.xmlschema - with_output_file do |file| - file.puts "#{avg},#{now},#{environment}" - end - end - - def environment - @env ||= [].tap do |env| - env << "#{$1}.#{$2}" if File.directory?('.git') && `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - env << rails_version if defined?(Rails::VERSION::STRING) - env << "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}" - env << RUBY_PLATFORM - end.join(',') - end - - protected - if defined?(Rails::VERSION::STRING) - HEADER = 'measurement,created_at,app,rails,ruby,platform' - else - HEADER = 'measurement,created_at,app,ruby,platform' - end - - def with_output_file - fname = output_filename - - if new = !File.exist?(fname) - FileUtils.mkdir_p(File.dirname(fname)) - end - - File.open(fname, 'ab') do |file| - file.puts(HEADER) if new - yield file - end - end - - def output_filename - "#{super}.csv" - end - - def rails_version - "rails-#{Rails::VERSION::STRING}#{rails_branch}" - end - - def rails_branch - if File.directory?('vendor/rails/.git') - Dir.chdir('vendor/rails') do - ".#{$1}.#{$2}" if `git branch -v` =~ /^\* (\S+)\s+(\S+)/ - end - end - end - end - - module Metrics - def self.[](name) - const_get(name.to_s.camelize) - rescue NameError - nil - end - - class Base - include ActiveSupport::NumberHelper - - attr_reader :total - - def initialize - @total = 0 - end - - def name - @name ||= self.class.name.demodulize.underscore - end - - def benchmark - with_gc_stats do - before = measure - yield - @total += (measure - before) - end - end - - # overridden by each implementation. - def profile; end - - protected - # overridden by each implementation. - def with_gc_stats; end - end - - class Time < Base - def measure - ::Time.now.to_f - end - - def format(measurement) - if measurement < 1 - '%d ms' % (measurement * 1000) - else - '%.2f sec' % measurement - end - end - end - - class Amount < Base - def format(measurement) - number_to_delimited(measurement.floor) - end - end - - class DigitalInformationUnit < Base - def format(measurement) - number_to_human_size(measurement, :precision => 2) - end - end - - # each implementation provides its own metrics like ProcessTime, Memory or GcRuns - end - end - end -end - -case RUBY_ENGINE - when 'ruby' then require 'active_support/testing/performance/ruby' - when 'rbx' then require 'active_support/testing/performance/rubinius' - when 'jruby' then require 'active_support/testing/performance/jruby' - else - $stderr.puts 'Your ruby interpreter is not supported for benchmarking.' - exit -end diff --git a/activesupport/lib/active_support/testing/performance/jruby.rb b/activesupport/lib/active_support/testing/performance/jruby.rb deleted file mode 100644 index 34e3f9f45f..0000000000 --- a/activesupport/lib/active_support/testing/performance/jruby.rb +++ /dev/null @@ -1,115 +0,0 @@ -require 'jruby/profiler' -require 'java' -java_import java.lang.management.ManagementFactory - -module ActiveSupport - module Testing - module Performance - DEFAULTS.merge!( - if ARGV.include?('--benchmark') - {:metrics => [:wall_time, :user_time, :memory, :gc_runs, :gc_time]} - else - { :metrics => [:wall_time], - :formats => [:flat, :graph] } - end).freeze - - protected - def run_gc - ManagementFactory.memory_mx_bean.gc - end - - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.is_a?(Metrics::WallTime) - end - - def run - return unless @supported - - @total = time_with_block do - @data = JRuby::Profiler.profile do - full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } - end - end - end - - def record - return unless @supported - - klasses = full_profile_options[:formats].map { |f| JRuby::Profiler.const_get("#{f.to_s.camelize}ProfilePrinter") }.compact - - klasses.each do |klass| - fname = output_filename(klass) - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - klass.new(@data).printProfile(file) - end - end - end - - protected - def output_filename(printer_class) - suffix = - case printer_class.name.demodulize - when 'FlatProfilePrinter'; 'flat.txt' - when 'GraphProfilePrinter'; 'graph.txt' - else printer_class.name.sub(/ProfilePrinter$/, '').underscore - end - - "#{super()}_#{suffix}" - end - end - - module Metrics - class Base - def profile - yield - end - - protected - def with_gc_stats - ManagementFactory.memory_mx_bean.gc - yield - end - end - - class WallTime < Time - def measure - super - end - end - - class CpuTime < Time - def measure - ManagementFactory.thread_mx_bean.get_current_thread_cpu_time / 1000 / 1000 / 1000.0 # seconds - end - end - - class UserTime < Time - def measure - ManagementFactory.thread_mx_bean.get_current_thread_user_time / 1000 / 1000 / 1000.0 # seconds - end - end - - class Memory < DigitalInformationUnit - def measure - ManagementFactory.memory_mx_bean.non_heap_memory_usage.used + ManagementFactory.memory_mx_bean.heap_memory_usage.used - end - end - - class GcRuns < Amount - def measure - ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_runs, current_gc| total_runs += current_gc.collection_count } - end - end - - class GcTime < Time - def measure - ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_time, current_gc| total_time += current_gc.collection_time } / 1000.0 # seconds - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/performance/rubinius.rb b/activesupport/lib/active_support/testing/performance/rubinius.rb deleted file mode 100644 index d9ebfbe352..0000000000 --- a/activesupport/lib/active_support/testing/performance/rubinius.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'rubinius/agent' - -module ActiveSupport - module Testing - module Performance - DEFAULTS.merge!( - if ARGV.include?('--benchmark') - {:metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time]} - else - { :metrics => [:wall_time], - :formats => [:flat, :graph] } - end).freeze - - protected - def run_gc - GC.run(true) - end - - class Performer; end - - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.is_a?(Metrics::WallTime) - end - - def run - return unless @supported - - @profiler = Rubinius::Profiler::Instrumenter.new - - @total = time_with_block do - @profiler.profile(false) do - full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } - end - end - end - - def record - return unless @supported - - if(full_profile_options[:formats].include?(:flat)) - create_path_and_open_file(:flat) do |file| - @profiler.show(file) - end - end - - if(full_profile_options[:formats].include?(:graph)) - create_path_and_open_file(:graph) do |file| - @profiler.show(file) - end - end - end - - protected - def create_path_and_open_file(printer_name) - fname = "#{output_filename}_#{printer_name}.txt" - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - yield(file) - end - end - end - - module Metrics - class Base - attr_reader :loopback - - def profile - yield - end - - protected - def with_gc_stats - @loopback = Rubinius::Agent.loopback - GC.run(true) - yield - end - end - - class WallTime < Time - def measure - super - end - end - - class Memory < DigitalInformationUnit - def measure - loopback.get("system.memory.counter.bytes").last - end - end - - class Objects < Amount - def measure - loopback.get("system.memory.counter.objects").last - end - end - - class GcRuns < Amount - def measure - loopback.get("system.gc.full.count").last + loopback.get("system.gc.young.count").last - end - end - - class GcTime < Time - def measure - (loopback.get("system.gc.full.wallclock").last + loopback.get("system.gc.young.wallclock").last) / 1000.0 - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/performance/ruby.rb b/activesupport/lib/active_support/testing/performance/ruby.rb deleted file mode 100644 index 7c149df1e4..0000000000 --- a/activesupport/lib/active_support/testing/performance/ruby.rb +++ /dev/null @@ -1,173 +0,0 @@ -begin - require 'ruby-prof' -rescue LoadError - $stderr.puts 'Specify ruby-prof as application\'s dependency in Gemfile to run benchmarks.' - raise -end - -module ActiveSupport - module Testing - module Performance - DEFAULTS.merge!( - if ARGV.include?('--benchmark') - { :metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time] } - else - { :min_percent => 0.01, - :metrics => [:process_time, :memory, :objects], - :formats => [:flat, :graph_html, :call_tree, :call_stack] } - end).freeze - - protected - remove_method :run_gc - def run_gc - GC.start - end - - class Profiler < Performer - def initialize(*args) - super - @supported = @metric.measure_mode rescue false - end - - remove_method :run - def run - return unless @supported - - RubyProf.measure_mode = @metric.measure_mode - RubyProf.start - RubyProf.pause - full_profile_options[:runs].to_i.times { run_test(@metric, :profile) } - @data = RubyProf.stop - @total = @data.threads.sum(0) { |thread| thread.methods.max.total_time } - end - - remove_method :record - def record - return unless @supported - - klasses = full_profile_options[:formats].map { |f| RubyProf.const_get("#{f.to_s.camelize}Printer") }.compact - - klasses.each do |klass| - fname = output_filename(klass) - FileUtils.mkdir_p(File.dirname(fname)) - File.open(fname, 'wb') do |file| - klass.new(@data).print(file, full_profile_options.slice(:min_percent)) - end - end - end - - protected - def output_filename(printer_class) - suffix = - case printer_class.name.demodulize - when 'FlatPrinter'; 'flat.txt' - when 'FlatPrinterWithLineNumbers'; 'flat_line_numbers.txt' - when 'GraphPrinter'; 'graph.txt' - when 'GraphHtmlPrinter'; 'graph.html' - when 'GraphYamlPrinter'; 'graph.yml' - when 'CallTreePrinter'; 'tree.txt' - when 'CallStackPrinter'; 'stack.html' - when 'DotPrinter'; 'graph.dot' - else printer_class.name.sub(/Printer$/, '').underscore - end - - "#{super()}_#{suffix}" - end - end - - module Metrics - class Base - def measure_mode - self.class::Mode - end - - remove_method :profile - def profile - RubyProf.resume - yield - ensure - RubyProf.pause - end - - protected - remove_method :with_gc_stats - def with_gc_stats - GC::Profiler.enable - GC.start - yield - ensure - GC::Profiler.disable - end - end - - class ProcessTime < Time - Mode = RubyProf::PROCESS_TIME if RubyProf.const_defined?(:PROCESS_TIME) - - def measure - RubyProf.measure_process_time - end - end - - class WallTime < Time - Mode = RubyProf::WALL_TIME if RubyProf.const_defined?(:WALL_TIME) - - def measure - RubyProf.measure_wall_time - end - end - - class CpuTime < Time - Mode = RubyProf::CPU_TIME if RubyProf.const_defined?(:CPU_TIME) - - def initialize(*args) - # FIXME: yeah my CPU is 2.33 GHz - RubyProf.cpu_frequency = 2.33e9 unless RubyProf.cpu_frequency > 0 - super - end - - def measure - RubyProf.measure_cpu_time - end - end - - class Memory < DigitalInformationUnit - Mode = RubyProf::MEMORY if RubyProf.const_defined?(:MEMORY) - - # Ruby 1.9 + GCdata patch - if GC.respond_to?(:malloc_allocated_size) - def measure - GC.malloc_allocated_size - end - end - end - - class Objects < Amount - Mode = RubyProf::ALLOCATIONS if RubyProf.const_defined?(:ALLOCATIONS) - - # Ruby 1.9 + GCdata patch - if GC.respond_to?(:malloc_allocations) - def measure - GC.malloc_allocations - end - end - end - - class GcRuns < Amount - Mode = RubyProf::GC_RUNS if RubyProf.const_defined?(:GC_RUNS) - - def measure - GC.count - end - end - - class GcTime < Time - Mode = RubyProf::GC_TIME if RubyProf.const_defined?(:GC_TIME) - - def measure - GC::Profiler.total_time - end - end - end - end - end -end diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index a65148cf1f..33f2b8dc9b 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -3,6 +3,19 @@ require 'active_support/callbacks' module ActiveSupport module Testing + # Adds support for +setup+ and +teardown+ callbacks. + # These callbacks serve as a replacement to overwriting the + # <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase. + # + # class ExampleTest < ActiveSupport::TestCase + # setup do + # # ... + # end + # + # teardown do + # # ... + # end + # end module SetupAndTeardown extend ActiveSupport::Concern @@ -12,21 +25,23 @@ module ActiveSupport end module ClassMethods + # Add a callback, which runs before <tt>TestCase#setup</tt>. def setup(*args, &block) set_callback(:setup, :before, *args, &block) end + # Add a callback, which runs after <tt>TestCase#teardown</tt>. def teardown(*args, &block) set_callback(:teardown, :after, *args, &block) end end - def before_setup + def before_setup # :nodoc: super run_callbacks :setup end - def after_teardown + def after_teardown # :nodoc: run_callbacks :teardown super end diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb index 9d43eb179f..843ce4a867 100644 --- a/activesupport/lib/active_support/testing/tagged_logging.rb +++ b/activesupport/lib/active_support/testing/tagged_logging.rb @@ -6,8 +6,8 @@ module ActiveSupport attr_writer :tagged_logger def before_setup - if tagged_logger - heading = "#{self.class}: #{__name__}" + if tagged_logger && tagged_logger.info? + heading = "#{self.class}: #{name}" divider = '-' * heading.size tagged_logger.info divider tagged_logger.info heading diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb new file mode 100644 index 0000000000..8c63815660 --- /dev/null +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -0,0 +1,131 @@ +module ActiveSupport + module Testing + class SimpleStubs # :nodoc: + Stub = Struct.new(:object, :method_name, :original_method) + + def initialize + @stubs = {} + end + + def stub_object(object, method_name, return_value) + key = [object.object_id, method_name] + + if stub = @stubs[key] + unstub_object(stub) + end + + new_name = "__simple_stub__#{method_name}" + + @stubs[key] = Stub.new(object, method_name, new_name) + + object.singleton_class.send :alias_method, new_name, method_name + object.define_singleton_method(method_name) { return_value } + end + + def unstub_all! + @stubs.each_value do |stub| + unstub_object(stub) + end + @stubs = {} + end + + private + + def unstub_object(stub) + singleton_class = stub.object.singleton_class + singleton_class.send :undef_method, stub.method_name + singleton_class.send :alias_method, stub.method_name, stub.original_method + singleton_class.send :undef_method, stub.original_method + end + end + + # Containing helpers that helps you test passage of time. + module TimeHelpers + # Changes current time to the time in the future or in the past by a given time difference by + # stubbing +Time.now+ and +Date.today+. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel 1.day + # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + # Date.current # => Sun, 10 Nov 2013 + # + # This method also accepts a block, which will return the current time back to its original + # state at the end of the block: + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel 1.day do + # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00 + # end + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel(duration, &block) + travel_to Time.now + duration, &block + end + + # Changes current time to the given time by stubbing +Time.now+ and + # +Date.today+ to return the time or date passed into this method. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # Date.current # => Wed, 24 Nov 2004 + # + # Dates are taken as their timestamp at the beginning of the day in the + # application time zone. <tt>Time.current</tt> returns said timestamp, + # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly, + # <tt>Date.current</tt> returns a date equal to the argument, and + # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may + # be different. (Note that you rarely want to deal with <tt>Time.now</tt>, + # or <tt>Date.today</tt>, in order to honor the application time zone + # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.) + # + # Note that the usec for the time passed will be set to 0 to prevent rounding + # errors with external services, like MySQL (which will round instead of floor, + # leading to off-by-one-second errors). + # + # This method also accepts a block, which will return the current time back to its original + # state at the end of the block: + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) do + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # end + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel_to(date_or_time) + if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime) + now = date_or_time.midnight.to_time + else + now = date_or_time.to_time.change(usec: 0) + end + + simple_stubs.stub_object(Time, :now, now) + simple_stubs.stub_object(Date, :today, now.to_date) + + if block_given? + begin + yield + ensure + travel_back + end + end + end + + # Returns the current time back to its original state, by removing the stubs added by + # `travel` and `travel_to`. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # travel_back + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel_back + simple_stubs.unstub_all! + end + + private + + def simple_stubs + @simple_stubs ||= SimpleStubs.new + end + end + end +end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 92a593965e..ea2d3391bd 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -1,5 +1,3 @@ -require 'active_support' - module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index fdaaacf2fe..0c6b4f445b 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -45,7 +45,7 @@ module ActiveSupport def initialize(utc_time, time_zone, local_time = nil, period = nil) @utc, @time_zone, @time = utc_time, time_zone, local_time - @period = @utc ? period : get_period_and_ensure_valid_local_time + @period = @utc ? period : get_period_and_ensure_valid_local_time(period) end # Returns a Time or DateTime instance that represents the time in +time_zone+. @@ -75,8 +75,8 @@ module ActiveSupport # Returns a <tt>Time.local()</tt> instance of the simultaneous time in your # system's <tt>ENV['TZ']</tt> zone. - def localtime - utc.respond_to?(:getlocal) ? utc.getlocal : utc.to_time.getlocal + def localtime(utc_offset = nil) + utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset) end alias_method :getlocal, :localtime @@ -109,23 +109,40 @@ module ActiveSupport alias_method :gmt_offset, :utc_offset alias_method :gmtoff, :utc_offset + # Returns a formatted string of the offset from UTC, or an alternative + # string if the time zone is already UTC. + # + # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)" + # Time.zone.now.formatted_offset(true) # => "-05:00" + # Time.zone.now.formatted_offset(false) # => "-0500" + # Time.zone = 'UTC' # => "UTC" + # Time.zone.now.formatted_offset(true, "0") # => "0" def formatted_offset(colon = true, alternate_utc_string = nil) utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon) end - # Time uses +zone+ to display the time zone abbreviation, so we're - # duck-typing it. + # Returns the time zone abbreviation. + # + # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)" + # Time.zone.now.zone # => "EST" def zone period.zone_identifier.to_s end + # Returns a string of the object's date, time, zone and offset from UTC. + # + # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00" def inspect "#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}" end + # Returns a string of the object's date and time in the ISO 8601 standard + # format. + # + # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00" def xmlschema(fraction_digits = 0) - fraction = if fraction_digits > 0 - (".%06i" % time.usec)[0, fraction_digits + 1] + fraction = if fraction_digits.to_i > 0 + (".%06i" % time.usec)[0, fraction_digits.to_i + 1] end "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}" @@ -138,15 +155,15 @@ module ActiveSupport # to +false+. # # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true - # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json - # # => "2005-02-01T15:15:10Z" + # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json + # # => "2005-02-01T05:15:10.000-10:00" # # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false - # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json - # # => "2005/02/01 15:15:10 +0000" + # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json + # # => "2005/02/01 05:15:10 -1000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format - xmlschema + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -177,8 +194,11 @@ module ActiveSupport end alias_method :rfc822, :rfc2822 - # <tt>:db</tt> format outputs time in UTC; all others output time in local. - # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly. + # Returns a string of the object's date and time. + # Accepts an optional <tt>format</tt>: + # * <tt>:default</tt> - default value, mimics Ruby 1.9 Time#to_s format. + # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db). + # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb. def to_s(format = :default) if format == :db utc.to_s(format) @@ -190,15 +210,11 @@ module ActiveSupport end alias_method :to_formatted_s, :to_s - # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and - # +formatted_offset+, respectively, before passing to Time#strftime, so - # that zone information is correct + # Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime, + # so that zone information is correct. def strftime(format) - format = format.gsub('%Z', zone) - .gsub('%z', formatted_offset(false)) - .gsub('%:z', formatted_offset(true)) - .gsub('%::z', formatted_offset(true) + ":00") - time.strftime(format) + format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}") + getlocal(utc_offset).strftime(format) end # Use the time in UTC for comparisons. @@ -206,18 +222,24 @@ module ActiveSupport utc <=> other end + # Returns true if the current object's time is within the specified + # +min+ and +max+ time. def between?(min, max) utc.between?(min, max) end + # Returns true if the current object's time is in the past. def past? utc.past? end + # Returns true if the current object's time falls within + # the current day. def today? time.today? end + # Returns true if the current object's time is in the future. def future? utc.future? end @@ -230,9 +252,23 @@ module ActiveSupport utc.hash end + # Adds an interval of time to the current object's time and return that + # value as a new TimeWithZone object. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EDT -04:00 + # now + 1000 # => Sun, 02 Nov 2014 01:43:08 EDT -04:00 + # + # If we're adding a Duration of variable length (i.e., years, months, days), + # move forward from #time, otherwise move forward from #utc, for accuracy + # when moving across DST boundaries. + # + # For instance, a time + 24.hours will advance exactly 24 hours, while a + # time + 1.day will advance 23-25 hours, depending on the day. + # + # now + 24.hours # => Mon, 03 Nov 2014 00:26:28 EST -05:00 + # now + 1.day # => Mon, 03 Nov 2014 01:26:28 EST -05:00 def +(other) - # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, - # otherwise move forward from #utc, for accuracy when moving across DST boundaries if duration_of_variable_length?(other) method_missing(:+, other) else @@ -240,12 +276,27 @@ module ActiveSupport result.in_time_zone(time_zone) end end + alias_method :since, :+ + # Returns a new TimeWithZone object that represents the difference between + # the current object's time and the +other+ time. + # + # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' + # now = Time.zone.now # => Sun, 02 Nov 2014 01:26:28 EST -05:00 + # now - 1000 # => Sun, 02 Nov 2014 01:09:48 EST -05:00 + # + # If subtracting a Duration of variable length (i.e., years, months, days), + # move backward from #time, otherwise move backward from #utc, for accuracy + # when moving across DST boundaries. + # + # For instance, a time - 24.hours will go subtract exactly 24 hours, while a + # time - 1.day will subtract 23-25 hours, depending on the day. + # + # now - 24.hours # => Sat, 01 Nov 2014 02:26:28 EDT -04:00 + # now - 1.day # => Sat, 01 Nov 2014 01:26:28 EDT -04:00 def -(other) - # If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time, - # otherwise move backwards #utc, for accuracy when moving across DST boundaries if other.acts_like?(:time) - utc.to_f - other.to_f + to_time - other.to_time elsif duration_of_variable_length?(other) method_missing(:-, other) else @@ -254,16 +305,6 @@ module ActiveSupport end end - def since(other) - # If we're adding a Duration of variable length (i.e., years, months, days), move forward from #time, - # otherwise move forward from #utc, for accuracy when moving across DST boundaries - if duration_of_variable_length?(other) - method_missing(:since, other) - else - utc.since(other).in_time_zone(time_zone) - end - end - def ago(other) since(-other) end @@ -278,7 +319,7 @@ module ActiveSupport end end - %w(year mon month day mday wday yday hour min sec to_date).each do |method_name| + %w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method_name} # def month time.#{method_name} # time.month @@ -286,26 +327,38 @@ module ActiveSupport EOV end - def usec - time.respond_to?(:usec) ? time.usec : 0 - end - def to_a [time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone] end + # Returns the object's date and time as a floating point number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_f # => 1417709320.285418 def to_f utc.to_f end + # Returns the object's date and time as an integer number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_i # => 1417709320 def to_i utc.to_i end alias_method :tv_sec, :to_i - # A TimeWithZone acts like a Time, so just return +self+. + # Returns the object's date and time as a rational number of seconds + # since the Epoch (January 1, 1970 00:00 UTC). + # + # Time.zone.now.to_r # => (708854548642709/500000) + def to_r + utc.to_r + end + + # Return an instance of Time in the system timezone. def to_time - utc + utc.to_time end def to_datetime @@ -336,6 +389,14 @@ module ActiveSupport initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc) end + # respond_to_missing? is not called in some cases, such as when type conversion is + # performed with Kernel#String + def respond_to?(sym, include_priv = false) + # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow + return false if sym.to_sym == :to_str + super + end + # Ensure proxy class responds to all methods that underlying time instance # responds to. def respond_to_missing?(sym, include_priv) @@ -348,15 +409,17 @@ module ActiveSupport # TimeWithZone with the existing +time_zone+. def method_missing(sym, *args, &block) wrap_with_time_zone time.__send__(sym, *args, &block) + rescue NoMethodError => e + raise e, e.message.sub(time.inspect, self.inspect), e.backtrace end private - def get_period_and_ensure_valid_local_time + def get_period_and_ensure_valid_local_time(period) # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc? begin - @time_zone.period_for_local(@time) + period || @time_zone.period_for_local(@time) rescue ::TZInfo::PeriodNotFound # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again @time += 1.hour @@ -374,7 +437,8 @@ module ActiveSupport def wrap_with_time_zone(time) if time.acts_like?(:time) - self.class.new(nil, time_zone, time) + periods = time_zone.periods_for_local(time) + self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil) elsif time.is_a?(Range) wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end) else diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index c5fbddcb5f..fd05a5459c 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,3 +1,5 @@ +require 'tzinfo' +require 'thread_safe' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' @@ -5,7 +7,7 @@ module ActiveSupport # The TimeZone class serves as a wrapper around TZInfo::Timezone instances. # It allows us to do the following: # - # * Limit the set of zones provided by TZInfo to a meaningful subset of 142 + # * Limit the set of zones provided by TZInfo to a meaningful subset of 146 # zones. # * Retrieve and display zones with a friendlier name # (e.g., "Eastern Time (US & Canada)" instead of "America/New_York"). @@ -62,6 +64,7 @@ module ActiveSupport "Newfoundland" => "America/St_Johns", "Brasilia" => "America/Sao_Paulo", "Buenos Aires" => "America/Argentina/Buenos_Aires", + "Montevideo" => "America/Montevideo", "Georgetown" => "America/Guyana", "Greenland" => "America/Godthab", "Mid-Atlantic" => "Atlantic/South_Georgia", @@ -150,7 +153,7 @@ module ActiveSupport "Taipei" => "Asia/Taipei", "Perth" => "Australia/Perth", "Irkutsk" => "Asia/Irkutsk", - "Ulaan Bataar" => "Asia/Ulaanbaatar", + "Ulaanbaatar" => "Asia/Ulaanbaatar", "Seoul" => "Asia/Seoul", "Osaka" => "Asia/Tokyo", "Sapporo" => "Asia/Tokyo", @@ -176,22 +179,81 @@ module ActiveSupport "Wellington" => "Pacific/Auckland", "Nuku'alofa" => "Pacific/Tongatapu", "Tokelau Is." => "Pacific/Fakaofo", + "Chatham Is." => "Pacific/Chatham", "Samoa" => "Pacific/Apia" } UTC_OFFSET_WITH_COLON = '%s%02d:%02d' - UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '') + UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '') - # Assumes self represents an offset from UTC in seconds (as returned from - # Time#utc_offset) and turns this into an +HH:MM formatted string. - # - # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" - def self.seconds_to_utc_offset(seconds, colon = true) - format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON - sign = (seconds < 0 ? '-' : '+') - hours = seconds.abs / 3600 - minutes = (seconds.abs % 3600) / 60 - format % [sign, hours, minutes] + @lazy_zones_map = ThreadSafe::Cache.new + + class << self + # Assumes self represents an offset from UTC in seconds (as returned from + # Time#utc_offset) and turns this into an +HH:MM formatted string. + # + # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00" + def seconds_to_utc_offset(seconds, colon = true) + format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON + sign = (seconds < 0 ? '-' : '+') + hours = seconds.abs / 3600 + minutes = (seconds.abs % 3600) / 60 + format % [sign, hours, minutes] + end + + def find_tzinfo(name) + TZInfo::TimezoneProxy.new(MAPPING[name] || name) + end + + alias_method :create, :new + + # Returns a TimeZone instance with the given name, or +nil+ if no + # such TimeZone instance exists. (This exists to support the use of + # this class with the +composed_of+ macro.) + def new(name) + self[name] + end + + # Returns an array of all TimeZone objects. There are multiple + # TimeZone objects per time zone, in many cases, to make it easier + # for users to find their own time zone. + def all + @zones ||= zones_map.values.sort + end + + def zones_map + @zones_map ||= begin + MAPPING.each_key {|place| self[place]} # load all the zones + @lazy_zones_map + end + end + + # Locate a specific time zone object. If the argument is a string, it + # is interpreted to mean the name of the timezone to locate. If it is a + # numeric value it is either the hour offset, or the second offset, of the + # timezone to find. (The first one with that offset will be returned.) + # Returns +nil+ if no such time zone is known to the system. + def [](arg) + case arg + when String + begin + @lazy_zones_map[arg] ||= create(arg).tap(&:utc_offset) + rescue TZInfo::InvalidTimezoneIdentifier + nil + end + when Numeric, ActiveSupport::Duration + arg *= 3600 if arg.abs <= 13 + all.find { |z| z.utc_offset == arg.to_i } + else + raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" + end + end + + # A convenience method for returning a collection of TimeZone objects + # for time zones in the USA. + def us_zones + @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } + end end include Comparable @@ -203,8 +265,6 @@ module ActiveSupport # (GMT). Seconds were chosen as the offset unit because that is the unit # that Ruby uses to represent time zone offsets (see Time#utc_offset). def initialize(name, utc_offset = nil, tzinfo = nil) - self.class.send(:require_tzinfo) - @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) @@ -216,8 +276,8 @@ module ActiveSupport if @utc_offset @utc_offset else - @current_period ||= tzinfo.try(:current_period) - @current_period.try(:utc_offset) + @current_period ||= tzinfo.current_period if tzinfo + @current_period.utc_offset if @current_period end end @@ -230,6 +290,7 @@ module ActiveSupport # Compare this time zone to the parameter. The two are compared first on # their offsets, and then by name. def <=>(zone) + return unless zone.respond_to? :utc_offset result = (utc_offset <=> zone.utc_offset) result = (name <=> zone.name) if result == 0 result @@ -238,7 +299,7 @@ module ActiveSupport # Compare #name and TZInfo identifier to a supplied regexp, returning +true+ # if a match is found. def =~(re) - return true if name =~ re || MAPPING[name] =~ re + re === name || re === MAPPING[name] end # Returns a textual representation of this time zone. @@ -277,14 +338,19 @@ module ActiveSupport # # Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00 # Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00 - def parse(str, now=now) + # + # However, if the date component is not provided, but any other upper + # components are supplied, then the day of the month defaults to 1: + # + # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00 + def parse(str, now=now()) parts = Date._parse(str, false) return if parts.empty? time = Time.new( parts.fetch(:year, now.year), parts.fetch(:mon, now.month), - parts.fetch(:mday, now.day), + parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day), parts.fetch(:hour, 0), parts.fetch(:min, 0), parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0), @@ -312,6 +378,16 @@ module ActiveSupport tzinfo.now.to_date end + # Returns the next date in this time zone. + def tomorrow + today + 1 + end + + # Returns the previous date in this time zone. + def yesterday + today - 1 + end + # Adjust the given time to the simultaneous time in the time zone # represented by +self+. Returns a Time.utc() instance -- if you want an # ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead. @@ -337,91 +413,13 @@ module ActiveSupport tzinfo.period_for_local(time, dst) end - def self.find_tzinfo(name) - TZInfo::TimezoneProxy.new(MAPPING[name] || name) - end - - class << self - alias_method :create, :new - - # Return a TimeZone instance with the given name, or +nil+ if no - # such TimeZone instance exists. (This exists to support the use of - # this class with the +composed_of+ macro.) - def new(name) - self[name] - end - - # Return an array of all TimeZone objects. There are multiple - # TimeZone objects per time zone, in many cases, to make it easier - # for users to find their own time zone. - def all - @zones ||= zones_map.values.sort - end - - def zones_map - @zones_map ||= begin - new_zones_names = MAPPING.keys - lazy_zones_map.keys - new_zones = Hash[new_zones_names.map { |place| [place, create(place)] }] - - lazy_zones_map.merge(new_zones) - end - end - - # Locate a specific time zone object. If the argument is a string, it - # is interpreted to mean the name of the timezone to locate. If it is a - # numeric value it is either the hour offset, or the second offset, of the - # timezone to find. (The first one with that offset will be returned.) - # Returns +nil+ if no such time zone is known to the system. - def [](arg) - case arg - when String - begin - lazy_zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset } - rescue TZInfo::InvalidTimezoneIdentifier - nil - end - when Numeric, ActiveSupport::Duration - arg *= 3600 if arg.abs <= 13 - all.find { |z| z.utc_offset == arg.to_i } - else - raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}" - end - end - - # A convenience method for returning a collection of TimeZone objects - # for time zones in the USA. - def us_zones - @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } - end - - protected - - def require_tzinfo - require 'tzinfo' unless defined?(::TZInfo) - rescue LoadError - $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install" - raise - end - - private - - def lookup(name) - (tzinfo = find_tzinfo(name)) && create(tzinfo.name.freeze) - end - - def lazy_zones_map - require_tzinfo - - @lazy_zones_map ||= Hash.new do |hash, place| - hash[place] = create(place) if MAPPING.has_key?(place) - end - end + def periods_for_local(time) #:nodoc: + tzinfo.periods_for_local(time) end private - - def time_now - Time.now - end + def time_now + Time.now + end end end diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat Binary files differindex df17a8cccf..760be4c07a 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/version.rb b/activesupport/lib/active_support/version.rb index 8a8f8f946d..fe03984546 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,10 +1,8 @@ -module ActiveSupport - module VERSION #:nodoc: - MAJOR = 4 - MINOR = 0 - TINY = 0 - PRE = "beta" +require_relative 'gem_version' - STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') +module ActiveSupport + # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt> + def self.version + gem_version end end diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index d082a0a499..009ee4db90 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -1,7 +1,9 @@ require 'time' require 'base64' +require 'bigdecimal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/date_time/calculations' module ActiveSupport # = XmlMini @@ -56,13 +58,13 @@ module ActiveSupport # TODO use regexp instead of Date.parse unless defined?(PARSING) PARSING = { - "symbol" => Proc.new { |symbol| symbol.to_sym }, + "symbol" => Proc.new { |symbol| symbol.to_s.to_sym }, "date" => Proc.new { |date| ::Date.parse(date) }, "datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc }, "integer" => Proc.new { |integer| integer.to_i }, "float" => Proc.new { |float| float.to_f }, "decimal" => Proc.new { |number| BigDecimal(number) }, - "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) }, + "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) }, "string" => Proc.new { |string| string.to_s }, "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, "base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) }, diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb index 4551dd2f2d..f303daa1a7 100644 --- a/activesupport/lib/active_support/xml_mini/jdom.rb +++ b/activesupport/lib/active_support/xml_mini/jdom.rb @@ -37,6 +37,12 @@ module ActiveSupport {} else @dbf = DocumentBuilderFactory.new_instance + # secure processing of java xml + # http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html + @dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false) + @dbf.setFeature("http://xml.org/sax/features/external-general-entities", false) + @dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false) + @dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true) xml_string_reader = StringReader.new(data) xml_input_source = InputSource.new(xml_string_reader) doc = @dbf.new_document_builder.parse(xml_input_source) @@ -135,7 +141,7 @@ module ActiveSupport (0...attributes.length).each do |i| attribute_hash[CONTENT_KEY] ||= '' attribute_hash[attributes.item(i).name] = attributes.item(i).value - end + end attribute_hash end diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb index acc018fd2d..70a95299ec 100644 --- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb +++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb @@ -32,7 +32,7 @@ module ActiveSupport end def on_start_element(name, attrs = {}) - new_hash = { CONTENT_KEY => '' }.merge(attrs) + new_hash = { CONTENT_KEY => '' }.merge!(attrs) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb index 30b94aac47..be2d6a4cb1 100644 --- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb +++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb @@ -38,7 +38,7 @@ module ActiveSupport end def start_element(name, attrs = []) - new_hash = { CONTENT_KEY => '' }.merge(Hash[attrs]) + new_hash = { CONTENT_KEY => '' }.merge!(Hash[attrs]) new_hash[HASH_SIZE_KEY] = new_hash.size + 1 case current_hash[name] |