diff options
Diffstat (limited to 'activesupport')
62 files changed, 1255 insertions, 871 deletions
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 104b5eafa1..994e44db00 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,8 +1,76 @@ +* Fix file descriptor being leaked on each call to `Kernel.silence_stream`. + + *Mario Visic* + +* Ensure `config.i18n.enforce_available_locales` is set before any other + configuration option. + + *Yves Senn* + +* Added `Date#all_week/month/quarter/year` for generating date ranges. + + *Dmitriy Meremyanin* + +* Add `Time.zone.yesterday` and `Time.zone.tomorrow`. These follow the + behavior of Ruby's `Date.yesterday` and `Date.tomorrow` but return localized + versions, similar to how `Time.zone.today` has returned a localized version + of `Date.today`. + + *Colin Bartlett* + +* Show valid keys when `assert_valid_keys` raises an exception, and show the + wrong value as it was entered. + + *Gonzalo Rodríguez-Baltanás Díaz* + +* Both `cattr_*` and `mattr_*` method definitions now live in `active_support/core_ext/module/attribute_accessors`. + + Requires to `active_support/core_ext/class/attribute_accessors` are + deprecated and will be removed in Ruby on Rails 4.2. + + *Genadi Samokovarov* + +* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to explicitly + convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` + + This will help to catch subtle bugs like: + + def recent?(days = 3) + self.created_at >= days.ago + end + + The above code would check if the model is created within the last 3 **seconds**. + + In the future, `Numeric#{ago,until,since,from_now}` should be removed completely, + or throw some sort of errors to indicate there are no implicit conversion from + Numeric to AS::Duration. + + *Godfrey Chan* + +* Requires JSON gem version 1.7.7 or above due to a security issue in older versions. + + *Godfrey Chan* + +* Removed the old pure-Ruby JSON encoder and switched to a new encoder based on the built-in JSON + gem. + + Support for encoding `BigDecimal` as a JSON number, as well as defining custom `encode_json` + methods to control the JSON output has been **removed from core**. The new encoder will always + encode BigDecimals as `String`s and ignore any custom `encode_json` methods. + + The old encoder has been extracted into the `activesupport-json_encoder` gem. Installing that + gem will bring back the ability to encode `BigDecimal`s as numbers as well as `encode_json` + support. + + Setting the related configuration `ActiveSupport.encode_big_decimal_as_string` without the + `activesupport-json_encoder` gem installed will raise an error. + + *Godfrey Chan* + * Add `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These methods change current time to the given time or time difference by stubbing `Time.now` and `Date.today` to return the time or date after the difference calculation, or the time or date that got passed into the - method respectively. These methods also accept a block, which will return current time back to - its original state at the end of the block. + method respectively. Example for `#travel`: @@ -157,7 +225,8 @@ *Simon Coffey* -* Add String#remove(pattern) as a short-hand for the common pattern of String#gsub(pattern, '') +* Add `String#remove(pattern)` as a short-hand for the common pattern of + `String#gsub(pattern, '')`. *DHH* @@ -260,11 +329,12 @@ *Carlos Antonio da Silva* -* Remove deprecated `BufferedLogger`. +* Remove deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead. *Yves Senn* -* Remove deprecated `assert_present` and `assert_blank` methods. +* Remove deprecated `assert_present` and `assert_blank` methods, use `assert + object.blank?` and `assert object.present?` instead. *Yves Senn* @@ -305,6 +375,10 @@ *Yves Senn* +* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. + + *Toshinori Kajihara* + * Raise an error when multiple `included` blocks are defined for a Concern. The old behavior would silently discard previously defined blocks, running only the last one. diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec index c27c50e47b..0427022dc6 100644 --- a/activesupport/activesupport.gemspec +++ b/activesupport/activesupport.gemspec @@ -20,9 +20,9 @@ Gem::Specification.new do |s| s.rdoc_options.concat ['--encoding', 'UTF-8'] - s.add_dependency('i18n', '~> 0.6', '>= 0.6.4') - s.add_dependency 'json', '~> 1.7' + s.add_dependency 'i18n', '~> 0.6', '>= 0.6.4' + s.add_dependency 'json', '~> 1.7', '>= 1.7.7' s.add_dependency 'tzinfo', '~> 1.1' - s.add_dependency 'minitest', '~> 5.0' + s.add_dependency 'minitest', '~> 5.1' s.add_dependency 'thread_safe','~> 0.1' end diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 5e1fe9e556..a40c6b559c 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -52,6 +52,7 @@ module ActiveSupport autoload :MessageEncryptor autoload :MessageVerifier autoload :Multibyte + autoload :NumberHelper autoload :OptionMerger autoload :OrderedHash autoload :OrderedOptions @@ -63,6 +64,12 @@ module ActiveSupport autoload :Rescuable autoload :SafeBuffer, "active_support/core_ext/string/output_safety" autoload :TestCase + + def self.eager_load! + super + + NumberHelper.eager_load! + end end autoload :I18n, "active_support/i18n" diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 5c1d473161..d584d50da8 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -3,7 +3,7 @@ require 'zlib' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/benchmark' -require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/numeric/bytes' require 'active_support/core_ext/numeric/time' require 'active_support/core_ext/object/to_param' diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index 5cd6065077..5795d86a42 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -30,7 +30,7 @@ module ActiveSupport 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) search_dir(cache_path) do |fname| diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb index 34ac91334a..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 diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index c3aac31323..3052e5011c 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+). @@ -89,7 +89,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) @@ -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 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/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_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb index 0cf955b889..083b165dce 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb @@ -1,181 +1,6 @@ -require 'active_support/core_ext/array/extract_options' +require 'active_support/deprecation' +require 'active_support/core_ext/module/attribute_accessors' -# 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 - # - # Also, you can pass a block to set up the attribute with a default value. - # - # class Person - # cattr_reader :hair_colors do - # [:brown, :black, :blonde, :red] - # end - # end - # - # Person.hair_colors # => [:brown, :black, :blonde, :red] - 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 - class_variable_set("@@#{sym}", yield) if block_given? - 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 +ActiveSupport::Deprecation.warn( + "The cattr_* method definitions have been moved into active_support/core_ext/module/attribute_accessors. Please require that instead." +) diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb index 6bc8f12176..09504a4d9c 100644 --- a/activesupport/lib/active_support/core_ext/date/conversions.rb +++ b/activesupport/lib/active_support/core_ext/date/conversions.rb @@ -37,12 +37,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/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb index f35c2be4c2..93e716585b 100644 --- a/activesupport/lib/active_support/core_ext/hash/keys.rb +++ b/activesupport/lib/active_support/core_ext/hash/keys.rb @@ -61,13 +61,15 @@ class Hash # 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. # - # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years" - # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: name" + # { name: 'Rob', 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 diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb index df11737a6b..3b5e205244 100644 --- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb +++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb @@ -48,6 +48,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. 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..f70a839074 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_acessor: 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/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 182e74d9e1..58146cdf7a 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] diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb index d97ce938c1..704c4248d9 100644 --- a/activesupport/lib/active_support/core_ext/numeric/time.rb +++ b/activesupport/lib/active_support/core_ext/numeric/time.rb @@ -63,6 +63,7 @@ class Numeric # Reads best without arguments: 10.minutes.ago def ago(time = ::Time.current) + ActiveSupport::Deprecation.warn "Calling #ago or #until on a number (e.g. 5.ago) is deprecated and will be removed in the future, use 5.seconds.ago instead" time - self end @@ -71,6 +72,7 @@ class Numeric # Reads best with argument: 10.minutes.since(time) def since(time = ::Time.current) + ActiveSupport::Deprecation.warn "Calling #since or #from_now on a number (e.g. 5.since) is deprecated and will be removed in the future, use 5.seconds.since instead" time + self end diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index 7c11d44f57..1675145ffe 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -62,40 +62,24 @@ 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 @@ -108,10 +92,6 @@ class Numeric def as_json(options = nil) #:nodoc: self end - - def encode_json(encoder) #:nodoc: - to_s - end end class Float @@ -132,15 +112,8 @@ class BigDecimal # 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.encode_big_decimal_as_string = 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 + finite? ? to_s : nil end end @@ -166,10 +139,6 @@ class Array def as_json(options = nil) #:nodoc: map { |v| options ? v.as_json(options.dup) : v.as_json } end - - def encode_json(encoder) #:nodoc: - "[#{map { |v| v.as_json.encode_json(encoder) } * ','}]" - end end class Hash @@ -189,10 +158,6 @@ class Hash Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }] end - - def encode_json(encoder) #:nodoc: - "{#{map { |k,v| "#{k.as_json.encode_json(encoder)}:#{v.as_json.encode_json(encoder)}" } * ','}}" - end end class Time @@ -225,7 +190,7 @@ class DateTime end end -class Process::Status +class Process::Status #:nodoc: def as_json(options = nil) { :exitstatus => exitstatus, :pid => pid } end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index b7b750c77b..cf9b1a4ec0 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -182,10 +182,6 @@ 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 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..1b2098fc84 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -4,9 +4,10 @@ require 'active_support/core_ext/kernel/singleton_class' class ERB module Util HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' } - JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' } + JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' } + HTML_ESCAPE_REGEXP = /[&"'><]/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ - JSON_ESCAPE_REGEXP = /[&"><]/ + JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u # A utility method for escaping HTML tag characters. # This method is also aliased as <tt>h</tt>. @@ -21,7 +22,7 @@ class ERB if s.html_safe? s else - s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe + s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE).html_safe end end @@ -48,17 +49,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_escape('is a > 0 & a < 10?') - # # => is a \u003E 0 \u0026 a \u003C 10? + # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"}) + # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}" # - # Note that after this operation is performed the output is not - # valid JSON. In particular double quotes are removed: + # json_escape(json) + # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}" + # + # JSON.parse(json) == JSON.parse(json_escape(json)) + # # => true + # + # The intended use case for this method is to escape JSON strings before including + # them inside a script tag to avoid XSS vulnerability: + # + # <script> + # var currentUser = <%= json_escape current_user.to_json %>; + # </script> + # + # 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). + # + # The output of this helper method is marked as HTML safe so that you can directly + # include it inside a <tt><script></tt> tag as shown above. + # + # However, it is NOT safe to use the output of this inside an HTML attribute, + # because quotation marks are not escaped. Doing so might break your page's layout. + # If you intend to use this inside an HTML attribute, you should use the + # +html_escape+ helper (or its +h+ alias) instead: + # + # <div data-user-info="<%= h current_user.to_json %>">...</div> # - # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}') - # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1} def json_escape(s) result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE) s.html_safe? ? result.html_safe : result @@ -171,7 +211,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) diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index 6e0af0db4d..5ebafcc26e 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -199,27 +199,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/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index 19d4ff51d7..6be19771f5 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -176,14 +176,22 @@ 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 + # Dependencies assumes 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 we assume therefore the user wants to refer to a top-level constant. + def guess_for_anonymous(const_name) + if Object.const_defined?(const_name) + raise NameError, "#{const_name} cannot be autoloaded from an anonymous class or module" + else + Object + end + end + def unloadable(const_desc = self) super(const_desc) end @@ -456,8 +464,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 diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb index 87b6407038..7df4857c25 100644 --- a/activesupport/lib/active_support/duration.rb +++ b/activesupport/lib/active_support/duration.rb @@ -99,6 +99,14 @@ module ActiveSupport private + # We define it as a workaround to Ruby 2.0.0-p353 bug. + # For more information, check rails/rails#13055. + # It should be dropped once a new Ruby patch-level + # release after 2.0.0-p353 happens. + 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/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 890dd9380b..dcdea70443 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -31,6 +31,11 @@ module I18n fallbacks = app.config.i18n.delete(:fallbacks) + if app.config.i18n.has_key?(:enforce_available_locales) + # this option needs to be set before `default_locale=` to work properly. + I18n.enforce_available_locales = app.config.i18n.delete(:enforce_available_locales) + end + app.config.i18n.each do |setting, value| case setting when :railties_load_path diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 0f7ae98a8a..cdee4c2ca5 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -117,7 +117,8 @@ module ActiveSupport result.gsub!(/([a-z\d]*)/i) { |match| "#{inflections.acronyms[match] || match.downcase}" } - options.fetch(:capitalize, true) ? result.gsub(/^\w/) { $&.upcase } : result + result.gsub!(/^\w/) { $&.upcase } if options.fetch(:capitalize, true) + result end # Capitalizes all the words and replaces some characters in the string to diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 0e1c379b5b..2859075e10 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -1,5 +1,3 @@ -#encoding: us-ascii - require 'active_support/core_ext/object/json' require 'active_support/core_ext/module/delegation' @@ -8,6 +6,7 @@ module ActiveSupport delegate :use_standard_json_time_format, :use_standard_json_time_format=, :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 @@ -18,79 +17,122 @@ module ActiveSupport # 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 Encoder + class JSONGemEncoder #:nodoc: attr_reader :options def initialize(options = nil) @options = options || {} end + # Encode the given object into a JSON string def encode(value) - value.as_json(options.dup).encode_json(self) + stringify jsonify value.as_json(options.dup) end - def escape(string) - Encoding.escape(string) - end - end + 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 + + # 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 - 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', - } + # Encode a "jsonified" Ruby data structure using the JSON gem + def stringify(jsonified) + ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false) + end + end 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 + # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e) + # as a safety measure. + attr_accessor :escape_html_entities_in_json - attr_accessor :escape_regex - attr_reader :escape_html_entities_in_json + # 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 - 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 + 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." + + raise NotImplementedError, message 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 + 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." + + ActiveSupport::Deprecation.warn message + + true end # Deprecate CircularReferenceError @@ -118,7 +160,7 @@ module ActiveSupport self.use_standard_json_time_format = true self.escape_html_entities_in_json = true - self.encode_big_decimal_as_string = true + self.json_encoder = JSONGemEncoder 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..7773611e11 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -76,12 +76,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 +91,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..a35d5980fe 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -37,7 +37,11 @@ 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 + raise InvalidSignature + end else raise InvalidSignature end diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb index e0151baa36..b169e3af01 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" - } - } - } - } + extend ActiveSupport::Autoload + + eager_autoload do + autoload :NumberConverter + autoload :NumberToRoundedConverter + autoload :NumberToDelimitedConverter + autoload :NumberToHumanConverter + autoload :NumberToHumanSizeConverter + autoload :NumberToPhoneConverter + autoload :NumberToCurrencyConverter + autoload :NumberToPercentageConverter + end - 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 - 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 @@ -420,36 +239,7 @@ module ActiveSupport # number_to_human_size(1234567890123, precision: 5) # => "1.1229 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 @@ -550,86 +340,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) - - 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_DECIMAL_UNITS[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..6405afc9a6 --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb @@ -0,0 +1,21 @@ +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) { "#{$1}#{options[:delimiter]}" } + [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..273667fdae --- /dev/null +++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb @@ -0,0 +1,62 @@ +module ActiveSupport + module NumberHelper + class NumberToRoundedConverter < NumberConverter # :nodoc: + self.namespace = :precision + self.validate_float = true + + def convert + @number = Float(number) + + precision = options.delete :precision + significant = options.delete :significant + + 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 = BigDecimal.new(number.to_s).round(precision).to_f + rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros + end + + delimited_number = NumberToDelimitedConverter.convert("%01.#{precision}f" % rounded_number, options) + format_number(delimited_number) + end + + private + + def digits_and_rounded_number(precision) + if number.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) + (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier + end + + def digit_count(number) + (Math.log10(number.abs) + 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 + end + end +end diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb index 18bc919734..d5c2222d2e 100644 --- a/activesupport/lib/active_support/tagged_logging.rb +++ b/activesupport/lib/active_support/tagged_logging.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/object/blank' require 'logger' require 'active_support/logger' diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb index 1fa73caefa..c349bb5fb1 100644 --- a/activesupport/lib/active_support/testing/declarative.rb +++ b/activesupport/lib/active_support/testing/declarative.rb @@ -7,11 +7,18 @@ module ActiveSupport unless method_defined?(:describe) def self.describe(text) - class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 - def self.name - "#{text}" - end - RUBY_EVAL + if block_given? + super + else + message = "`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n" + ActiveSupport::Deprecation.warn message + + class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 + def self.name + "#{text}" + end + RUBY_EVAL + end end end 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..75ead48376 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? diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 3cf82a24b9..a22e61d286 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -1,3 +1,4 @@ +require 'thread_safe' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/try' @@ -184,6 +185,8 @@ module ActiveSupport UTC_OFFSET_WITH_COLON = '%s%02d:%02d' UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '') + @lazy_zones_map = ThreadSafe::Cache.new + # Assumes self represents an offset from UTC in seconds (as returned from # Time#utc_offset) and turns this into an +HH:MM formatted string. # @@ -314,6 +317,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. @@ -362,10 +375,8 @@ module ActiveSupport 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) + MAPPING.each_key {|place| self[place]} # load all the zones + @lazy_zones_map end end @@ -378,7 +389,7 @@ module ActiveSupport case arg when String begin - lazy_zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset } + lazy_zones_map[arg] ||= create(arg).tap { |tz| tz.utc_offset } rescue TZInfo::InvalidTimezoneIdentifier nil end @@ -407,16 +418,9 @@ module ActiveSupport 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 + @lazy_zones_map end end diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb index 65de48a7f6..4600855998 100644 --- a/activesupport/test/abstract_unit.rb +++ b/activesupport/test/abstract_unit.rb @@ -15,7 +15,6 @@ silence_warnings do end require 'active_support/testing/autorun' -require 'empty_bool' ENV['NO_RELOAD'] = '1' require 'active_support' diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/blank_test.rb index a68c074777..3e3176f993 100644 --- a/activesupport/test/core_ext/blank_test.rb +++ b/activesupport/test/core_ext/blank_test.rb @@ -4,6 +4,14 @@ require 'abstract_unit' require 'active_support/core_ext/object/blank' class BlankTest < ActiveSupport::TestCase + class EmptyTrue + def empty?() true; end + end + + class EmptyFalse + def empty?() false; end + end + BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', [], {} ] NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ] diff --git a/activesupport/test/core_ext/class/attribute_accessor_test.rb b/activesupport/test/core_ext/class/attribute_accessor_test.rb deleted file mode 100644 index 3bc948f3a6..0000000000 --- a/activesupport/test/core_ext/class/attribute_accessor_test.rb +++ /dev/null @@ -1,70 +0,0 @@ -require 'abstract_unit' -require 'active_support/core_ext/class/attribute_accessors' - -class ClassAttributeAccessorTest < ActiveSupport::TestCase - def setup - @class = Class.new do - cattr_accessor :foo - cattr_accessor :bar, :instance_writer => false - cattr_reader :shaq, :instance_reader => false - cattr_accessor :camp, :instance_accessor => false - cattr_accessor(:defa) { 'default_accessor_value' } - cattr_reader(:defr) { 'default_reader_value' } - cattr_writer(:defw) { 'default_writer_value' } - end - @object = @class.new - end - - def test_should_use_mattr_default - assert_nil @class.foo - assert_nil @object.foo - end - - def test_should_set_mattr_value - @class.foo = :test - assert_equal :test, @object.foo - - @object.foo = :test2 - assert_equal :test2, @class.foo - end - - def test_should_not_create_instance_writer - assert_respond_to @class, :foo - assert_respond_to @class, :foo= - assert_respond_to @object, :bar - assert !@object.respond_to?(:bar=) - end - - def test_should_not_create_instance_reader - assert_respond_to @class, :shaq - assert !@object.respond_to?(:shaq) - end - - def test_should_not_create_instance_accessors - assert_respond_to @class, :camp - assert !@object.respond_to?(:camp) - assert !@object.respond_to?(:camp=) - end - - def test_should_raise_name_error_if_attribute_name_is_invalid - exception = assert_raises NameError do - Class.new do - cattr_reader "1nvalid" - end - end - assert_equal "invalid class attribute name: 1nvalid", exception.message - - exception = assert_raises NameError do - Class.new do - cattr_writer "1nvalid" - end - end - assert_equal "invalid class attribute name: 1nvalid", exception.message - end - - def test_should_use_default_value_if_block_passed - assert_equal 'default_accessor_value', @class.defa - assert_equal 'default_reader_value', @class.defr - assert_equal 'default_writer_value', @class.class_variable_get('@@defw') - end -end diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb index eab3aa7a6e..5d0af035cc 100644 --- a/activesupport/test/core_ext/date_ext_test.rb +++ b/activesupport/test/core_ext/date_ext_test.rb @@ -276,6 +276,23 @@ class DateExtCalculationsTest < ActiveSupport::TestCase end end + def test_all_week + assert_equal Date.new(2011,6,6)..Date.new(2011,6,12), Date.new(2011,6,7).all_week + assert_equal Date.new(2011,6,5)..Date.new(2011,6,11), Date.new(2011,6,7).all_week(:sunday) + end + + def test_all_month + assert_equal Date.new(2011,6,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_month + end + + def test_all_quarter + assert_equal Date.new(2011,4,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_quarter + end + + def test_all_year + assert_equal Date.new(2011,1,1)..Date.new(2011,12,31), Date.new(2011,6,7).all_year + end + def test_xmlschema with_env_tz 'US/Eastern' do assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema) diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index ed267cf4b9..8eae8c832c 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -71,6 +71,19 @@ class DurationTest < ActiveSupport::TestCase assert_equal 86400 * 1.7, 1.7.days end + def test_since_and_ago + t = Time.local(2000) + assert t + 1, 1.second.since(t) + assert t - 1, 1.second.ago(t) + end + + def test_since_and_ago_without_argument + now = Time.now + assert 1.second.since >= now + 1 + now = Time.now + assert 1.second.ago >= now - 1 + end + def test_since_and_ago_with_fractional_days t = Time.local(2000) # since @@ -96,10 +109,10 @@ class DurationTest < ActiveSupport::TestCase with_env_tz 'US/Eastern' do Time.stubs(:now).returns Time.local(2000) # since - assert_equal false, 5.seconds.since.is_a?(ActiveSupport::TimeWithZone) + assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since # ago - assert_equal false, 5.seconds.ago.is_a?(ActiveSupport::TimeWithZone) + assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago end end @@ -109,11 +122,11 @@ class DurationTest < ActiveSupport::TestCase with_env_tz 'US/Eastern' do Time.stubs(:now).returns Time.local(2000) # since - assert_equal true, 5.seconds.since.is_a?(ActiveSupport::TimeWithZone) + assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time assert_equal 'Eastern Time (US & Canada)', 5.seconds.since.time_zone.name # ago - assert_equal true, 5.seconds.ago.is_a?(ActiveSupport::TimeWithZone) + assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name end @@ -145,6 +158,11 @@ class DurationTest < ActiveSupport::TestCase assert_equal '172800', 2.days.to_json end + def test_case_when + cased = case 1.day when 1.day then "ok" end + assert_equal cased, "ok" + end + protected def with_env_tz(new_tz = 'US/Eastern') old_tz, ENV['TZ'] = ENV['TZ'], new_tz diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index b059bc3e89..1ee9fbf46e 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -624,10 +624,15 @@ class HashExtTest < ActiveSupport::TestCase { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end - assert_raise(ArgumentError, "Unknown key: failore") do + exception = assert_raise ArgumentError do { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ]) + end + assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message + + exception = assert_raise ArgumentError do { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny) end + assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message end def test_assorted_keys_not_stringified diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb index b8951de402..18b251173f 100644 --- a/activesupport/test/core_ext/kernel_test.rb +++ b/activesupport/test/core_ext/kernel_test.rb @@ -38,6 +38,22 @@ class KernelTest < ActiveSupport::TestCase # Skip if we can't STDERR.tell end + def test_silence_stream + old_stream_position = STDOUT.tell + silence_stream(STDOUT) { STDOUT.puts 'hello world' } + assert_equal old_stream_position, STDOUT.tell + rescue Errno::ESPIPE + # Skip if we can't stream.tell + end + + def test_silence_stream_closes_file_descriptors + stream = StringIO.new + dup_stream = StringIO.new + stream.stubs(:dup).returns(dup_stream) + dup_stream.expects(:close) + silence_stream(stream) { stream.puts 'hello world' } + end + def test_quietly old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell quietly do diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb index a577f90bdd..48f3cc579f 100644 --- a/activesupport/test/core_ext/module/attribute_accessor_test.rb +++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb @@ -8,6 +8,11 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase mattr_accessor :bar, :instance_writer => false mattr_reader :shaq, :instance_reader => false mattr_accessor :camp, :instance_accessor => false + + cattr_accessor(:defa) { 'default_accessor_value' } + cattr_reader(:defr) { 'default_reader_value' } + cattr_writer(:defw) { 'default_writer_value' } + cattr_accessor(:quux) { :quux } end @class = Class.new @class.instance_eval { include m } @@ -27,6 +32,11 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase assert_equal :test2, @module.foo end + def test_cattr_accessor_default_value + assert_equal :quux, @module.quux + assert_equal :quux, @object.quux + end + def test_should_not_create_instance_writer assert_respond_to @module, :foo assert_respond_to @module, :foo= @@ -46,16 +56,24 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase end def test_should_raise_name_error_if_attribute_name_is_invalid - assert_raises NameError do + exception = assert_raises NameError do Class.new do - mattr_reader "invalid attribute name" + cattr_reader "1nvalid" end end + assert_equal "invalid attribute name: 1nvalid", exception.message - assert_raises NameError do + exception = assert_raises NameError do Class.new do - mattr_writer "invalid attribute name" + cattr_writer "1nvalid" end end + assert_equal "invalid attribute name: 1nvalid", exception.message + end + + def test_should_use_default_value_if_block_passed + assert_equal 'default_accessor_value', @module.defa + assert_equal 'default_reader_value', @module.defr + assert_equal 'default_writer_value', @module.class_variable_get('@@defw') end end diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb index 23bbb8d7d2..3b1dabea8d 100644 --- a/activesupport/test/core_ext/numeric_ext_test.rb +++ b/activesupport/test/core_ext/numeric_ext_test.rb @@ -22,21 +22,16 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase end end - def test_intervals - @seconds.values.each do |seconds| - assert_equal seconds.since(@now), @now + seconds - assert_equal seconds.until(@now), @now - seconds - end + def test_deprecated_since_and_ago + assert_equal @now + 1, assert_deprecated { 1.since(@now) } + assert_equal @now - 1, assert_deprecated { 1.ago(@now) } end - # Test intervals based from Time.now - def test_now - @seconds.values.each do |seconds| - now = Time.now - assert seconds.ago >= now - seconds - now = Time.now - assert seconds.from_now >= now + seconds - end + def test_deprecated_since_and_ago_without_argument + now = Time.now + assert assert_deprecated { 1.since } >= now + 1 + now = Time.now + assert assert_deprecated { 1.ago } >= now - 1 end def test_irregular_durations @@ -78,10 +73,10 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase end def test_duration_after_conversion_is_no_longer_accurate - assert_equal 30.days.to_i.since(@now), 1.month.to_i.since(@now) - assert_equal 365.25.days.to_f.since(@now), 1.year.to_f.since(@now) - assert_equal 30.days.to_i.since(@dtnow), 1.month.to_i.since(@dtnow) - assert_equal 365.25.days.to_f.since(@dtnow), 1.year.to_f.since(@dtnow) + assert_equal 30.days.to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now) + assert_equal 365.25.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now) + assert_equal 30.days.to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow) + assert_equal 365.25.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow) end def test_add_one_year_to_leap_day @@ -94,11 +89,11 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase with_env_tz 'US/Eastern' do Time.stubs(:now).returns Time.local(2000) # since - assert_equal false, 5.since.is_a?(ActiveSupport::TimeWithZone) - assert_equal Time.local(2000,1,1,0,0,5), 5.since + assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since } + assert_equal Time.local(2000,1,1,0,0,5), assert_deprecated { 5.since } # ago - assert_equal false, 5.ago.is_a?(ActiveSupport::TimeWithZone) - assert_equal Time.local(1999,12,31,23,59,55), 5.ago + assert_not_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago } + assert_equal Time.local(1999,12,31,23,59,55), assert_deprecated { 5.ago } end end @@ -107,13 +102,13 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase with_env_tz 'US/Eastern' do Time.stubs(:now).returns Time.local(2000) # since - assert_equal true, 5.since.is_a?(ActiveSupport::TimeWithZone) - assert_equal Time.utc(2000,1,1,0,0,5), 5.since.time - assert_equal 'Eastern Time (US & Canada)', 5.since.time_zone.name + assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.since } + assert_equal Time.utc(2000,1,1,0,0,5), assert_deprecated { 5.since.time } + assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.since.time_zone.name } # ago - assert_equal true, 5.ago.is_a?(ActiveSupport::TimeWithZone) - assert_equal Time.utc(1999,12,31,23,59,55), 5.ago.time - assert_equal 'Eastern Time (US & Canada)', 5.ago.time_zone.name + assert_instance_of ActiveSupport::TimeWithZone, assert_deprecated { 5.ago } + assert_equal Time.utc(1999,12,31,23,59,55), assert_deprecated { 5.ago.time } + assert_equal 'Eastern Time (US & Canada)', assert_deprecated { 5.ago.time_zone.name } end ensure Time.zone = nil diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 854b0a38bd..6d6afc85c4 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -42,7 +42,7 @@ class RangeTest < ActiveSupport::TestCase assert((1...10).include?(1...10)) end - def test_should_include_other_with_exlusive_end + def test_should_include_other_with_exclusive_end assert((1..10).include?(1...10)) end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index 2392b71960..e56bab6d4c 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -530,29 +530,21 @@ class DependenciesTest < ActiveSupport::TestCase end end - def test_const_missing_should_not_double_load - $counting_loaded_times = 0 + def test_const_missing_in_anonymous_modules_loads_top_level_constants with_autoloading_fixtures do - require_dependency '././counting_loader' - assert_equal 1, $counting_loaded_times - assert_raise(NameError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader } - assert_equal 1, $counting_loaded_times + # class_eval STRING pushes the class to the nesting of the eval'ed code. + klass = Class.new.class_eval "E" + assert_equal E, klass end end - def test_const_missing_within_anonymous_module - $counting_loaded_times = 0 - m = Module.new - m.module_eval "def a() CountingLoader; end" - extend m + def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object with_autoloading_fixtures do - kls = nil - assert_nothing_raised { kls = a } - assert_equal "CountingLoader", kls.name - assert_equal 1, $counting_loaded_times + require_dependency 'e' - assert_nothing_raised { kls = a } - assert_equal 1, $counting_loaded_times + mod = Module.new + msg = 'E cannot be autoloaded from an anonymous class or module' + assert_raise(NameError, msg) { mod::E } end end diff --git a/activesupport/test/empty_bool.rb b/activesupport/test/empty_bool.rb deleted file mode 100644 index 005b3523ef..0000000000 --- a/activesupport/test/empty_bool.rb +++ /dev/null @@ -1,7 +0,0 @@ -class EmptyTrue - def empty?() true; end -end - -class EmptyFalse - def empty?() false; end -end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 6184df481f..35967ba656 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -70,10 +70,11 @@ class InflectorTest < ActiveSupport::TestCase def test_overwrite_previous_inflectors - assert_equal("series", ActiveSupport::Inflector.singularize("series")) - ActiveSupport::Inflector.inflections.singular "series", "serie" - assert_equal("serie", ActiveSupport::Inflector.singularize("series")) - ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal + with_dup do + assert_equal("series", ActiveSupport::Inflector.singularize("series")) + ActiveSupport::Inflector.inflections.singular "series", "serie" + assert_equal("serie", ActiveSupport::Inflector.singularize("series")) + end end MixtureToTitleCase.each_with_index do |(before, titleized), index| diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index b34a946baf..4bd1b2e47c 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -63,6 +63,7 @@ module InflectorTestCases "news" => "news", "series" => "series", + "miniseries" => "miniseries", "species" => "species", "quiz" => "quizzes", diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb index 00f43be6d4..78cf4819f9 100644 --- a/activesupport/test/json/encoding_test.rb +++ b/activesupport/test/json/encoding_test.rb @@ -18,8 +18,12 @@ class TestJSONEncoding < ActiveSupport::TestCase end class Custom - def as_json(options) - 'custom' + def initialize(serialized) + @serialized = serialized + end + + def as_json(options = nil) + @serialized end end @@ -62,11 +66,11 @@ class TestJSONEncoding < ActiveSupport::TestCase [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]] - StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")], + StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")], [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ], [ 'http://test.host/posts/1', %("http://test.host/posts/1")], - [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\342\200\250\342\200\251", - %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F\\u2028\\u2029") ]] + [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029", + %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]] ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] @@ -81,7 +85,13 @@ class TestJSONEncoding < ActiveSupport::TestCase ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] - CustomTests = [[ Custom.new, '"custom"' ]] + CustomTests = [[ Custom.new("custom"), '"custom"' ], + [ Custom.new(nil), 'null' ], + [ Custom.new(:a), '"a"' ], + [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], + [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], + [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] @@ -162,6 +172,22 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal "𐒑", decoded_hash['string'] end + def test_reading_encode_big_decimal_as_string_option + assert_deprecated do + assert ActiveSupport.encode_big_decimal_as_string + end + end + + def test_setting_deprecated_encode_big_decimal_as_string_option + assert_raise(NotImplementedError) do + ActiveSupport.encode_big_decimal_as_string = true + end + + assert_raise(NotImplementedError) do + ActiveSupport.encode_big_decimal_as_string = false + end + end + def test_exception_raised_when_encoding_circular_reference_in_array a = [1] a << a @@ -328,7 +354,7 @@ class TestJSONEncoding < ActiveSupport::TestCase assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) end - def test_to_json_should_not_keep_options_around + def test_hash_to_json_should_not_keep_options_around f = CustomWithOptions.new f.foo = "hello" f.bar = "world" @@ -338,6 +364,16 @@ class TestJSONEncoding < ActiveSupport::TestCase "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json)) end + def test_array_to_json_should_not_keep_options_around + f = CustomWithOptions.new + f.foo = "hello" + f.bar = "world" + + array = [f, {"foo" => "other_foo", "test" => "other_test"}] + assert_equal([{"foo"=>"hello","bar"=>"world"}, + {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json)) + end + def test_hash_as_json_without_options json = { foo: OptionsTest.new }.as_json assert_equal({"foo" => :default}, json) @@ -379,17 +415,6 @@ class TestJSONEncoding < ActiveSupport::TestCase ActiveSupport::JSON.decode(json_string_and_date)) end - def test_opt_out_big_decimal_string_serialization - big_decimal = BigDecimal('2.5') - - begin - ActiveSupport.encode_big_decimal_as_string = false - assert_equal big_decimal.to_s, big_decimal.to_json - ensure - ActiveSupport.encode_big_decimal_as_string = true - end - end - def test_nil_true_and_false_represented_as_themselves assert_equal nil, nil.as_json assert_equal true, true.as_json diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index 203156baa1..b6c0a08b05 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -66,6 +66,17 @@ class MessageEncryptorTest < ActiveSupport::TestCase ActiveSupport.use_standard_json_time_format = prev end + def test_message_obeys_strict_encoding + bad_encoding_characters = "\n!@#" + message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string"+bad_encoding_characters) + + assert_not_decrypted("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}") + assert_not_verified("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}") + + assert_not_decrypted([iv, message] * bad_encoding_characters) + assert_not_verified([iv, message] * bad_encoding_characters) + end + private def assert_not_decrypted(value) @@ -81,7 +92,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase end def munge(base64_string) - bits = ::Base64.decode64(base64_string) + bits = ::Base64.strict_decode64(base64_string) bits.reverse! ::Base64.strict_encode64(bits) end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index f0f261d710..f208814468 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -11,7 +11,7 @@ require 'active_support/time' require 'active_support/json' class MessageVerifierTest < ActiveSupport::TestCase - + class JSONSerializer def dump(value) ActiveSupport::JSON.encode(value) @@ -21,7 +21,7 @@ class MessageVerifierTest < ActiveSupport::TestCase ActiveSupport::JSON.decode(value) end end - + def setup @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") @data = { :some => "data", :now => Time.local(2010) } @@ -43,7 +43,7 @@ class MessageVerifierTest < ActiveSupport::TestCase assert_not_verified("#{data}--#{hash.reverse}") assert_not_verified("purejunk") end - + def test_alternative_serialization_method prev = ActiveSupport.use_standard_json_time_format ActiveSupport.use_standard_json_time_format = true @@ -54,7 +54,7 @@ class MessageVerifierTest < ActiveSupport::TestCase ensure ActiveSupport.use_standard_json_time_format = prev end - + def assert_not_verified(message) assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do @verifier.verify(message) diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb index 1fadef3637..111db59b2b 100644 --- a/activesupport/test/number_helper_test.rb +++ b/activesupport/test/number_helper_test.rb @@ -102,7 +102,6 @@ module ActiveSupport assert_equal '12 345 678', number_helper.number_to_delimited(12345678, :delimiter => ' ') assert_equal '12,345,678-05', number_helper.number_to_delimited(12345678.05, :separator => '-') assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :separator => ',', :delimiter => '.') - assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :delimiter => '.', :separator => ',') end end @@ -370,12 +369,6 @@ module ActiveSupport end end - def test_extending_or_including_number_helper_correctly_hides_private_methods - [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper| - assert !number_helper.respond_to?(:valid_float?) - assert number_helper.respond_to?(:valid_float?, true) - end - end end end end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 84c3154e53..1107b48460 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -89,16 +89,38 @@ class TimeZoneTest < ActiveSupport::TestCase end def test_today - Time.stubs(:now).returns(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST + travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today - Time.stubs(:now).returns(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST + travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today - Time.stubs(:now).returns(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST + travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today - Time.stubs(:now).returns(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST + travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today end + def test_tomorrow + travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST + assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow + travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST + assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow + travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST + assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow + travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST + assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow + end + + def test_yesterday + travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST + assert_equal Date.new(1999, 12, 30), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday + travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST + assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday + travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST + assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday + travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST + assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday + end + def test_local time = ActiveSupport::TimeZone["Hawaii"].local(2007, 2, 5, 15, 30, 45) assert_equal Time.utc(2007, 2, 5, 15, 30, 45), time.time |