diff options
Diffstat (limited to 'activesupport/lib/active_support')
124 files changed, 2837 insertions, 1752 deletions
diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb index 151008bbaa..f537818300 100644 --- a/activesupport/lib/active_support/all.rb +++ b/activesupport/lib/active_support/all.rb @@ -1,4 +1,3 @@ require 'active_support' -require 'active_support/deprecation' require 'active_support/time' require 'active_support/core_ext' diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb index c88ae3e661..d06f22ad5c 100644 --- a/activesupport/lib/active_support/backtrace_cleaner.rb +++ b/activesupport/lib/active_support/backtrace_cleaner.rb @@ -13,7 +13,7 @@ module ActiveSupport # can focus on the rest. # # bc = BacktraceCleaner.new - # bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix + # bc.add_filter { |line| line.gsub(Rails.root.to_s, '') } # strip the Rails.root prefix # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems # bc.clean(exception.backtrace) # perform the cleanup # @@ -22,7 +22,7 @@ module ActiveSupport # <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the # backtrace to a pristine state. If you need to reconfigure an existing # BacktraceCleaner so that it does not filter or modify the paths of any lines - # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt> + # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!</tt> # These two methods will give you a completely untouched backtrace. # # Inspired by the Quiet Backtrace gem by Thoughtbot. @@ -65,14 +65,14 @@ module ActiveSupport @silencers << block end - # Will remove all silencers, but leave in the filters. This is useful if - # your context of debugging suddenly expands as you suspect a bug in one of + # Removes all silencers, but leaves in the filters. Useful if your + # context of debugging suddenly expands as you suspect a bug in one of # the libraries you use. def remove_silencers! @silencers = [] end - # Removes all filters, but leaves in silencers. Useful if you suddenly + # Removes all filters, but leaves in the silencers. Useful if you suddenly # need to see entire filepaths in the backtrace that you had already # filtered out. def remove_filters! diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 308a1b2cd9..ff67a6828c 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -3,19 +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/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. @@ -88,25 +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 + # Obtains the specified cache store class, given the name of the +store+. + # Raises an error when the store class cannot be found. + def retrieve_store_class(store) + require "active_support/cache/#{store}" + rescue LoadError => e + raise "Could not find cache store adapter for #{store} (#{e})" + else + ActiveSupport::Cache.const_get(store.to_s.camelize) + end end # An abstract cache store class. There are multiple cache store @@ -153,7 +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 @@ -180,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 @@ -234,9 +235,9 @@ module ActiveSupport # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>. # Yes, this process is extending the time for a stale value by another few # seconds. Because of extended life of the previous cache, other processes - # will continue to use slightly stale data for a just a big longer. In the + # will continue to use slightly stale data for a just a bit longer. In the # meantime that first process will go ahead and will write into cache the - # new value. After that all the processes will start getting new value. + # new value. After that all the processes will start getting the new value. # The key is to keep <tt>:race_condition_ttl</tt> small. # # If the process regenerating the entry errors out, the entry will be @@ -359,20 +360,19 @@ module ActiveSupport # # Options are passed to the underlying cache implementation. # - # Returns an array with the data for each of the names. For example: + # Returns a hash with the data for each of the names. For example: # # cache.write("bim", "bam") - # cache.fetch_multi("bim", "boom") {|key| key * 2 } - # #=> ["bam", "boomboom"] + # cache.fetch_multi("bim", "boom") { |key| key * 2 } + # # => { "bam" => "bam", "boom" => "boomboom" } # def fetch_multi(*names) options = names.extract_options! options = merged_options(options) - results = read_multi(*names, options) - names.map do |name| - results.fetch(name) do + names.each_with_object({}) do |name, memo| + memo[name] = results.fetch(name) do value = yield name write(name, value, options) value @@ -385,6 +385,7 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def write(name, value, options = nil) options = merged_options(options) + instrument(:write, name, options) do entry = Entry.new(value, options) write_entry(namespaced_key(name, options), entry, options) @@ -396,16 +397,18 @@ module ActiveSupport # Options are passed to the underlying cache implementation. def delete(name, options = nil) options = merged_options(options) + instrument(:delete, name) do delete_entry(namespaced_key(name, options), options) end end - # Return +true+ if the cache contains an entry for the given key. + # Returns +true+ if the cache contains an entry for the given key. # # Options are passed to the underlying cache implementation. def exist?(name, options = nil) options = merged_options(options) + instrument(:exist?, name) do entry = read_entry(namespaced_key(name, options), options) (entry && !entry.expired?) || false @@ -451,7 +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) @@ -539,13 +542,9 @@ module ActiveSupport def instrument(operation, key, options = nil) log(operation, key, options) - if self.class.instrument - payload = { :key => key } - payload.merge!(options) if options.is_a?(Hash) - ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } - else - yield(nil) - end + payload = { :key => key } + payload.merge!(options) if options.is_a?(Hash) + ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) } end def log(operation, key, options = nil) @@ -585,6 +584,7 @@ module ActiveSupport result = instrument(:generate, name, options) do |payload| yield(name) end + write(name, result, options) result end @@ -608,6 +608,7 @@ module ActiveSupport else @value = value end + @created_at = Time.now.to_f @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @@ -658,6 +659,7 @@ module ActiveSupport # serialize entries to protect against accidental cache modifications. def dup_value! convert_version_4beta1_entry! if defined?(@v) + if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false) if @value.is_a?(String) @value = @value.dup @@ -672,8 +674,10 @@ module ActiveSupport if value && options[:compress] compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize + return true if serialized_value_size >= compress_threshold end + false end @@ -696,10 +700,12 @@ module ActiveSupport @value = @v remove_instance_variable(:@v) end + if defined?(@c) @compressed = @c remove_instance_variable(:@c) end + if defined?(@x) && @x @created_at ||= Time.now.to_f @expires_in = @x - @created_at diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 472f23c1c5..d08ecd2f7d 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -14,6 +14,7 @@ module ActiveSupport DIR_FORMATTER = "%03X" FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) + FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room EXCLUDED_DIRS = ['.', '..'].freeze def initialize(cache_path, options = nil) @@ -22,18 +23,19 @@ module ActiveSupport extend Strategy::LocalCache end - # Deletes all items from the cache. In this case it deletes all the entries in the specified - # file store directory except for .gitkeep. Be careful which directory is specified in your + # Deletes all items from the cache. In this case it deletes all the entries in the specified + # file store directory except for .gitkeep. Be careful which directory is specified in your # config file when using +FileStore+ because everything in that directory will be deleted. def clear(options = nil) root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)} FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)}) end - # Premptively iterates through all stored keys and removes the ones which have expired. + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) - each_key(options) do |key| + search_dir(cache_path) do |fname| + key = file_path_key(fname) entry = read_entry(key, options) delete_entry(key, options) if entry && entry.expired? end @@ -42,33 +44,13 @@ module ActiveSupport # Increments an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def increment(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i + amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, amount, options) end # Decrements an already existing integer value that is stored in the cache. # If the key is not found nothing is done. def decrement(name, amount = 1, options = nil) - file_name = key_file_path(namespaced_key(name, options)) - lock_file(file_name) do - options = merged_options(options) - if num = read(name, options) - num = num.to_i - amount - write(name, num, options) - num - else - nil - end - end + modify_value(name, -amount, options) end def delete_matched(matcher, options = nil) @@ -96,6 +78,7 @@ module ActiveSupport def write_entry(key, entry, options) file_name = key_file_path(key) + return false if options[:unless_exist] && File.exist?(file_name) ensure_cache_path(File.dirname(file_name)) File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)} true @@ -135,6 +118,10 @@ module ActiveSupport # Translate a key into a file path. def key_file_path(key) + if key.size > FILEPATH_MAX_SIZE + key = Digest::MD5.hexdigest(key) + end + fname = URI.encode_www_form_component(key) hash = Zlib.adler32(fname) hash, dir_1 = hash.divmod(0x1000) @@ -182,6 +169,22 @@ module ActiveSupport end end end + + # Modifies the amount of an already existing integer value that is stored in the cache. + # If the key is not found nothing is done. + def modify_value(name, amount, options) + file_name = key_file_path(namespaced_key(name, options)) + + lock_file(file_name) do + options = merged_options(options) + + if num = read(name, options) + num = num.to_i + amount + write(name, num, options) + num + end + end + end end end end diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb index 512296554f..61b4f0b8b0 100644 --- a/activesupport/lib/active_support/cache/mem_cache_store.rb +++ b/activesupport/lib/active_support/cache/mem_cache_store.rb @@ -41,17 +41,15 @@ module ActiveSupport # # If no addresses are specified, then MemCacheStore will connect to # localhost port 11211 (the default memcached port). - # - # Instead of addresses one can pass in a MemCache-like object. For example: - # - # require 'memcached' # gem install memcached; uses C bindings to libmemcached - # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211")) def initialize(*addresses) addresses = addresses.flatten options = addresses.extract_options! super(options) - if addresses.first.respond_to?(:get) + unless [String, Dalli::Client, NilClass].include?(addresses.first.class) + raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance." + end + if addresses.first.is_a?(Dalli::Client) @data = addresses.first else mem_cache_options = options.dup @@ -87,7 +85,7 @@ module ActiveSupport instrument(:increment, name, :amount => amount) do @data.incr(escape_key(namespaced_key(name, options)), amount) end - rescue Dalli::DalliError + rescue Dalli::DalliError => e logger.error("DalliError (#{e}): #{e.message}") if logger nil end @@ -101,7 +99,7 @@ module ActiveSupport instrument(:decrement, name, :amount => amount) do @data.decr(escape_key(namespaced_key(name, options)), amount) end - rescue Dalli::DalliError + rescue Dalli::DalliError => e logger.error("DalliError (#{e}): #{e.message}") if logger nil end diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index b979521c99..8a0523d0e2 100644 --- a/activesupport/lib/active_support/cache/memory_store.rb +++ b/activesupport/lib/active_support/cache/memory_store.rb @@ -36,7 +36,7 @@ module ActiveSupport end end - # Premptively iterates through all stored keys and removes the ones which have expired. + # Preemptively iterates through all stored keys and removes the ones which have expired. def cleanup(options = nil) options = merged_options(options) instrument(:cleanup, :size => @data.size) do @@ -124,7 +124,6 @@ module ActiveSupport protected - # See https://gist.github.com/ssimeonov/6047200 PER_ENTRY_OVERHEAD = 240 def cached_size(key, entry) diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index fb42c4a41e..73c6b3cb88 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/string/inflections' +require 'active_support/per_thread_registry' module ActiveSupport module Cache @@ -8,6 +9,8 @@ module ActiveSupport # duration of a block. Repeated calls to the cache for the same key will hit the # in-memory cache for faster access. module LocalCache + autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware' + # Class for storing and registering the local caches. class LocalCacheRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry @@ -23,6 +26,9 @@ module ActiveSupport def set_cache_for(local_cache_key, value) @registry[local_cache_key] = value end + + def self.set_cache_for(l, v); instance.set_cache_for l, v; end + def self.cache_for(l); instance.cache_for l; end end # Simple memory backed cache. This cache is not thread safe and is intended only @@ -60,32 +66,6 @@ module ActiveSupport def with_local_cache use_temporary_local_cache(LocalStore.new) { yield } end - - #-- - # This class wraps up local storage for middlewares. Only the middleware method should - # construct them. - class Middleware # :nodoc: - attr_reader :name, :local_cache_key - - def initialize(name, local_cache_key) - @name = name - @local_cache_key = local_cache_key - @app = nil - end - - def new(app) - @app = app - self - end - - def call(env) - LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) - @app.call(env) - ensure - LocalCacheRegistry.set_cache_for(local_cache_key, nil) - end - end - # Middleware class can be inserted as a Rack handler to be local cache for the # duration of request. def middleware @@ -106,13 +86,13 @@ module ActiveSupport def increment(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - increment_or_decrement(value, name, amount, options) + set_cache_value(value, name, amount, options) value end def decrement(name, amount = 1, options = nil) # :nodoc: value = bypass_local_cache{super} - increment_or_decrement(value, name, amount, options) + set_cache_value(value, name, amount, options) value end @@ -140,8 +120,7 @@ module ActiveSupport super end - private - def increment_or_decrement(value, name, amount, options) + def set_cache_value(value, name, amount, options) if local_cache local_cache.mute do if value @@ -153,6 +132,8 @@ module ActiveSupport end end + private + def local_cache_key @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym end diff --git a/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb new file mode 100644 index 0000000000..a6f24b1a3c --- /dev/null +++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb @@ -0,0 +1,44 @@ +require 'rack/body_proxy' +require 'rack/utils' + +module ActiveSupport + module Cache + module Strategy + module LocalCache + + #-- + # This class wraps up local storage for middlewares. Only the middleware method should + # construct them. + class Middleware # :nodoc: + attr_reader :name, :local_cache_key + + def initialize(name, local_cache_key) + @name = name + @local_cache_key = local_cache_key + @app = nil + end + + def new(app) + @app = app + self + end + + def call(env) + LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new) + response = @app.call(env) + response[2] = ::Rack::BodyProxy.new(response[2]) do + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + end + response + rescue Rack::Utils::InvalidParameterError + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + [400, {}, []] + rescue Exception + LocalCacheRegistry.set_cache_for(local_cache_key, nil) + raise + end + end + end + end + end +end diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 5c738572a8..cd467e13f6 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -7,14 +7,14 @@ 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+). @@ -71,7 +71,8 @@ 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 @@ -89,7 +90,7 @@ module ActiveSupport private - # A hook invoked everytime a before callback is halted. + # A hook invoked every time a before callback is halted. # This can be overridden in AS::Callback implementors in order # to provide better debugging/logging. def halted_callback_hook(filter) @@ -131,8 +132,6 @@ module ActiveSupport end end - private - def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter) lambda { |env| target = env.target @@ -149,6 +148,7 @@ module ActiveSupport next_callback.call env } end + private_class_method :halting_and_conditional def self.halting(next_callback, user_callback, halted_lambda, filter) lambda { |env| @@ -166,6 +166,7 @@ module ActiveSupport next_callback.call env } end + private_class_method :halting def self.conditional(next_callback, user_callback, user_conditions) lambda { |env| @@ -178,6 +179,7 @@ module ActiveSupport next_callback.call env } end + private_class_method :conditional def self.simple(next_callback, user_callback) lambda { |env| @@ -185,6 +187,7 @@ module ActiveSupport next_callback.call env } end + private_class_method :simple end class After @@ -208,8 +211,6 @@ module ActiveSupport end end - private - def self.halting_and_conditional(next_callback, user_callback, user_conditions) lambda { |env| env = next_callback.call env @@ -223,6 +224,7 @@ module ActiveSupport env } end + private_class_method :halting_and_conditional def self.halting(next_callback, user_callback) lambda { |env| @@ -233,6 +235,7 @@ module ActiveSupport env } end + private_class_method :halting def self.conditional(next_callback, user_callback, user_conditions) lambda { |env| @@ -246,6 +249,7 @@ module ActiveSupport env } end + private_class_method :conditional def self.simple(next_callback, user_callback) lambda { |env| @@ -254,6 +258,7 @@ module ActiveSupport env } end + private_class_method :simple end class Around @@ -269,8 +274,6 @@ module ActiveSupport end end - private - def self.halting_and_conditional(next_callback, user_callback, user_conditions) lambda { |env| target = env.target @@ -288,23 +291,25 @@ module ActiveSupport end } end + private_class_method :halting_and_conditional def self.halting(next_callback, user_callback) lambda { |env| target = env.target value = env.value - unless env.halted + if env.halted + next_callback.call env + else user_callback.call(target, value) { env = next_callback.call env env.value } env - else - next_callback.call env end } end + private_class_method :halting def self.conditional(next_callback, user_callback, user_conditions) lambda { |env| @@ -322,6 +327,7 @@ module ActiveSupport end } end + private_class_method :conditional def self.simple(next_callback, user_callback) lambda { |env| @@ -332,6 +338,7 @@ module ActiveSupport env } end + private_class_method :simple end end @@ -409,15 +416,8 @@ module ActiveSupport # Procs:: A proc to call with the object. # Objects:: An object with a <tt>before_foo</tt> method on it to call. # - # All of these objects are compiled into methods and handled - # the same after this point: - # - # Symbols:: Already methods. - # Strings:: class_eval'd into methods. - # Procs:: using define_method compiled into methods. - # Objects:: - # a method is created that calls the before_foo method - # on the object. + # All of these objects are converted into a lambda and handled + # the same after this point. def make_lambda(filter) case filter when Symbol @@ -566,7 +566,7 @@ module ActiveSupport # # set_callback :save, :before, :before_meth # set_callback :save, :after, :after_meth, if: :condition - # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff } + # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff } # # The second arguments indicates whether the callback is to be run +:before+, # +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This @@ -574,10 +574,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 @@ -648,7 +648,7 @@ module ActiveSupport self.set_callbacks name, callbacks.dup.clear end - # Define sets of events in the object lifecycle that support callbacks. + # Define sets of events in the object life cycle that support callbacks. # # define_callbacks :validate # define_callbacks :initialize, :save, :destroy @@ -718,12 +718,6 @@ module ActiveSupport # would call <tt>Audit#save</tt>. def define_callbacks(*names) options = names.extract_options! - if options.key?(:terminator) && String === options[:terminator] - ActiveSupport::Deprecation.warn "String based terminators are deprecated, please use a lambda" - value = options[:terminator] - line = class_eval "lambda { |result| #{value} }", __FILE__, __LINE__ - options[:terminator] = lambda { |target, result| target.instance_exec(result, &line) } - end names.each do |name| class_attribute "_#{name}_callbacks" diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb index b796d01dfd..9d5cee54e3 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 @@ -130,5 +130,13 @@ module ActiveSupport super end end + + def class_methods(&class_methods_module_definition) + mod = const_defined?(:ClassMethods) ? + const_get(:ClassMethods) : + const_set(:ClassMethods, Module.new) + + mod.module_eval(&class_methods_module_definition) + end end end diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb index e0d39d509f..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/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb index 4f1e432b61..caa499dfa2 100644 --- a/activesupport/lib/active_support/core_ext/array/access.rb +++ b/activesupport/lib/active_support/core_ext/array/access.rb @@ -5,6 +5,8 @@ class Array # %w( a b c d ).from(2) # => ["c", "d"] # %w( a b c d ).from(10) # => [] # %w().from(0) # => [] + # %w( a b c d ).from(-2) # => ["c", "d"] + # %w( a b c ).from(-10) # => [] def from(position) self[position, length] || [] end @@ -15,8 +17,10 @@ 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 + self[0..position] end # Equal to <tt>self[1]</tt>. @@ -48,6 +52,8 @@ class Array end # Equal to <tt>self[41]</tt>. Also known as accessing "the reddit". + # + # (1..42).to_a.forty_two # => 42 def forty_two self[41] end diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb index 37f007c751..87ae052eb0 100644 --- a/activesupport/lib/active_support/core_ext/array/grouping.rb +++ b/activesupport/lib/active_support/core_ext/array/grouping.rb @@ -18,6 +18,11 @@ class Array # ["3", "4"] # ["5"] def in_groups_of(number, fill_with = nil) + if number.to_i <= 0 + raise ArgumentError, + "Group size must be a positive integer, was #{number.inspect}" + end + if fill_with == false collection = self else @@ -53,7 +58,7 @@ class Array # ["4", "5"] # ["6", "7"] def in_groups(number, fill_with = nil) - # size / number gives minor group size; + # size.div number gives minor group size; # size % number gives how many objects need extra accommodation; # each group hold either division or division + 1 items. division = size.div number @@ -83,10 +88,10 @@ class Array # # [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]] # (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]] - def split(value = nil, &block) - if block + def split(value = nil) + if block_given? inject([[]]) do |results, element| - if block.call(element) + if yield(element) results << [] else results.last << element @@ -95,9 +100,9 @@ class Array results end else - results, arr = [[]], self + results, arr = [[]], self.dup until arr.empty? - if (idx = index(value)) + if (idx = arr.index(value)) results.last.concat(arr.shift(idx)) arr.shift results << [] diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb index 27718f19d4..f8d48b69df 100644 --- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb +++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb @@ -1,7 +1,7 @@ class Array - # The human way of thinking about adding stuff to the end of a list is with append + # The human way of thinking about adding stuff to the end of a list is with append. alias_method :append, :<< - # The human way of thinking about adding stuff to the beginning of a list is with prepend + # The human way of thinking about adding stuff to the beginning of a list is with prepend. alias_method :prepend, :unshift end
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb index 39b8cea807..843c592669 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -1,22 +1,7 @@ require 'bigdecimal' require 'bigdecimal/util' -require 'yaml' class BigDecimal - YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' } - - def encode_with(coder) - string = to_s - coder.represent_scalar(nil, YAML_MAPPING[string] || string) - end - - # Backport this method if it doesn't exist - unless method_defined?(:to_d) - def to_d - self - end - end - DEFAULT_STRING_FORMAT = 'F' def to_formatted_s(*args) if args[0].is_a?(Symbol) 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 83038f9da5..f2a221c396 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -118,7 +118,10 @@ class Class 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 34859617c9..84d5e95e7a 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -1,170 +1,4 @@ -require 'active_support/core_ext/array/extract_options' - -# Extends the class object with class and instance accessors for class attributes, -# just like the native attr* accessors for instance attributes. -class Class - # Defines a class attribute if it's not defined and creates a reader method that - # returns the attribute value. - # - # class Person - # cattr_reader :hair_colors - # end - # - # Person.class_variable_set("@@hair_colors", [:brown, :black]) - # Person.hair_colors # => [:brown, :black] - # Person.new.hair_colors # => [:brown, :black] - # - # The attribute name must be a valid method name in Ruby. - # - # class Person - # cattr_reader :"1_Badname " - # end - # # => NameError: invalid attribute name - # - # If you want to opt out the instance reader method, you can pass <tt>instance_reader: false</tt> - # or <tt>instance_accessor: false</tt>. - # - # class Person - # cattr_reader :hair_colors, instance_reader: false - # end - # - # Person.new.hair_colors # => NoMethodError - def cattr_reader(*syms) - options = syms.extract_options! - syms.each do |sym| - raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym} - @@#{sym} - end - EOS - - unless options[:instance_reader] == false || options[:instance_accessor] == false - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{sym} - @@#{sym} - end - EOS - end - end - end - - # Defines a class attribute if it's not defined and creates a writer method to allow - # assignment to the attribute. - # - # class Person - # cattr_writer :hair_colors - # end - # - # Person.hair_colors = [:brown, :black] - # Person.class_variable_get("@@hair_colors") # => [:brown, :black] - # Person.new.hair_colors = [:blonde, :red] - # Person.class_variable_get("@@hair_colors") # => [:blonde, :red] - # - # The attribute name must be a valid method name in Ruby. - # - # class Person - # cattr_writer :"1_Badname " - # end - # # => NameError: invalid attribute name - # - # If you want to opt out the instance writer method, pass <tt>instance_writer: false</tt> - # or <tt>instance_accessor: false</tt>. - # - # class Person - # cattr_writer :hair_colors, instance_writer: false - # end - # - # Person.new.hair_colors = [:blonde, :red] # => NoMethodError - # - # Also, you can pass a block to set up the attribute with a default value. - # - # class Person - # cattr_writer :hair_colors do - # [:brown, :black, :blonde, :red] - # end - # end - # - # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] - def cattr_writer(*syms) - options = syms.extract_options! - syms.each do |sym| - raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - unless defined? @@#{sym} - @@#{sym} = nil - end - - def self.#{sym}=(obj) - @@#{sym} = obj - end - EOS - - unless options[:instance_writer] == false || options[:instance_accessor] == false - class_eval(<<-EOS, __FILE__, __LINE__ + 1) - def #{sym}=(obj) - @@#{sym} = obj - end - EOS - end - send("#{sym}=", yield) if block_given? - end - end - - # Defines both class and instance accessors for class attributes. - # - # class Person - # cattr_accessor :hair_colors - # end - # - # Person.hair_colors = [:brown, :black, :blonde, :red] - # Person.hair_colors # => [:brown, :black, :blonde, :red] - # Person.new.hair_colors # => [:brown, :black, :blonde, :red] - # - # If a subclass changes the value then that would also change the value for - # parent class. Similarly if parent class changes the value then that would - # change the value of subclasses too. - # - # class Male < Person - # end - # - # Male.hair_colors << :blue - # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] - # - # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. - # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. - # - # class Person - # cattr_accessor :hair_colors, instance_writer: false, instance_reader: false - # end - # - # Person.new.hair_colors = [:brown] # => NoMethodError - # Person.new.hair_colors # => NoMethodError - # - # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. - # - # class Person - # cattr_accessor :hair_colors, instance_accessor: false - # end - # - # Person.new.hair_colors = [:brown] # => NoMethodError - # Person.new.hair_colors # => NoMethodError - # - # Also you can pass a block to set up the attribute with a default value. - # - # class Person - # cattr_accessor :hair_colors do - # [:brown, :black, :blonde, :red] - # end - # end - # - # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] - def cattr_accessor(*syms, &blk) - cattr_reader(*syms) - cattr_writer(*syms, &blk) - end -end +# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d, +# but we keep this around for libraries that directly require it knowing they +# want cattr_*. No need to deprecate. +require 'active_support/core_ext/module/attribute_accessors' diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb index ff870f5fd1..1c305c5970 100644 --- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb +++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb @@ -1,25 +1,30 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/remove_method' +require 'active_support/core_ext/module/deprecation' + class Class def superclass_delegating_accessor(name, options = {}) # Create private _name and _name= methods that can still be used if the public - # methods are overridden. This allows - _superclass_delegating_accessor("_#{name}") + # methods are overridden. + _superclass_delegating_accessor("_#{name}", options) - # Generate the public methods name, name=, and name? + # Generate the public methods name, name=, and name?. # These methods dispatch to the private _name, and _name= methods, making them - # overridable + # overridable. singleton_class.send(:define_method, name) { send("_#{name}") } singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") } singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) } - # If an instance_reader is needed, generate methods for name and name= on the - # class itself, so instances will be able to see them - define_method(name) { send("_#{name}") } if options[:instance_reader] != false - define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false + # If an instance_reader is needed, generate public instance methods name and name?. + if options[:instance_reader] != false + define_method(name) { send("_#{name}") } + define_method("#{name}?") { !!send("#{name}") } + end end + deprecate superclass_delegating_accessor: :class_attribute + private # Take the object being set and store it in a method. This gives us automatic # inheritance behavior, without having to store the object in an instance diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb index af048d0c85..c60e833441 100644 --- a/activesupport/lib/active_support/core_ext/date/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date/calculations.rb @@ -129,7 +129,7 @@ class Date options.fetch(:day, day) ) end - + # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there. def compare_with_coercion(other) if other.is_a?(Time) diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 6bc8f12176..df419a6e63 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -1,6 +1,7 @@ require 'date' require 'active_support/inflector/methods' require 'active_support/core_ext/date/zones' +require 'active_support/core_ext/module/remove_method' class Date DATE_FORMATS = { @@ -19,8 +20,10 @@ class Date # Ruby 1.9 has Date#to_time which converts to localtime only. remove_method :to_time - # Ruby 1.9 has Date#xmlschema which converts to a string without the time component. - remove_method :xmlschema + # Ruby 1.9 has Date#xmlschema which converts to a string without the time + # component. This removal may generate an issue on FreeBSD, that's why we + # need to use remove_possible_method here + remove_possible_method :xmlschema # Convert to a formatted string. See DATE_FORMATS for predefined formats. # @@ -37,12 +40,12 @@ class Date # date.to_formatted_s(:rfc822) # => "10 Nov 2007" # date.to_formatted_s(:iso8601) # => "2007-11-10" # - # == Adding your own time formats to to_formatted_s + # == Adding your own date formats to to_formatted_s # You can add your own formats to the Date::DATE_FORMATS hash. # Use the format name as the hash key and either a strftime string # or Proc instance that takes a date argument as the value. # - # # config/initializers/time_formats.rb + # # config/initializers/date_formats.rb # Date::DATE_FORMATS[:month_and_year] = '%B %Y' # Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") } def to_formatted_s(format = :default) diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index c869a0e210..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 @@ -213,6 +213,27 @@ 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(date_or_time) diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb index 8e5d723074..dc4e767e9d 100644 --- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb @@ -53,6 +53,16 @@ class DateTime # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, # <tt>:minutes</tt>, <tt>:seconds</tt>. def advance(options) + unless options[:weeks].nil? + options[:weeks], partial_weeks = options[:weeks].divmod(1) + options[:days] = options.fetch(:days, 0) + 7 * partial_weeks + end + + unless options[:days].nil? + options[:days], partial_days = options[:days].divmod(1) + options[:hours] = options.fetch(:hours, 0) + 24 * partial_days + end + d = to_date.advance(options) datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ @@ -63,7 +73,7 @@ class DateTime if seconds_to_advance.zero? datetime_advanced_by_date else - datetime_advanced_by_date.since seconds_to_advance + datetime_advanced_by_date.since(seconds_to_advance) end end @@ -151,7 +161,13 @@ class DateTime # Layers additional behavior on DateTime#<=> so that Time and # ActiveSupport::TimeWithZone instances can be compared with a DateTime. def <=>(other) - super other.to_datetime + if other.kind_of?(Infinity) + super + elsif other.respond_to? :to_datetime + super other.to_datetime + else + nil + end end end diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb index 6ddfb72a0d..2a9c09fc29 100644 --- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb @@ -71,9 +71,9 @@ class DateTime civil(year, month, day, hour, min, sec, offset) end - # Converts +self+ to a floating-point number of seconds since the Unix epoch. + # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch. def to_f - seconds_since_unix_epoch.to_f + seconds_since_unix_epoch.to_f + sec_fraction end # Converts +self+ to an integer number of seconds since the Unix epoch. diff --git a/activesupport/lib/active_support/core_ext/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb new file mode 100644 index 0000000000..593c51bba2 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb @@ -0,0 +1,51 @@ +require 'securerandom' + +module Digest + module UUID + DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc: + + # Generates a v5 non-random UUID (Universally Unique IDentifier). + # + # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs. + # uuid_from_hash always generates the same UUID for a given name and namespace combination. + # + # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt + def self.uuid_from_hash(hash_class, uuid_namespace, name) + if hash_class == Digest::MD5 + version = 3 + elsif hash_class == Digest::SHA1 + version = 5 + else + raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}." + end + + hash = hash_class.new + hash.update(uuid_namespace) + hash.update(name) + + ary = hash.digest.unpack('NnnnnN') + ary[2] = (ary[2] & 0x0FFF) | (version << 12) + ary[3] = (ary[3] & 0x3FFF) | 0x8000 + + "%08x-%04x-%04x-%04x-%04x%08x" % ary + end + + # Convenience method for uuid_from_hash using Digest::MD5. + def self.uuid_v3(uuid_namespace, name) + self.uuid_from_hash(Digest::MD5, uuid_namespace, name) + end + + # Convenience method for uuid_from_hash using Digest::SHA1. + def self.uuid_v5(uuid_namespace, name) + self.uuid_from_hash(Digest::SHA1, uuid_namespace, name) + end + + # Convenience method for SecureRandom.uuid. + def self.uuid_v4 + SecureRandom.uuid + end + end +end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index 4501b7ff58..1343beb87a 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -35,7 +35,7 @@ module Enumerable if block_given? Hash[map { |elem| [yield(elem), elem] }] else - to_enum :index_by + to_enum(:index_by) { size if respond_to?(:size) } end end diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb index c3e6124a57..0e7e3ba378 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 diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb index 686f12c6da..af4d1da0eb 100644 --- a/activesupport/lib/active_support/core_ext/hash.rb +++ b/activesupport/lib/active_support/core_ext/hash.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/hash/compact' require 'active_support/core_ext/hash/conversions' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/except' @@ -5,3 +6,4 @@ require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/hash/reverse_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb new file mode 100644 index 0000000000..6566215a4d --- /dev/null +++ b/activesupport/lib/active_support/core_ext/hash/compact.rb @@ -0,0 +1,20 @@ +class Hash + # Returns a hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil} + # hash.compact # => { a: true, b: false} + # hash # => { a: true, b: false, c: nil} + # { c: nil }.compact # => {} + def compact + self.select { |_, value| !value.nil? } + end + + # Replaces current hash with non +nil+ values. + # + # hash = { a: true, b: false, c: nil} + # hash.compact! # => { a: true, b: false} + # hash # => { a: true, b: false} + def compact! + self.reject! { |_, value| value.nil? } + end +end diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb index fbf2877117..2149d4439d 100644 --- a/activesupport/lib/active_support/core_ext/hash/conversions.rb +++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb @@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections' class Hash # Returns a string containing an XML representation of its receiver: # - # {'foo' => 1, 'bar' => 2}.to_xml + # { foo: 1, bar: 2 }.to_xml # # => # # <?xml version="1.0" encoding="UTF-8"?> # # <hash> @@ -43,7 +43,10 @@ class Hash # end # # { foo: Foo.new }.to_xml(skip_instruct: true) - # # => "<hash><bar>fooing!</bar></hash>" + # # => + # # <hash> + # # <bar>fooing!</bar> + # # </hash> # # * Otherwise, a node with +key+ as tag is created with a string representation of # +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added. @@ -102,7 +105,7 @@ class Hash # hash = Hash.from_xml(xml) # # => {"hash"=>{"foo"=>1, "bar"=>2}} # - # DisallowedType is raise if the XML contains attributes with <tt>type="yaml"</tt> or + # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML. def from_xml(xml, disallowed_types = nil) ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h @@ -201,7 +204,7 @@ module ActiveSupport end def become_empty_string?(value) - # {"string" => true} + # { "string" => true } # No tests fail when the second term is removed. value['type'] == 'string' && value['nil'] != 'true' end @@ -218,7 +221,7 @@ module ActiveSupport def garbage?(value) # If the type is the only element which makes it then # this still makes the value nil, except if type is - # a XML node(where type['value'] is a Hash) + # an XML node(where type['value'] is a Hash) value['type'] && !value['type'].is_a?(::Hash) && value.size == 1 end @@ -235,6 +238,6 @@ module ActiveSupport value.map! { |i| deep_to_h(i) } value.length > 1 ? value : value.first end + end end - diff --git a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb index e07db50b77..763d563231 100644 --- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb @@ -1,27 +1,38 @@ class Hash # Returns a new hash with +self+ and +other_hash+ merged recursively. # - # h1 = { x: { y: [4,5,6] }, z: [7,8,9] } - # h2 = { x: { y: [7,8,9] }, z: 'xyz' } + # h1 = { a: true, b: { c: [1, 2, 3] } } + # h2 = { a: false, b: { x: [3, 4, 5] } } # - # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"} - # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]} - # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) } - # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]} + # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } } + # + # Like with Hash#merge in the standard library, a block can be provided + # to merge values: + # + # h1 = { a: 100, b: 200, c: { c1: 100 } } + # h2 = { b: 250, c: { c1: 200 } } + # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val } + # # => { a: 100, b: 450, c: { c1: 300 } } def deep_merge(other_hash, &block) dup.deep_merge!(other_hash, &block) end # Same as +deep_merge+, but modifies +self+. def deep_merge!(other_hash, &block) - other_hash.each_pair do |k,v| - tv = self[k] - if tv.is_a?(Hash) && v.is_a?(Hash) - self[k] = tv.deep_merge(v, &block) + other_hash.each_pair do |current_key, other_value| + this_value = self[current_key] + + self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash) + this_value.deep_merge(other_value, &block) else - self[k] = block && tv ? block.call(k, tv, v) : v + if block_given? && key?(current_key) + block.call(current_key, this_value, other_value) + else + other_value + end end end + self end end diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb index d90e996ad4..682d089881 100644 --- a/activesupport/lib/active_support/core_ext/hash/except.rb +++ b/activesupport/lib/active_support/core_ext/hash/except.rb @@ -1,5 +1,5 @@ class Hash - # Return a hash that includes everything but the given keys. This is useful for + # Returns a hash that includes everything but the given keys. This is useful for # limiting a set of parameters to everything but a few known toggles: # # @person.update(params[:person].except(:admin)) 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..f4105f66b0 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,18 +17,19 @@ 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 } end @@ -38,13 +40,13 @@ class Hash transform_keys!{ |key| key.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 } 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 } 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..8ad600b171 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -26,6 +26,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/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index 36ab836457..80c531b694 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. @@ -60,8 +67,7 @@ module Kernel # puts 'This code gets executed and nothing related to ZeroDivisionError was seen' def suppress(*exception_classes) yield - rescue Exception => e - raise unless exception_classes.any? { |cls| e.kind_of?(cls) } + rescue *exception_classes end # Captures the given stream and returns it: @@ -80,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}") @@ -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/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/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..d317df5079 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -1,10 +1,59 @@ require 'active_support/core_ext/array/extract_options' +# Extends the module object with class/module and instance accessors for +# class/module attributes, just like the native attr* accessors for instance +# attributes. class Module + # Defines a class attribute and creates a class and instance reader methods. + # The underlying the class variable is set to +nil+, if it is not previously + # defined. + # + # module HairColors + # mattr_reader :hair_colors + # end + # + # HairColors.hair_colors # => nil + # HairColors.class_variable_set("@@hair_colors", [:brown, :black]) + # HairColors.hair_colors # => [:brown, :black] + # + # The attribute name must be a valid method name in Ruby. + # + # module Foo + # mattr_reader :"1_Badname " + # end + # # => NameError: invalid attribute name + # + # If you want to opt out the creation on the instance reader method, pass + # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>. + # + # module HairColors + # mattr_writer :hair_colors, instance_reader: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors # => NoMethodError + # + # + # Also, you can pass a block to set up the attribute with a default value. + # + # module HairColors + # cattr_reader :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # class Person + # include HairColors + # end + # + # Person.hair_colors # => [:brown, :black, :blonde, :red] def mattr_reader(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) @@#{sym} = nil unless defined? @@#{sym} @@ -20,14 +69,60 @@ class Module end EOS end + class_variable_set("@@#{sym}", yield) if block_given? end end + alias :cattr_reader :mattr_reader + # Defines a class attribute and creates a class and instance writer methods to + # allow assignment to the attribute. + # + # module HairColors + # mattr_writer :hair_colors + # end + # + # class Person + # include HairColors + # end + # + # HairColors.hair_colors = [:brown, :black] + # Person.class_variable_get("@@hair_colors") # => [:brown, :black] + # Person.new.hair_colors = [:blonde, :red] + # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red] + # + # If you want to opt out the instance writer method, pass + # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>. + # + # module HairColors + # mattr_writer :hair_colors, instance_writer: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors = [:blonde, :red] # => NoMethodError + # + # Also, you can pass a block to set up the attribute with a default value. + # + # class HairColors + # mattr_writer :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # class Person + # include HairColors + # end + # + # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red] def mattr_writer(*syms) options = syms.extract_options! syms.each do |sym| - raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/ + raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/ class_eval(<<-EOS, __FILE__, __LINE__ + 1) + @@#{sym} = nil unless defined? @@#{sym} + def self.#{sym}=(obj) @@#{sym} = obj end @@ -40,27 +135,78 @@ class Module end EOS end + send("#{sym}=", yield) if block_given? end end + alias :cattr_writer :mattr_writer - # Extends the module object with module and instance accessors for class attributes, - # just like the native attr* accessors for instance attributes. + # Defines both class and instance accessors for class attributes. # - # module AppConfiguration - # mattr_accessor :google_api_key + # module HairColors + # mattr_accessor :hair_colors + # end # - # self.google_api_key = "123456789" + # class Person + # include HairColors # end # - # AppConfiguration.google_api_key # => "123456789" - # AppConfiguration.google_api_key = "overriding the api key!" - # AppConfiguration.google_api_key # => "overriding the api key!" + # Person.hair_colors = [:brown, :black, :blonde, :red] + # Person.hair_colors # => [:brown, :black, :blonde, :red] + # Person.new.hair_colors # => [:brown, :black, :blonde, :red] + # + # If a subclass changes the value then that would also change the value for + # parent class. Similarly if parent class changes the value then that would + # change the value of subclasses too. + # + # class Male < Person + # end + # + # Male.hair_colors << :blue + # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue] # # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>. - # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>. - def mattr_accessor(*syms) - mattr_reader(*syms) - mattr_writer(*syms) + # + # module HairColors + # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors = [:brown] # => NoMethodError + # Person.new.hair_colors # => NoMethodError + # + # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods. + # + # module HairColors + # mattr_accessor :hair_colors, instance_accessor: false + # end + # + # class Person + # include HairColors + # end + # + # Person.new.hair_colors = [:brown] # => NoMethodError + # Person.new.hair_colors # => NoMethodError + # + # Also you can pass a block to set up the attribute with a default value. + # + # module HairColors + # mattr_accessor :hair_colors do + # [:brown, :black, :blonde, :red] + # end + # end + # + # class Person + # include HairColors + # end + # + # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red] + def mattr_accessor(*syms, &blk) + mattr_reader(*syms, &blk) + mattr_writer(*syms, &blk) end + alias :cattr_accessor :mattr_accessor end diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb new file mode 100644 index 0000000000..07a392404e --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/concerning.rb @@ -0,0 +1,135 @@ +require 'active_support/concern' + +class Module + # = Bite-sized separation of concerns + # + # We often find ourselves with a medium-sized chunk of behavior that we'd + # like to extract, but only mix in to a single class. + # + # Extracting a plain old Ruby object to encapsulate it and collaborate or + # delegate to the original object is often a good choice, but when there's + # no additional state to encapsulate or we're making DSL-style declarations + # about the parent class, introducing new collaborators can obfuscate rather + # than simplify. + # + # The typical route is to just dump everything in a monolithic class, perhaps + # with a comment, as a least-bad alternative. Using modules in separate files + # means tedious sifting to get a big-picture view. + # + # = Dissatisfying ways to separate small concerns + # + # == Using comments: + # + # class Todo + # # Other todo implementation + # # ... + # + # ## Event tracking + # has_many :events + # + # before_create :track_creation + # after_destroy :track_deletion + # + # private + # def track_creation + # # ... + # end + # end + # + # == With an inline module: + # + # Noisy syntax. + # + # class Todo + # # Other todo implementation + # # ... + # + # module EventTracking + # extend ActiveSupport::Concern + # + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # include EventTracking + # end + # + # == Mix-in noise exiled to its own file: + # + # Once our chunk of behavior starts pushing the scroll-to-understand it's + # boundary, we give in and move it to a separate file. At this size, the + # overhead feels in good proportion to the size of our extraction, despite + # diluting our at-a-glance sense of how things really work. + # + # class Todo + # # Other todo implementation + # # ... + # + # include TodoEventTracking + # end + # + # = Introducing Module#concerning + # + # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to + # separate bite-sized concerns. + # + # class Todo + # # Other todo implementation + # # ... + # + # concerning :EventTracking do + # included do + # has_many :events + # before_create :track_creation + # after_destroy :track_deletion + # end + # + # private + # def track_creation + # # ... + # end + # end + # end + # + # Todo.ancestors + # # => Todo, Todo::EventTracking, Object + # + # This small step has some wonderful ripple effects. We can + # * grok the behavior of our class in one glance, + # * clean up monolithic junk-drawer classes by separating their concerns, and + # * stop leaning on protected/private for crude "this is internal stuff" modularity. + module Concerning + # Define a new concern and mix it in. + def concerning(topic, &block) + include concern(topic, &block) + end + + # A low-cruft shortcut to define a concern. + # + # concern :EventTracking do + # ... + # end + # + # is equivalent to + # + # module EventTracking + # extend ActiveSupport::Concern + # + # ... + # end + def concern(topic, &module_definition) + const_set topic, Module.new { + extend ::ActiveSupport::Concern + module_eval(&module_definition) + } + end + end + include Concerning +end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index bca3800344..e926392952 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -138,6 +138,8 @@ class Module # # Foo.new("Bar").name # raises NoMethodError: undefined method `name' # + # The target method must be public, otherwise it will raise +NoMethodError+. + # def delegate(*methods) options = methods.pop unless options.is_a?(Hash) && to = options[:to] @@ -168,39 +170,28 @@ class Module # methods still accept two arguments. definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block' - # The following generated methods call the target exactly once, storing + # The following generated method calls the target exactly once, storing # the returned value in a dummy variable. # # Reason is twofold: On one hand doing less calls is in general better. # On the other hand it could be that the target has side-effects, # whereas conceptually, from the user point of view, the delegator should # be doing one call. - if allow_nil - module_eval(<<-EOS, file, line - 3) - def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - _ = #{to} # _ = client - if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name) - _.#{method}(#{definition}) # _.name(*args, &block) - end # end - end # end - EOS - else - exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - module_eval(<<-EOS, file, line - 2) - def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block) - _ = #{to} # _ = client - _.#{method}(#{definition}) # _.name(*args, &block) - rescue NoMethodError => e # rescue NoMethodError => e - location = "%s:%d:in `%s'" % [__FILE__, __LINE__ - 2, '#{method_prefix}#{method}'] # location = "%s:%d:in `%s'" % [__FILE__, __LINE__ - 2, 'customer_name'] - if _.nil? && e.backtrace.first == location # if _.nil? && e.backtrace.first == location - #{exception} # # add helpful message to the exception - else # else - raise # raise - end # end - end # end - EOS - end + exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") + + method_def = [ + "def #{method_prefix}#{method}(#{definition})", + " _ = #{to}", + " if !_.nil? || nil.respond_to?(:#{method})", + " _.#{method}(#{definition})", + " else", + " #{exception unless allow_nil}", + " end", + "end" + ].join ';' + + module_eval(method_def, file, line) end end end diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb index d873de197f..56d670fbe8 100644 --- a/activesupport/lib/active_support/core_ext/module/deprecation.rb +++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb @@ -1,5 +1,3 @@ -require 'active_support/deprecation/method_wrappers' - class Module # deprecate :foo # deprecate bar: 'message' diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index 87b9a23aef..689fae4830 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -61,19 +61,9 @@ class Numeric end alias :fortnight :fortnights - # Reads best without arguments: 10.minutes.ago - def ago(time = ::Time.current) - time - self + # Used with the standard time durations, like 1.hour.in_milliseconds -- + # so we can feed them to JavaScript functions like getTime(). + def in_milliseconds + self * 1000 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 - 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..f1106cca9b 100644 --- a/activesupport/lib/active_support/core_ext/object.rb +++ b/activesupport/lib/active_support/core_ext/object.rb @@ -2,13 +2,14 @@ require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/object/deep_dup' +require 'active_support/core_ext/object/itself' require 'active_support/core_ext/object/try' require 'active_support/core_ext/object/inclusion' require 'active_support/core_ext/object/conversions' require 'active_support/core_ext/object/instance_variables' -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/object/to_param' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/object/with_options' diff --git a/activesupport/lib/active_support/core_ext/object/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb index 8a5eb4bc93..38e43478df 100644 --- a/activesupport/lib/active_support/core_ext/object/blank.rb +++ b/activesupport/lib/active_support/core_ext/object/blank.rb @@ -4,36 +4,42 @@ class Object # An object is blank if it's false, empty, or a whitespace string. # For example, '', ' ', +nil+, [], and {} are all blank. # - # This simplifies: + # This simplifies # - # if address.nil? || address.empty? + # address.nil? || address.empty? # - # ...to: + # to # - # if address.blank? + # address.blank? + # + # @return [true, false] def blank? - respond_to?(:empty?) ? empty? : !self + respond_to?(:empty?) ? !!empty? : !self end - # An object is present if it's not <tt>blank?</tt>. + # An object is present if it's not blank. + # + # @return [true, false] def present? !blank? end - # Returns object if it's <tt>present?</tt> otherwise returns +nil+. - # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>. + # Returns the receiver if it's present otherwise returns +nil+. + # <tt>object.presence</tt> is equivalent to # - # This is handy for any representation of objects where blank is the same - # as not present at all. For example, this simplifies a common check for - # HTTP POST/query parameters: + # object.present? ? object : nil + # + # For example, something like # # state = params[:state] if params[:state].present? # country = params[:country] if params[:country].present? # region = state || country || 'US' # - # ...becomes: + # becomes # # region = params[:state].presence || params[:country].presence || 'US' + # + # @return [Object] def presence self if present? end @@ -43,6 +49,8 @@ class NilClass # +nil+ is blank: # # nil.blank? # => true + # + # @return [true] def blank? true end @@ -52,6 +60,8 @@ class FalseClass # +false+ is blank: # # false.blank? # => true + # + # @return [true] def blank? true end @@ -61,6 +71,8 @@ class TrueClass # +true+ is not blank: # # true.blank? # => false + # + # @return [false] def blank? false end @@ -71,6 +83,8 @@ class Array # # [].blank? # => true # [1,2,3].blank? # => false + # + # @return [true, false] alias_method :blank?, :empty? end @@ -79,18 +93,28 @@ class Hash # # {}.blank? # => true # { key: 'value' }.blank? # => false + # + # @return [true, false] alias_method :blank?, :empty? end class String + BLANK_RE = /\A[[:space:]]*\z/ + # A string is blank if it's empty or contains whitespaces only: # - # ''.blank? # => true - # ' '.blank? # => true - # ' '.blank? # => true - # ' something here '.blank? # => false + # ''.blank? # => true + # ' '.blank? # => true + # "\t\n\r".blank? # => true + # ' blah '.blank? # => false + # + # Unicode whitespace is supported: + # + # "\u00a0".blank? # => true + # + # @return [true, false] def blank? - self !~ /[^[:space:]]/ + BLANK_RE === self end end @@ -99,6 +123,8 @@ class Numeric #:nodoc: # # 1.blank? # => false # 0.blank? # => false + # + # @return [false] def blank? false end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 1d639f3af6..2e99f4a1b8 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,8 +22,8 @@ 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 } 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 b5671f66d0..55f281b213 100644 --- a/activesupport/lib/active_support/core_ext/object/inclusion.rb +++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb @@ -12,4 +12,16 @@ class Object rescue NoMethodError raise ArgumentError.new("The parameter passed to #in? must respond to #include?") end + + # Returns the receiver if it's included in the argument otherwise returns +nil+. + # Argument must be any object which responds to +#include?+. Usage: + # + # params[:bucket_type].presence_in %w( project calendar ) + # + # This will throw an ArgumentError if the argument doesn't respond to +#include?+. + # + # @return [Object] + def presence_in(another_object) + self.in?(another_object) ? self : nil + end end diff --git a/activesupport/lib/active_support/core_ext/object/itself.rb b/activesupport/lib/active_support/core_ext/object/itself.rb new file mode 100644 index 0000000000..d71cea6674 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/itself.rb @@ -0,0 +1,15 @@ +class Object + # TODO: Remove this file when we drop support for Ruby < 2.2 + unless respond_to?(:itself) + # Returns the object itself. + # + # Useful for chaining methods, such as Active Record scopes: + # + # Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at) + # + # @return Object + def itself + self + end + end +end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb new file mode 100644 index 0000000000..698b2d1920 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -0,0 +1,197 @@ +# Hack to load json gem first so we can overwrite its to_json. +require 'json' +require 'bigdecimal' +require 'active_support/core_ext/big_decimal/conversions' # for #to_s +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/instance_variables' +require 'time' +require 'active_support/core_ext/time/conversions' +require 'active_support/core_ext/date_time/conversions' +require 'active_support/core_ext/date/conversions' +require 'active_support/core_ext/module/aliasing' + +# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting +# their default behavior. That said, we need to define the basic to_json method in all of them, +# otherwise they will always use to_json gem implementation, which is backwards incompatible in +# several cases (for instance, the JSON implementation for Hash does not work) with inheritance +# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. +# +# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the +# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always +# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the +# calls to the original to_json method. +# +# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is +# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply +# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump} +# should give exactly the same results with or without active support. +[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass| + klass.class_eval do + def to_json_with_active_support_encoder(options = nil) + if options.is_a?(::JSON::State) + # Called from JSON.{generate,dump}, forward it to JSON gem's to_json + self.to_json_without_active_support_encoder(options) + else + # to_json is being invoked directly, use ActiveSupport's encoder + ActiveSupport::JSON.encode(self, options) + end + end + + alias_method_chain :to_json, :active_support_encoder + end +end + +class Object + def as_json(options = nil) #:nodoc: + if respond_to?(:to_hash) + to_hash.as_json(options) + else + instance_values.as_json(options) + end + end +end + +class Struct #:nodoc: + def as_json(options = nil) + Hash[members.zip(values)].as_json(options) + end +end + +class TrueClass + def as_json(options = nil) #:nodoc: + self + end +end + +class FalseClass + def as_json(options = nil) #:nodoc: + self + end +end + +class NilClass + def as_json(options = nil) #:nodoc: + self + end +end + +class String + def as_json(options = nil) #:nodoc: + self + end +end + +class Symbol + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Numeric + def as_json(options = nil) #:nodoc: + self + end +end + +class Float + # Encoding Infinity or NaN to JSON should return "null". The default returns + # "Infinity" or "NaN" which are not valid JSON. + def as_json(options = nil) #:nodoc: + finite? ? self : nil + end +end + +class BigDecimal + # A BigDecimal would be naturally represented as a JSON number. Most libraries, + # however, parse non-integer JSON numbers directly as floats. Clients using + # those libraries would get in general a wrong number and no way to recover + # other than manually inspecting the string with the JSON code itself. + # + # That's why a JSON string is returned. The JSON literal is not numeric, but + # if the other end knows by contract that the data is supposed to be a + # BigDecimal, it still has the chance to post-process the string and get the + # real value. + def as_json(options = nil) #:nodoc: + finite? ? to_s : nil + end +end + +class Regexp + def as_json(options = nil) #:nodoc: + to_s + end +end + +module Enumerable + def as_json(options = nil) #:nodoc: + to_a.as_json(options) + end +end + +class Range + def as_json(options = nil) #:nodoc: + to_s + end +end + +class Array + def as_json(options = nil) #:nodoc: + map { |v| options ? v.as_json(options.dup) : v.as_json } + end +end + +class Hash + def as_json(options = nil) #:nodoc: + # create a subset of the hash by applying :only or :except + subset = if options + if attrs = options[:only] + slice(*Array(attrs)) + elsif attrs = options[:except] + except(*Array(attrs)) + else + self + end + else + self + end + + Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }] + end +end + +class Time + def as_json(options = nil) #:nodoc: + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + xmlschema(ActiveSupport::JSON::Encoding.time_precision) + else + %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) + end + end +end + +class Date + def as_json(options = nil) #:nodoc: + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + strftime("%Y-%m-%d") + else + strftime("%Y/%m/%d") + end + end +end + +class DateTime + def as_json(options = nil) #:nodoc: + if ActiveSupport::JSON::Encoding.use_standard_json_time_format + xmlschema(ActiveSupport::JSON::Encoding.time_precision) + else + strftime('%Y/%m/%d %H:%M:%S %z') + end + end +end + +class Process::Status #:nodoc: + def as_json(options = nil) + { :exitstatus => exitstatus, :pid => pid } + end +end diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb deleted file mode 100644 index 83cc8066e7..0000000000 --- a/activesupport/lib/active_support/core_ext/object/to_json.rb +++ /dev/null @@ -1,27 +0,0 @@ -# Hack to load json gem first so we can overwrite its to_json. -begin - require 'json' -rescue LoadError -end - -# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting -# their default behavior. That said, we need to define the basic to_json method in all of them, -# otherwise they will always use to_json gem implementation, which is backwards incompatible in -# several cases (for instance, the JSON implementation for Hash does not work) with inheritance -# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json. -[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass| - klass.class_eval do - # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info. - def to_json(options = nil) - ActiveSupport::JSON.encode(self, options) - end - end -end - -module Process - class Status - def as_json(options = nil) - { :exitstatus => exitstatus, :pid => pid } - end - end -end diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb index 3b137ce6ae..684d4ef57e 100644 --- a/activesupport/lib/active_support/core_ext/object/to_param.rb +++ b/activesupport/lib/active_support/core_ext/object/to_param.rb @@ -1,58 +1 @@ -class Object - # Alias of <tt>to_s</tt>. - def to_param - to_s - end -end - -class NilClass - # Returns +self+. - def to_param - self - end -end - -class TrueClass - # Returns +self+. - def to_param - self - end -end - -class FalseClass - # Returns +self+. - def to_param - self - end -end - -class Array - # Calls <tt>to_param</tt> on all its elements and joins the result with - # slashes. This is used by <tt>url_for</tt> in Action Pack. - def to_param - collect { |e| e.to_param }.join '/' - end -end - -class Hash - # Returns a string representation of the receiver suitable for use as a URL - # query string: - # - # {name: 'David', nationality: 'Danish'}.to_param - # # => "name=David&nationality=Danish" - # - # An optional namespace can be passed to enclose the param names: - # - # {name: 'David', nationality: 'Danish'}.to_param('user') - # # => "user[name]=David&user[nationality]=Danish" - # - # The string pairs "key=value" that conform the query string - # are sorted lexicographically in ascending order. - # - # This method is also aliased as +to_query+. - def to_param(namespace = nil) - collect do |key, value| - value.to_query(namespace ? "#{namespace}[#{key}]" : key) - end.sort! * '&' - end -end +require 'active_support/core_ext/object/to_query' diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 5d5fcf00e0..ccd568bbf5 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 { |e| e.to_param }.join '/' + end + # Converts an array into a string suitable for use as a URL query string, # using the given +key+ as the param name. # # ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding" def to_query(key) prefix = "#{key}[]" - collect { |value| value.to_query(prefix) }.join '&' + + if empty? + nil.to_query(prefix) + else + collect { |value| value.to_query(prefix) }.join '&' + end end end class Hash - alias_method :to_query, :to_param + # Returns a string representation of the receiver suitable for use as a URL + # query string: + # + # {name: 'David', nationality: 'Danish'}.to_query + # # => "name=David&nationality=Danish" + # + # An optional namespace can be passed to enclose key names: + # + # {name: 'David', nationality: 'Danish'}.to_query('user') + # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish" + # + # The string pairs "key=value" that conform the query string + # are sorted lexicographically in ascending order. + # + # This method is also aliased as +to_param+. + def to_query(namespace = nil) + collect do |key, value| + unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty? + value.to_query(namespace ? "#{namespace}[#{key}]" : key) + end + end.compact.sort! * '&' + end + + alias_method :to_param, :to_query end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 48190e1e66..31919474ed 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -33,6 +33,11 @@ class Object # ... # 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 @@ -40,7 +45,11 @@ class Object # delegator itself. 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) if respond_to?(a.first) end diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb index 42e388b065..42e87c4424 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,22 @@ 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) + def with_options(options, &block) + option_merger = ActiveSupport::OptionMerger.new(self, options) + block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger) end end diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb index 1d8b1ede5a..9368e81235 100644 --- a/activesupport/lib/active_support/core_ext/range.rb +++ b/activesupport/lib/active_support/core_ext/range.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/range/conversions' require 'active_support/core_ext/range/include_range' require 'active_support/core_ext/range/overlaps' +require 'active_support/core_ext/range/each' diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb new file mode 100644 index 0000000000..ecef78f55f --- /dev/null +++ b/activesupport/lib/active_support/core_ext/range/each.rb @@ -0,0 +1,23 @@ +require 'active_support/core_ext/module/aliasing' + +class Range #:nodoc: + + def each_with_time_with_zone(&block) + ensure_iteration_allowed + each_without_time_with_zone(&block) + end + alias_method_chain :each, :time_with_zone + + def step_with_time_with_zone(n = 1, &block) + ensure_iteration_allowed + step_without_time_with_zone(n, &block) + end + alias_method_chain :step, :time_with_zone + + private + def ensure_iteration_allowed + if first.is_a?(Time) + raise TypeError, "can't iterate from #{first.class}" + end + end +end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index ee3b6d2b3f..ebd0dd3fc7 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -8,22 +8,22 @@ class String # the beginning of the range is greater than the end of the string. # # str = "hello" - # str.at(0) #=> "h" - # str.at(1..3) #=> "ell" - # str.at(-2) #=> "l" - # str.at(-2..-1) #=> "lo" - # str.at(5) #=> nil - # str.at(5..-1) #=> "" + # str.at(0) # => "h" + # str.at(1..3) # => "ell" + # str.at(-2) # => "l" + # str.at(-2..-1) # => "lo" + # str.at(5) # => nil + # str.at(5..-1) # => "" # # If a Regexp is given, the matching portion of the string is returned. # If a String is given, that given string is returned if it occurs in # the string. In both cases, nil is returned if there is no match. # # str = "hello" - # str.at(/lo/) #=> "lo" - # str.at(/ol/) #=> nil - # str.at("lo") #=> "lo" - # str.at("ol") #=> nil + # str.at(/lo/) # => "lo" + # str.at(/ol/) # => nil + # str.at("lo") # => "lo" + # str.at("ol") # => nil def at(position) self[position] end @@ -32,15 +32,15 @@ class String # If the position is negative, it is counted from the end of the string. # # str = "hello" - # str.from(0) #=> "hello" - # str.from(3) #=> "lo" - # str.from(-2) #=> "lo" + # str.from(0) # => "hello" + # str.from(3) # => "lo" + # str.from(-2) # => "lo" # # You can mix it with +to+ method and do fun things like: # # str = "hello" - # str.from(0).to(-1) #=> "hello" - # str.from(1).to(-2) #=> "ell" + # str.from(0).to(-1) # => "hello" + # str.from(1).to(-2) # => "ell" def from(position) self[position..-1] end @@ -49,34 +49,34 @@ class String # If the position is negative, it is counted from the end of the string. # # str = "hello" - # str.to(0) #=> "h" - # str.to(3) #=> "hell" - # str.to(-2) #=> "hell" + # str.to(0) # => "h" + # str.to(3) # => "hell" + # str.to(-2) # => "hell" # # You can mix it with +from+ method and do fun things like: # # str = "hello" - # str.from(0).to(-1) #=> "hello" - # str.from(1).to(-2) #=> "ell" + # str.from(0).to(-1) # => "hello" + # str.from(1).to(-2) # => "ell" def to(position) - self[0, position + 1] + self[0..position] end # Returns the first character. If a limit is supplied, returns a substring # from the beginning of the string until it reaches the limit value. If the - # given limit is greater than or equal to the string length, returns self. + # given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" - # str.first #=> "h" - # str.first(1) #=> "h" - # str.first(2) #=> "he" - # str.first(0) #=> "" - # str.first(6) #=> "hello" + # str.first # => "h" + # str.first(1) # => "h" + # str.first(2) # => "he" + # str.first(0) # => "" + # str.first(6) # => "hello" def first(limit = 1) if limit == 0 '' elsif limit >= size - self + self.dup else to(limit - 1) end @@ -84,19 +84,19 @@ class String # Returns the last character of the string. If a limit is supplied, returns a substring # from the end of the string until it reaches the limit value (counting backwards). If - # the given limit is greater than or equal to the string length, returns self. + # the given limit is greater than or equal to the string length, returns a copy of self. # # str = "hello" - # str.last #=> "o" - # str.last(1) #=> "o" - # str.last(2) #=> "lo" - # str.last(0) #=> "" - # str.last(6) #=> "hello" + # str.last # => "o" + # str.last(1) # => "o" + # str.last(2) # => "lo" + # str.last(0) # => "" + # str.last(6) # => "hello" def last(limit = 1) if limit == 0 '' elsif limit >= size - self + self.dup else from(-limit) end diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb index 6691fc0995..3e0cb8a7ac 100644 --- a/activesupport/lib/active_support/core_ext/string/conversions.rb +++ b/activesupport/lib/active_support/core_ext/string/conversions.rb @@ -36,20 +36,20 @@ class String # Converts a string to a Date value. # - # "1-1-2012".to_date #=> Sun, 01 Jan 2012 - # "01/01/2012".to_date #=> Sun, 01 Jan 2012 - # "2012-12-13".to_date #=> Thu, 13 Dec 2012 - # "12/13/2012".to_date #=> ArgumentError: invalid date + # "1-1-2012".to_date # => Sun, 01 Jan 2012 + # "01/01/2012".to_date # => Sun, 01 Jan 2012 + # "2012-12-13".to_date # => Thu, 13 Dec 2012 + # "12/13/2012".to_date # => ArgumentError: invalid date def to_date ::Date.parse(self, false) unless blank? end # Converts a string to a DateTime value. # - # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000 - # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000 - # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000 - # "12/13/2012".to_datetime #=> ArgumentError: invalid date + # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000 + # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000 + # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000 + # "12/13/2012".to_datetime # => ArgumentError: invalid date def to_datetime ::DateTime.parse(self, false) unless blank? end diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb index 114bcb87f0..0ac684f6ee 100644 --- a/activesupport/lib/active_support/core_ext/string/exclude.rb +++ b/activesupport/lib/active_support/core_ext/string/exclude.rb @@ -2,9 +2,9 @@ class String # The inverse of <tt>String#include?</tt>. Returns true if the string # does not include the other string. # - # "hello".exclude? "lo" #=> false - # "hello".exclude? "ol" #=> true - # "hello".exclude? ?h #=> false + # "hello".exclude? "lo" # => false + # "hello".exclude? "ol" # => true + # "hello".exclude? ?h # => false def exclude?(string) !include?(string) end diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb index a4cacaf789..2b1583d4ac 100644 --- a/activesupport/lib/active_support/core_ext/string/filters.rb +++ b/activesupport/lib/active_support/core_ext/string/filters.rb @@ -3,7 +3,7 @@ class String # the string, and then changing remaining consecutive whitespace # groups into one space each. # - # Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E). + # Note that it handles both ASCII and Unicode whitespace. # # %{ Multi-line # string }.squish # => "Multi-line string" @@ -20,6 +20,16 @@ class String self end + # Returns a new string with all occurrences of the pattern removed. Short-hand for String#gsub(pattern, ''). + def remove(pattern) + gsub pattern, '' + end + + # Alters the string by removing all occurrences of the pattern. Short-hand for String#gsub!(pattern, ''). + def remove!(pattern) + gsub! pattern, '' + end + # Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>: # # 'Once upon a time in a world far far away'.truncate(27) @@ -52,4 +62,28 @@ class String "#{self[0, stop]}#{omission}" end + + # Truncates a given +text+ after a given number of words (<tt>words_count</tt>): + # + # 'Once upon a time in a world far far away'.truncate_words(4) + # # => "Once upon a time..." + # + # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words: + # + # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>') + # # => "Once<br>upon<br>a<br>time<br>in..." + # + # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."): + # + # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)') + # # => "And they found that many... (continued)" + def truncate_words(words_count, options = {}) + sep = options[:separator] || /\s+/ + sep = Regexp.escape(sep.to_s) unless Regexp === sep + if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m + $1 + (options[:omission] || '...') + else + dup + end + end end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 56e8a5f98d..a943752f17 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -31,7 +31,7 @@ class String def pluralize(count = nil, locale = :en) locale = count if count.is_a?(Symbol) if count == 1 - self + self.dup else ActiveSupport::Inflector.pluralize(self, locale) end @@ -130,6 +130,8 @@ class String # # 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections" # 'Inflections'.demodulize # => "Inflections" + # '::Inflections'.demodulize # => "Inflections" + # ''.demodulize # => '' # # See also +deconstantize+. def demodulize @@ -182,21 +184,23 @@ class String # # 'egg_and_hams'.classify # => "EggAndHam" # 'posts'.classify # => "Post" - # - # Singular names are not handled correctly. - # - # 'business'.classify # => "Business" def classify ActiveSupport::Inflector.classify(self) end - # Capitalizes the first word, turns underscores into spaces, and strips '_id'. + # Capitalizes the first word, turns underscores into spaces, and strips a + # trailing '_id' if present. # Like +titleize+, this is meant for creating pretty output. # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - def humanize - ActiveSupport::Inflector.humanize(self) + # The capitalization of the first word can be turned off by setting the + # optional parameter +capitalize+ to false. + # By default, this parameter is true. + # + # 'employee_salary'.humanize # => "Employee salary" + # 'author_id'.humanize # => "Author" + # 'author_id'.humanize(capitalize: false) # => "author" + 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 dc033ed11b..c761325108 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') @@ -48,17 +57,56 @@ class ERB module_function :html_escape_once - # A utility method for escaping HTML entities in JSON strings - # using \uXXXX JavaScript escape sequences for string literals: + # A utility method for escaping HTML entities in JSON strings. Specifically, the + # &, > and < characters are replaced with their equivalent unicode escaped form - + # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also + # escaped as they are treated as newline characters in some JavaScript engines. + # These sequences have identical meaning as the original characters inside the + # context of a JSON string, so assuming the input is a valid and well-formed + # JSON value, the output will have equivalent meaning when parsed: + # + # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"}) + # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}" + # + # json_escape(json) + # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}" + # + # JSON.parse(json) == JSON.parse(json_escape(json)) + # # => true + # + # The intended use case for this method is to escape JSON strings before including + # them inside a script tag to avoid XSS vulnerability: # - # json_escape('is a > 0 & a < 10?') - # # => is a \u003E 0 \u0026 a \u003C 10? + # <script> + # var currentUser = <%= raw json_escape(current_user.to_json) %>; + # </script> # - # Note that after this operation is performed the output is not - # valid JSON. In particular double quotes are removed: + # 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. # - # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}') - # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1} + # If you need to output JSON elsewhere in your HTML, you can just do something + # like this, as any unsafe characters (including quotation marks) will be + # automatically escaped for you: + # + # <div data-user-info="<%= current_user.to_json %>">...</div> + # + # WARNING: this helper only works with valid JSON. Using this on non-JSON values + # will open up serious XSS vulnerabilities. For example, if you replace the + # +current_user.to_json+ in the example above with user input instead, the browser + # will happily eval() that string as JavaScript. + # + # The escaping performed in this method is identical to those performed in the + # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is + # set to true. Because this transformation is idempotent, this helper can be + # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true. + # + # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+ + # is enabled, or if you are unsure where your JSON string originated from, it + # is recommended that you always apply this helper (other libraries, such as the + # JSON gem, do not provide this kind of protection by default; also some gems + # might override +to_json+ to bypass Active Support's encoder). def json_escape(s) result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE) s.html_safe? ? result.html_safe : result @@ -84,7 +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 @@ -130,28 +178,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 +223,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,6 +236,13 @@ 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 diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb index 5481766f10..4cd6634558 100644 --- a/activesupport/lib/active_support/core_ext/thread.rb +++ b/activesupport/lib/active_support/core_ext/thread.rb @@ -23,29 +23,29 @@ class Thread # 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] + _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 + _locals[key.to_sym] = value end - # Returns an an array of the names of the thread-local variables (as Symbols). + # Returns 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] + # 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 + _locals.keys end # Returns <tt>true</tt> if the given string (or symbol) exists as a @@ -53,22 +53,34 @@ class Thread # # me = Thread.current # me.thread_variable_set(:oliver, "a") - # me.thread_variable?(:oliver) #=> true - # me.thread_variable?(:stanley) #=> false + # 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) + _locals.has_key?(key.to_sym) + end + + # Freezes the thread so that thread local variables cannot be set via + # Thread#thread_variable_set, nor can fiber local variables be set. + # + # me = Thread.current + # me.freeze + # me.thread_variable_set(:oliver, "a") #=> RuntimeError: can't modify frozen thread locals + # me[:oliver] = "a" #=> RuntimeError: can't modify frozen thread locals + def freeze + _locals.freeze + super end private - def locals - if defined?(@locals) - @locals + def _locals + if defined?(@_locals) + @_locals else - LOCK.synchronize { @locals ||= {} } + 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 6e0af0db4d..ab8307429a 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -64,11 +64,12 @@ class Time # Returns a new Time where one or more of the elements have been changed according # to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>) reset cascadingly, so if only the hour is passed, - # then minute, sec, and usec is set to 0. If the hour and minute is passed, then - # sec and usec is set to 0. The +options+ parameter takes a hash with any of these - # keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, - # <tt>:sec</tt>, <tt>:usec</tt>. + # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only + # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour + # and minute is passed, then sec, usec and nsec is set to 0. The +options+ + # parameter takes a hash with any of these keys: <tt>:year</tt>, <tt>:month</tt>, + # <tt>:day</tt>, <tt>:hour</tt>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt> + # <tt>:nsec</tt>. 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) @@ -80,21 +81,29 @@ class Time new_hour = options.fetch(:hour, hour) new_min = options.fetch(:min, options[:hour] ? 0 : min) new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec) - new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + + if new_nsec = options[:nsec] + raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec] + new_usec = Rational(new_nsec, 1000) + else + new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000)) + end if utc? ::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) elsif zone ::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec) else + raise ArgumentError, 'argument out of range' if new_usec > 999999 ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset) end end - # Uses Date to provide precise Time calculations for years, months, and days. - # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>, - # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, - # <tt>:minutes</tt>, <tt>:seconds</tt>. + # Uses Date to provide precise Time calculations for years, months, and days + # according to the proleptic Gregorian calendar. The +options+ parameter + # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>, + # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>, + # <tt>:seconds</tt>. def advance(options) unless options[:weeks].nil? options[:weeks], partial_weeks = options[:weeks].divmod(1) @@ -107,6 +116,7 @@ class Time end d = to_date.advance(options) + d = d.gregorian if d.julian? time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day) seconds_to_advance = \ options.fetch(:seconds, 0) + @@ -199,27 +209,6 @@ class Time beginning_of_day..end_of_day end - # Returns a Range representing the whole week of the current time. - # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set. - def all_week(start_day = Date.beginning_of_week) - beginning_of_week(start_day)..end_of_week(start_day) - end - - # Returns a Range representing the whole month of the current time. - def all_month - beginning_of_month..end_of_month - end - - # Returns a Range representing the whole quarter of the current time. - def all_quarter - beginning_of_quarter..end_of_quarter - end - - # Returns a Range representing the whole year of the current time. - def all_year - beginning_of_year..end_of_year - end - def plus_with_duration(other) #:nodoc: if ActiveSupport::Duration === other other.since(self) diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index 9fd26156c7..dbf1f2f373 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -20,7 +20,7 @@ class Time :iso8601 => lambda { |time| time.iso8601 } } - # Converts to a formatted string. See DATE_FORMATS for builtin formats. + # Converts to a formatted string. See DATE_FORMATS for built-in formats. # # This method is aliased to <tt>to_s</tt>. # diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 73559bfe0e..93a11d4586 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' @@ -175,14 +176,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 @@ -198,9 +208,19 @@ module ActiveSupport #:nodoc: 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) @@ -388,7 +408,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 @@ -445,8 +466,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 @@ -459,7 +478,7 @@ module ActiveSupport #:nodoc: if loaded.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 +516,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 @@ -648,6 +667,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 diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb index 0281c9222e..46e9996d59 100644 --- a/activesupport/lib/active_support/deprecation.rb +++ b/activesupport/lib/active_support/deprecation.rb @@ -32,7 +32,7 @@ module ActiveSupport # and the second is a library name # # ActiveSupport::Deprecation.new('2.0', 'MyLibrary') - def initialize(deprecation_horizon = '4.1', gem_name = 'Rails') + def initialize(deprecation_horizon = '5.0', gem_name = 'Rails') self.gem_name = gem_name self.deprecation_horizon = deprecation_horizon # By default, warnings are not silenced and debugging is off. diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index 90db180124..328b8c320a 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -1,14 +1,24 @@ 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 Rails.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/duration.rb b/activesupport/lib/active_support/duration.rb index 7cd27717f9..e861a17426 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -7,7 +7,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 +39,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 +53,10 @@ 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) @@ -76,19 +84,21 @@ 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 + protected def sum(sign, time = ::Time.current) #:nodoc: @@ -107,6 +117,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 d6918bede2..78b627c286 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -92,7 +92,7 @@ module ActiveSupport def watched @watched || begin - all = @files.select { |f| File.exists?(f) } + all = @files.select { |f| File.exist?(f) } all.concat(Dir[@glob]) if @glob all end diff --git a/activesupport/lib/active_support/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb deleted file mode 100644 index 81e63e76a7..0000000000 --- a/activesupport/lib/active_support/file_watcher.rb +++ /dev/null @@ -1,36 +0,0 @@ -module ActiveSupport - class FileWatcher - class Backend - def initialize(path, watcher) - @watcher = watcher - @path = path - end - - def trigger(files) - @watcher.trigger(files) - end - end - - def initialize - @regex_matchers = {} - end - - def watch(pattern, &block) - @regex_matchers[pattern] = block - end - - def trigger(files) - trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } } - - files.each do |file, state| - @regex_matchers.each do |pattern, block| - trigger_files[block][state] << file if pattern === file - end - end - - trigger_files.each do |block, payload| - block.call payload - end - end - end -end diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb new file mode 100644 index 0000000000..0a2b4c1618 --- /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 = 4 + MINOR = 2 + TINY = 0 + PRE = "beta1" + + STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") + end +end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 3da99872c0..3d8f2d572b 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -55,7 +55,7 @@ module ActiveSupport end def initialize(constructor = {}) - if constructor.is_a?(Hash) + if constructor.respond_to?(:to_hash) super() update(constructor) else @@ -72,8 +72,10 @@ module ActiveSupport end def self.new_from_hash_copying_default(hash) + hash = hash.to_hash new(hash).tap do |new_hash| new_hash.default = hash.default + new_hash.default_proc = hash.default_proc if hash.default_proc end end @@ -125,7 +127,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 +161,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 +174,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 +216,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 @@ -228,16 +237,20 @@ module ActiveSupport def to_options!; self end def select(*args, &block) - dup.tap {|hash| hash.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 - _new_hash= {} + _new_hash = Hash.new(default) each do |key, value| - _new_hash[convert_key(key)] = convert_value(value, for: :to_hash) + _new_hash[key] = convert_value(value, for: :to_hash) end - Hash.new(default).merge!(_new_hash) + _new_hash end protected 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 4ea6abfa12..2ca1124e76 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -1,5 +1,11 @@ require 'active_support/inflector/inflections' +#-- +# Defines the standard inflection rules. These are the starting point for +# new projects and are not considered complete. The current set of inflection +# rules is frozen. This means, we do not change them to become more complete. +# This is a safety measure to keep existing applications from breaking. +#++ module ActiveSupport Inflector.inflections(:en) do |inflect| inflect.plural(/$/, 's') diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb index c96debb93f..97401ccec7 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("|")}/ @@ -160,7 +160,7 @@ module ActiveSupport # uncountable 'money', 'information' # uncountable %w( money information rice ) def uncountable(*words) - (@uncountables << words).flatten! + @uncountables += words.flatten.map(&:downcase) end # Specifies a humanized form of a string by a regular expression rule or diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index d063fcb49c..f35e71ce81 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -36,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" @@ -89,8 +89,9 @@ module ActiveSupport # # 'SSLError'.underscore.camelize # => "SslError" def underscore(camel_cased_word) + return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/ word = camel_cased_word.to_s.gsub('::', '/') - word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" } + word.gsub!(/(?:([A-Za-z\d])|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$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" + # + # If "SSL" was defined to be an acronym: + # + # humanize('ssl_error') # => "SSL error" # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - def humanize(lower_case_and_underscored_word) + def humanize(lower_case_and_underscored_word, options = {}) result = lower_case_and_underscored_word.to_s.dup + inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } - result.gsub!(/_id$/, "") + + result.sub!(/\A_+/, '') + result.sub!(/_id\z/, '') result.tr!('_', ' ') - result.gsub(/([a-z\d]*)/i) { |match| + + result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" - }.gsub(/^\w/) { $&.upcase } + end + + if options.fetch(:capitalize, true) + result.sub!(/\A\w/) { |match| match.upcase } + end + + result end # Capitalizes all the words and replaces some characters in the string to @@ -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) @@ -220,7 +250,7 @@ module ActiveSupport def constantize(camel_cased_word) names = camel_cased_word.split('::') - # Trigger a builtin NameError exception including the ill-formed constant in the message. + # Trigger a built-in NameError exception including the ill-formed constant in the message. Object.const_get(camel_cased_word) if names.empty? # Remove the first blank element in case of '::ClassName' notation. @@ -234,8 +264,8 @@ module ActiveSupport next candidate if constant.const_defined?(name, false) next candidate unless Object.const_defined?(name) - # Go down the ancestors to check it it's owned - # directly before we reach Object or the end of ancestors. + # Go down the ancestors to check if it is owned directly. The check + # stops when we reach Object or the end of ancestors tree. constant = constant.ancestors.inject do |const, ancestor| break const if ancestor == Object break ancestor if ancestor.const_defined?(name, false) @@ -273,8 +303,8 @@ module ActiveSupport def safe_constantize(camel_cased_word) constantize(camel_cased_word) rescue NameError => e - raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ || - e.name.to_s == camel_cased_word.to_s + raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) || + e.name.to_s == camel_cased_word.to_s) rescue ArgumentError => e raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/ end @@ -318,10 +348,11 @@ module ActiveSupport private - # Mount a regular expression that will match part by part of the constant. + # Mounts a regular expression, returned as a string to ease interpolation, + # that will match part by part the given constant. # - # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/ - # const_regexp("::") # => /::/ + # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?" + # const_regexp("::") # => "::" def const_regexp(camel_cased_word) #:nodoc: parts = camel_cased_word.split("::") diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 30833a4cb1..8b5fc70dee 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -7,14 +7,24 @@ module ActiveSupport mattr_accessor :parse_json_times module JSON + # matches YAML-formatted dates + DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ + class << self # Parses a JSON string (JavaScript Object Notation) into a hash. # See www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, proc = nil, options = {}) - data = ::JSON.load(json, proc, options) + def decode(json, options = {}) + if options.present? + raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \ + "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \ + "and has been removed." + end + + data = ::JSON.parse(json, quirks_mode: true) + if ActiveSupport.parse_json_times convert_dates_from(data) else diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 77b5d8d227..f29d42276d 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,344 +1,172 @@ -#encoding: us-ascii - -require 'active_support/core_ext/object/to_json' +require 'active_support/core_ext/object/json' require 'active_support/core_ext/module/delegation' -require 'bigdecimal' -require 'active_support/core_ext/big_decimal/conversions' # for #to_s -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/object/instance_variables' -require 'time' -require 'active_support/core_ext/time/conversions' -require 'active_support/core_ext/date_time/conversions' -require 'active_support/core_ext/date/conversions' -require 'set' - module ActiveSupport class << self delegate :use_standard_json_time_format, :use_standard_json_time_format=, + :time_precision, :time_precision=, :escape_html_entities_in_json, :escape_html_entities_in_json=, :encode_big_decimal_as_string, :encode_big_decimal_as_string=, + :json_encoder, :json_encoder=, :to => :'ActiveSupport::JSON::Encoding' end module JSON - # matches YAML-formatted dates - DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ - # Dumps objects in JSON (JavaScript Object Notation). # See www.json.org for more info. # # ActiveSupport::JSON.encode({ team: 'rails', players: '36' }) # # => "{\"team\":\"rails\",\"players\":\"36\"}" def self.encode(value, options = nil) - Encoding::Encoder.new(options).encode(value) + Encoding.json_encoder.new(options).encode(value) end module Encoding #:nodoc: - class CircularReferenceError < StandardError; end - - class Encoder + class JSONGemEncoder #:nodoc: attr_reader :options def initialize(options = nil) @options = options || {} - @seen = Set.new end - def encode(value, use_options = true) - check_for_circular_references(value) do - jsonified = use_options ? value.as_json(options_for(value)) : value.as_json - jsonified.encode_json(self) - end + # Encode the given object into a JSON string + def encode(value) + stringify jsonify value.as_json(options.dup) end - # like encode, but only calls as_json, without encoding to string. - def as_json(value, use_options = true) - check_for_circular_references(value) do - use_options ? value.as_json(options_for(value)) : value.as_json + private + # Rails does more escaping than the JSON gem natively does (we + # escape \u2028 and \u2029 and optionally >, <, & to work around + # certain browser problems). + ESCAPED_CHARS = { + "\u2028" => '\u2028', + "\u2029" => '\u2029', + '>' => '\u003e', + '<' => '\u003c', + '&' => '\u0026', + } + + ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u + ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u + + # This class wraps all the strings we see and does the extra escaping + class EscapedString < String #:nodoc: + def to_json(*) + if Encoding.escape_html_entities_in_json + super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS + else + super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS + end + end end - end - def options_for(value) - if value.is_a?(Array) || value.is_a?(Hash) - # hashes and arrays need to get encoder in the options, so that - # they can detect circular references. - options.merge(:encoder => self) - else - options.dup + # Mark these as private so we don't leak encoding-specific constructs + private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES, + :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString + + # Convert an object into a "JSON-ready" representation composed of + # primitives like Hash, Array, String, Numeric, and true/false/nil. + # Recursively calls #as_json to the object to recursively build a + # fully JSON-ready object. + # + # This allows developers to implement #as_json without having to + # worry about what base types of objects they are allowed to return + # or having to remember to call #as_json recursively. + # + # Note: the +options+ hash passed to +object.to_json+ is only passed + # to +object.as_json+, not any of this method's recursive +#as_json+ + # calls. + def jsonify(value) + case value + when String + EscapedString.new(value) + when Numeric, NilClass, TrueClass, FalseClass + value + when Hash + Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }] + when Array + value.map { |v| jsonify(v) } + else + jsonify value.as_json + end end - end - - def escape(string) - Encoding.escape(string) - end - private - def check_for_circular_references(value) - unless @seen.add?(value.__id__) - raise CircularReferenceError, 'object references itself' - end - yield - ensure - @seen.delete(value.__id__) + # Encode a "jsonified" Ruby data structure using the JSON gem + def stringify(jsonified) + ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false) end end - - ESCAPED_CHARS = { - "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002', - "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005', - "\x06" => '\u0006', "\x07" => '\u0007', "\x0B" => '\u000B', - "\x0E" => '\u000E', "\x0F" => '\u000F', "\x10" => '\u0010', - "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013', - "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016', - "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019', - "\x1A" => '\u001A', "\x1B" => '\u001B', "\x1C" => '\u001C', - "\x1D" => '\u001D', "\x1E" => '\u001E', "\x1F" => '\u001F', - "\010" => '\b', - "\f" => '\f', - "\n" => '\n', - "\xe2\x80\xa8" => '\u2028', - "\xe2\x80\xa9" => '\u2029', - "\r" => '\r', - "\t" => '\t', - '"' => '\"', - '\\' => '\\\\', - '>' => '\u003E', - '<' => '\u003C', - '&' => '\u0026', - "#{0xe2.chr}#{0x80.chr}#{0xa8.chr}" => '\u2028', - "#{0xe2.chr}#{0x80.chr}#{0xa9.chr}" => '\u2029', - } - class << self # If true, use ISO 8601 format for dates and times. Otherwise, fall back # to the Active Support legacy format. attr_accessor :use_standard_json_time_format - # If false, serializes BigDecimal objects as numeric instead of wrapping - # them in a string. - attr_accessor :encode_big_decimal_as_string - - attr_accessor :escape_regex - attr_reader :escape_html_entities_in_json - - def escape_html_entities_in_json=(value) - self.escape_regex = \ - if @escape_html_entities_in_json = value - /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\><&]/ - else - /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\]/ - end - end - - 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 - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class FalseClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end - -class NilClass - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - 'null' - end -end - -class String - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - encoder.escape(self) - end -end - -class Symbol - def as_json(options = nil) #:nodoc: - to_s - end -end - -class Numeric - def as_json(options = nil) #:nodoc: - self - end - - def encode_json(encoder) #:nodoc: - to_s - end -end + # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e) + # as a safety measure. + attr_accessor :escape_html_entities_in_json -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 + # Sets the precision of encoded time values. + # Defaults to 3 (equivalent to millisecond precision) + attr_accessor :time_precision -class BigDecimal - # A BigDecimal would be naturally represented as a JSON number. Most libraries, - # however, parse non-integer JSON numbers directly as floats. Clients using - # those libraries would get in general a wrong number and no way to recover - # other than manually inspecting the string with the JSON code itself. - # - # That's why a JSON string is returned. The JSON literal is not numeric, but - # if the other end knows by contract that the data is supposed to be a - # BigDecimal, it still has the chance to post-process the string and get the - # real value. - # - # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to - # override this behavior. - def as_json(options = nil) #:nodoc: - if finite? - ActiveSupport.encode_big_decimal_as_string ? to_s : self - else - nil - end - end -end + # 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 Regexp - def as_json(options = nil) #:nodoc: - to_s - end -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." -module Enumerable - def as_json(options = nil) #:nodoc: - to_a.as_json(options) - end -end + raise NotImplementedError, message + end -class Range - def as_json(options = nil) #:nodoc: - to_s - 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 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 + ActiveSupport::Deprecation.warn message - 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/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 598c46bce5..51d2da3a79 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -57,18 +57,16 @@ module ActiveSupport # secret they've provided is at least 30 characters in length. def ensure_secret_secure(secret) if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_key_base = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/initializers/secret_token.rb" + raise ArgumentError, "A secret is required to generate an integrity hash " \ + "for cookie session data. Set a secret_key_base of at least " \ + "#{SECRET_MIN_LENGTH} characters in config/secrets.yml." end if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" + raise ArgumentError, "Secret should be something secure, " \ + "like \"#{SecureRandom.hex(16)}\". The value you " \ + "provided, \"#{secret}\", is shorter than the minimum length " \ + "of #{SECRET_MIN_LENGTH} characters." end end end diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index 4a55bbb350..33fccdcf95 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -1,4 +1,4 @@ -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/logger_silence' require 'logger' diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index bffdfc6201..92ab6fe648 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -12,7 +12,7 @@ module ActiveSupport # This can be used in situations similar to the <tt>MessageVerifier</tt>, but # where you don't want users to be able to determine the value of the payload. # - # salt = SecureRandom.random_bytes(64) + # salt = SecureRandom.random_bytes(64) # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...> # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." @@ -40,6 +40,7 @@ module ActiveSupport # Options: # * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by # <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'. + # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+. # * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+. def initialize(secret, *signature_key_or_options) options = signature_key_or_options.extract_options! @@ -47,7 +48,7 @@ module ActiveSupport @secret = secret @sign_secret = sign_secret @cipher = options[:cipher] || 'aes-256-cbc' - @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer) + @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer) @serializer = options[:serializer] || Marshal end @@ -76,12 +77,12 @@ module ActiveSupport encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final - [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--") + "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}" end def _decrypt(encrypted_message) cipher = new_cipher - encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.decode64(v)} + encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)} cipher.decrypt cipher.key = @secret @@ -91,7 +92,7 @@ module ActiveSupport decrypted_data << cipher.final @serializer.load(decrypted_data) - rescue OpenSSLCipherError, TypeError + rescue OpenSSLCipherError, TypeError, ArgumentError raise InvalidMessage end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index e0cd92ae3c..6cb2884fb7 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -27,6 +27,7 @@ module ActiveSupport class InvalidSignature < StandardError; end def initialize(secret, options = {}) + raise ArgumentError, 'Secret should not be nil.' unless secret @secret = secret @digest = options[:digest] || 'SHA1' @serializer = options[:serializer] || Marshal @@ -37,7 +38,12 @@ module ActiveSupport data, digest = signed_message.split("--") if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) - @serializer.load(::Base64.decode64(data)) + begin + @serializer.load(::Base64.strict_decode64(data)) + rescue ArgumentError => argument_error + raise InvalidSignature if argument_error.message =~ %r{invalid base64} + raise + end else raise InvalidSignature end diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb index a42e7f6542..3c0cf9f137 100644 --- a/activesupport/lib/active_support/multibyte/chars.rb +++ b/activesupport/lib/active_support/multibyte/chars.rb @@ -56,11 +56,10 @@ module ActiveSupport #:nodoc: # Forward all undefined methods to the wrapped string. def method_missing(method, *args, &block) + result = @wrapped_string.__send__(method, *args, &block) if method.to_s =~ /!$/ - result = @wrapped_string.__send__(method, *args, &block) self if result else - result = @wrapped_string.__send__(method, *args, &block) result.kind_of?(String) ? chars(result) : result end end diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 04e6b71580..7d45961515 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -11,7 +11,7 @@ module ActiveSupport NORMALIZATION_FORMS = [:c, :kc, :d, :kd] # The Unicode version that is supported by the implementation - UNICODE_VERSION = '6.2.0' + UNICODE_VERSION = '6.3.0' # The default normalization used for operations that require # normalization. It can be set to any of the normalizations @@ -42,7 +42,6 @@ module ActiveSupport 0x0085, # White_Space # Cc <control-0085> 0x00A0, # White_Space # Zs NO-BREAK SPACE 0x1680, # White_Space # Zs OGHAM SPACE MARK - 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR (0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE 0x2028, # White_Space # Zl LINE SEPARATOR 0x2029, # White_Space # Zp PARAGRAPH SEPARATOR @@ -212,37 +211,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) - return string if string.empty? - - if force - return string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace) + # 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 - # We can't transcode to the same format, so we choose a nearly-identical encoding. - # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to - # CP1252 when we get errors. The final string will be 'converted' back to UTF-8 - # before returning. - reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC) - - source = string.dup - out = ''.force_encoding(Encoding::UTF_8_MAC) + reader.finish - loop do - reader.primitive_convert(source, out) - _, _, _, error_bytes, _ = reader.primitive_errinfo - break if error_bytes.nil? - out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace) + out.encode!(Encoding::UTF_8) end - - reader.finish - - out.encode!(Encoding::UTF_8) end # Returns the KC normalization of the string by default. NFKC is @@ -287,6 +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 @@ -322,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 @@ -354,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 @@ -364,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 b32aa75e59..325a3d75dc 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -141,6 +141,11 @@ module ActiveSupport # # ActiveSupport::Notifications.unsubscribe(subscriber) # + # You can also unsubscribe by passing the name of the subscriber object. Note + # that this will unsubscribe all subscriptions with the given name: + # + # ActiveSupport::Notifications.unsubscribe("render") + # # == Default Queue # # Notifications ships with a queue implementation that consumes and publishes events @@ -173,12 +178,12 @@ module ActiveSupport unsubscribe(subscriber) end - def unsubscribe(args) - notifier.unsubscribe(args) + def unsubscribe(subscriber_or_name) + notifier.unsubscribe(subscriber_or_name) end def instrumenter - InstrumentationRegistry.instrumenter_for(notifier) + InstrumentationRegistry.instance.instrumenter_for(notifier) end end diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb index 99fe03e6d0..6bf8c7d5de 100644 --- a/activesupport/lib/active_support/notifications/fanout.rb +++ b/activesupport/lib/active_support/notifications/fanout.rb @@ -25,9 +25,15 @@ module ActiveSupport subscriber end - def unsubscribe(subscriber) + def unsubscribe(subscriber_or_name) synchronize do - @subscribers.reject! { |s| s.matches?(subscriber) } + case subscriber_or_name + when String + @subscribers.reject! { |s| s.matches?(subscriber_or_name) } + else + @subscribers.delete(subscriber_or_name) + end + @listeners_for.clear end end @@ -97,31 +103,27 @@ module ActiveSupport end def subscribed_to?(name) - @pattern === name.to_s + @pattern === name end - def matches?(subscriber_or_name) - self === subscriber_or_name || - @pattern && @pattern === subscriber_or_name + def matches?(name) + @pattern && @pattern === name end end class Timed < Evented - def initialize(pattern, delegate) - @timestack = [] - super - end - def publish(name, *args) @delegate.call name, *args end def start(name, id, payload) - @timestack.push Time.now + timestack = Thread.current[:_timestack] ||= [] + timestack.push Time.now end def finish(name, id, payload) - started = @timestack.pop + timestack = Thread.current[:_timestack] + started = timestack.pop @delegate.call(name, started, Time.now, id, payload) end end diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb index 0c9a729ce5..3a244b34b5 100644 --- a/activesupport/lib/active_support/notifications/instrumenter.rb +++ b/activesupport/lib/active_support/notifications/instrumenter.rb @@ -54,10 +54,11 @@ module ActiveSupport @transaction_id = transaction_id @end = ending @children = [] + @duration = nil end 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 54cff6d394..5ecda9593a 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 @@ -253,14 +119,7 @@ module ActiveSupport # 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.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,39 +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 - multiplier = 10 ** (digits - precision) - rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier - digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed - end - precision -= digits - precision = 0 if precision < 0 # don't let it be negative - else - rounded_number = BigDecimal.new(number.to_s).round(precision).to_f - rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros - end - formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options) - 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 @@ -413,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 @@ -460,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 @@ -515,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 # @@ -550,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..9ae27a896a --- /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..9a3dc526ae --- /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 { |e| -e } + 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..eafe2844f7 --- /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 e03bb4ca0f..a33e2c58a9 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -41,7 +41,7 @@ module ActiveSupport end # +InheritableOptions+ provides a constructor to build an +OrderedOptions+ - # hash inherited from the another hash. + # hash inherited from another hash. # # Use this if you already have some hash and you want to create a new one based on it. # diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index aa682fb36c..ca2e4d5625 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -32,21 +32,22 @@ module ActiveSupport # # If the class has an initializer, it must accept no arguments. module PerThreadRegistry - protected + def self.extended(object) + object.instance_variable_set '@per_thread_registry_key', object.name.freeze + end + + def instance + Thread.current[@per_thread_registry_key] ||= new + end + protected def method_missing(name, *args, &block) # :nodoc: # Caches the method definition as a singleton method of the receiver. define_singleton_method(name) do |*a, &b| - per_thread_registry_instance.public_send(name, *a, &b) + instance.public_send(name, *a, &b) end send(name, *args, &block) end - - private - - def per_thread_registry_instance - Thread.current[name] ||= new - end end end diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb index 34c6f900c1..98be78b41b 100644 --- a/activesupport/lib/active_support/subscriber.rb +++ b/activesupport/lib/active_support/subscriber.rb @@ -31,22 +31,54 @@ module ActiveSupport # Attach the subscriber to a namespace. def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications) + @namespace = namespace + @subscriber = subscriber + @notifier = notifier + subscribers << subscriber + # Add event subscribers for all existing methods on the class. subscriber.public_methods(false).each do |event| - next if %w{ start finish }.include?(event.to_s) + add_event_subscriber(event) + end + end - notifier.subscribe("#{event}.#{namespace}", subscriber) + # Adds event subscribers for all new methods added to the class. + def method_added(event) + # Only public methods are added as subscribers, and only if a notifier + # has been set up. This means that subscribers will only be set up for + # classes that call #attach_to. + if public_method_defined?(event) && notifier + add_event_subscriber(event) end end def subscribers @@subscribers ||= [] end + + protected + + attr_reader :subscriber, :notifier, :namespace + + def add_event_subscriber(event) + return if %w{ start finish }.include?(event.to_s) + + pattern = "#{event}.#{namespace}" + + # don't add multiple subscribers (eg. if methods are redefined) + return if subscriber.patterns.include?(pattern) + + subscriber.patterns << pattern + notifier.subscribe(pattern, subscriber) + end end + attr_reader :patterns # :nodoc: + def initialize @queue_key = [self.class.name, object_id].join "-" + @patterns = [] super end @@ -71,7 +103,7 @@ module ActiveSupport private def event_stack - SubscriberQueueRegistry.get_queue(@queue_key) + SubscriberQueueRegistry.instance.get_queue(@queue_key) end end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index 18bc919734..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 2f27aff2bd..4c3e77b7fd 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -7,52 +7,54 @@ require 'active_support/testing/deprecation' require 'active_support/testing/declarative' require 'active_support/testing/isolation' require 'active_support/testing/constant_lookup' +require 'active_support/testing/time_helpers' require 'active_support/core_ext/kernel/reporting' require 'active_support/deprecation' -begin - silence_warnings { require 'mocha/setup' } -rescue LoadError -end - -module Minitest # :nodoc: - class << self - remove_method :__run - end +module ActiveSupport + class TestCase < ::Minitest::Test + Assertion = Minitest::Assertion - def self.__run reporter, options # :nodoc: - # FIXME: MT5's runnables is not ordered. This is needed because - # we have have tests have cross-class order-dependent bugs. - suites = Runnable.runnables.sort_by { |ts| ts.name.to_s } + class << self + def test_order=(new_order) + ActiveSupport.test_order = new_order + end - parallel, serial = suites.partition { |s| s.test_order == :parallel } + def test_order + test_order = ActiveSupport.test_order - ParallelEach.new(parallel).map { |suite| suite.run reporter, options } + - serial.map { |suite| suite.run reporter, options } - end -end + if test_order.nil? + ActiveSupport::Deprecation.warn "You did not specify a value for the " \ + "configuration option 'active_support.test_order'. In Rails 5.0, " \ + "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`." -module ActiveSupport - class TestCase < ::Minitest::Test - Assertion = Minitest::Assertion + test_order = :sorted + self.test_order = test_order + end - alias_method :method_name, :name + test_order + end - $tags = {} - def self.for_tag(tag) - yield if $tags[tag] + alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent! end - # FIXME: we have tests that depend on run order, we should fix that and - # remove this method. - def self.test_order # :nodoc: - :sorted - end + alias_method :method_name, :name include ActiveSupport::Testing::TaggedLogging include ActiveSupport::Testing::SetupAndTeardown include ActiveSupport::Testing::Assertions include ActiveSupport::Testing::Deprecation + include ActiveSupport::Testing::TimeHelpers extend ActiveSupport::Testing::Declarative # test/unit backwards compatibility methods diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 76a591bc3b..11cca82995 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. # diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb index 1b2a75c35d..07d477c0db 100644 --- a/activesupport/lib/active_support/testing/constant_lookup.rb +++ b/activesupport/lib/active_support/testing/constant_lookup.rb @@ -36,12 +36,8 @@ module ActiveSupport while names.size > 0 do names.last.sub!(/Test$/, "") begin - constant = names.join("::").constantize + constant = names.join("::").safe_constantize break(constant) if yield(constant) - rescue NoMethodError # subclass of NameError - raise - rescue NameError - # Constant wasn't found, move on ensure names.pop end diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index 1fa73caefa..0bf3643a56 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -1,23 +1,6 @@ module ActiveSupport module Testing module Declarative - - def self.extended(klass) #:nodoc: - klass.class_eval do - - unless method_defined?(:describe) - def self.describe(text) - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.name - "#{text}" - end - RUBY_EVAL - end - end - - end - end - unless defined?(Spec) # Helper to define a test method using a String. Under the hood, it replaces # spaces with underscores and defines the test method. @@ -27,7 +10,7 @@ module ActiveSupport # end def test(name, &block) test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym - defined = instance_method(test_name) rescue false + defined = method_defined? test_name raise "#{test_name} is already defined in #{self}" if defined if block_given? define_method(test_name, &block) diff --git a/activesupport/lib/active_support/testing/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb index a8342904dc..6c94c611b6 100644 --- a/activesupport/lib/active_support/testing/deprecation.rb +++ b/activesupport/lib/active_support/testing/deprecation.rb @@ -19,18 +19,17 @@ module ActiveSupport result end - private - def collect_deprecations - old_behavior = ActiveSupport::Deprecation.behavior - deprecations = [] - ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| - deprecations << message - end - result = yield - [result, deprecations] - ensure - ActiveSupport::Deprecation.behavior = old_behavior + def collect_deprecations + old_behavior = ActiveSupport::Deprecation.behavior + deprecations = [] + ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack| + deprecations << message end + result = yield + [result, deprecations] + ensure + ActiveSupport::Deprecation.behavior = old_behavior + end end end end diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index d5d31cecbe..68bda35980 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -1,5 +1,4 @@ require 'rbconfig' -require 'minitest/parallel_each' module ActiveSupport module Testing @@ -7,11 +6,9 @@ module ActiveSupport require 'thread' def self.included(klass) #:nodoc: - klass.extend(Module.new { - def test_methods - ParallelEach.new super - end - }) + klass.class_eval do + parallelize_me! + end end def self.forking_env? @@ -40,6 +37,8 @@ module ActiveSupport module Forking def run_in_isolation(&blk) read, write = IO.pipe + read.binmode + write.binmode pid = fork do read.close @@ -71,14 +70,24 @@ module ActiveSupport exit! else Tempfile.open("isolation") do |tmpfile| - ENV["ISOLATION_TEST"] = self.class.name - ENV["ISOLATION_OUTPUT"] = tmpfile.path + env = { + ISOLATION_TEST: self.class.name, + ISOLATION_OUTPUT: tmpfile.path + } load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ") - `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}` - - ENV.delete("ISOLATION_TEST") - ENV.delete("ISOLATION_OUTPUT") + orig_args = ORIG_ARGV.join(" ") + test_opts = "-n#{self.class.name}##{self.name}" + command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}" + + # IO.popen lets us pass env in a cross-platform way + child = IO.popen([env, command]) + + begin + Process.wait(child.pid) + rescue Errno::ECHILD # The child process may exit before we wait + nil + end return tmpfile.read.unpack("m")[0] end diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb index f4cee64091..843ce4a867 100644 --- a/activesupport/lib/active_support/testing/tagged_logging.rb +++ b/activesupport/lib/active_support/testing/tagged_logging.rb @@ -6,7 +6,7 @@ module ActiveSupport attr_writer :tagged_logger def before_setup - if tagged_logger + if tagged_logger && tagged_logger.info? heading = "#{self.class}: #{name}" divider = '-' * heading.size tagged_logger.info divider diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb new file mode 100644 index 0000000000..1112c6e0b1 --- /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, &block) + 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 + block.call + ensure + travel_back + end + end + end + + # Returns the current time back to its original state, by removing the stubs added by + # `travel` and `travel_to`. + # + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 + # travel_back + # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 + def travel_back + simple_stubs.unstub_all! + end + + private + + def simple_stubs + @simple_stubs ||= SimpleStubs.new + end + end + end +end diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb index 92a593965e..ea2d3391bd 100644 --- a/activesupport/lib/active_support/time.rb +++ b/activesupport/lib/active_support/time.rb @@ -1,5 +1,3 @@ -require 'active_support' - module ActiveSupport autoload :Duration, 'active_support/duration' autoload :TimeWithZone, 'active_support/time_with_zone' diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 95b9b8e5ae..4a0ed356b1 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+. @@ -132,8 +132,8 @@ module ActiveSupport end def xmlschema(fraction_digits = 0) - fraction = if fraction_digits > 0 - (".%06i" % time.usec)[0, fraction_digits + 1] + fraction = if fraction_digits.to_i > 0 + (".%06i" % time.usec)[0, fraction_digits.to_i + 1] end "#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}" @@ -146,15 +146,15 @@ module ActiveSupport # to +false+. # # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true - # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json - # # => "2005-02-01T15:15:10Z" + # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json + # # => "2005-02-01T05:15:10.000-10:00" # # # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false - # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json - # # => "2005/02/01 15:15:10 +0000" + # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json + # # => "2005/02/01 05:15:10 -1000" def as_json(options = nil) if ActiveSupport::JSON::Encoding.use_standard_json_time_format - xmlschema(3) + xmlschema(ActiveSupport::JSON::Encoding.time_precision) else %(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)}) end @@ -185,8 +185,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) @@ -259,7 +262,7 @@ module ActiveSupport # 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 @@ -350,6 +353,14 @@ module ActiveSupport initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc) end + # respond_to_missing? is not called in some cases, such as when type conversion is + # performed with Kernel#String + def respond_to?(sym, include_priv = false) + # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow + return false if sym.to_sym == :to_str + super + end + # Ensure proxy class responds to all methods that underlying time instance # responds to. def respond_to_missing?(sym, include_priv) @@ -367,12 +378,12 @@ module ActiveSupport end private - def get_period_and_ensure_valid_local_time + def get_period_and_ensure_valid_local_time(period) # we don't want a Time.local instance enforcing its own DST rules as well, # so transfer time values to a utc constructor if necessary @time = transfer_time_values_to_utc_constructor(@time) unless @time.utc? begin - @time_zone.period_for_local(@time) + period || @time_zone.period_for_local(@time) rescue ::TZInfo::PeriodNotFound # time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again @time += 1.hour @@ -390,7 +401,8 @@ module ActiveSupport def wrap_with_time_zone(time) if time.acts_like?(:time) - self.class.new(nil, time_zone, time) + periods = time_zone.periods_for_local(time) + self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil) elsif time.is_a?(Range) wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end) else diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 3cf82a24b9..49dfee96ec 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' @@ -184,16 +186,74 @@ module ActiveSupport UTC_OFFSET_WITH_COLON = '%s%02d:%02d' UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '') - # 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 { |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 end include Comparable @@ -205,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) @@ -218,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 @@ -232,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 @@ -279,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), @@ -314,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. @@ -339,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 2571faa019..394ee95f4b 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 8762330a6e..fe03984546 100644 --- a/activesupport/lib/active_support/version.rb +++ b/activesupport/lib/active_support/version.rb @@ -1,11 +1,8 @@ +require_relative 'gem_version' + module ActiveSupport - # Returns the version of the currently loaded ActiveSupport as a Gem::Version + # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt> def self.version - Gem::Version.new "4.1.0.beta" - end - - module VERSION #:nodoc: - MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments - STRING = ActiveSupport.version.to_s + gem_version end end diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb index d082a0a499..009ee4db90 100644 --- a/activesupport/lib/active_support/xml_mini.rb +++ b/activesupport/lib/active_support/xml_mini.rb @@ -1,7 +1,9 @@ require 'time' require 'base64' +require 'bigdecimal' require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/string/inflections' +require 'active_support/core_ext/date_time/calculations' module ActiveSupport # = XmlMini @@ -56,13 +58,13 @@ module ActiveSupport # TODO use regexp instead of Date.parse unless defined?(PARSING) PARSING = { - "symbol" => Proc.new { |symbol| symbol.to_sym }, + "symbol" => Proc.new { |symbol| symbol.to_s.to_sym }, "date" => Proc.new { |date| ::Date.parse(date) }, "datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc }, "integer" => Proc.new { |integer| integer.to_i }, "float" => Proc.new { |float| float.to_f }, "decimal" => Proc.new { |number| BigDecimal(number) }, - "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) }, + "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) }, "string" => Proc.new { |string| string.to_s }, "yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml }, "base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) }, |