diff options
Diffstat (limited to 'activesupport')
14 files changed, 896 insertions, 733 deletions
diff --git a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb index efb9d1b8a0..061c959442 100644 --- a/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb +++ b/activesupport/lib/active_support/core_ext/hash/reverse_merge.rb @@ -12,6 +12,7 @@ class Hash def reverse_merge(other_hash) other_hash.merge(self) end + alias_method :with_defaults, :reverse_merge # Destructive +reverse_merge+. def reverse_merge!(other_hash) @@ -19,4 +20,5 @@ class Hash merge!(other_hash) { |key, left, right| left } end alias_method :reverse_update, :reverse_merge! + alias_method :with_defaults!, :reverse_merge! end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index cdf27f49ad..85ab739095 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -267,7 +267,10 @@ class Module module_eval <<-RUBY, __FILE__, __LINE__ + 1 def respond_to_missing?(name, include_private = false) - #{target}.respond_to?(name, include_private) + # It may look like an oversight, but we deliberately do not pass + # +include_private+, because they do not get delegated. + + #{target}.respond_to?(name) || super end def method_missing(method, *args, &block) diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 7a2fc5c1b5..b27cfe53f4 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -100,12 +100,17 @@ class String # a nicer looking title. +titleize+ is meant for creating pretty output. It is not # used in the Rails internals. # + # The trailing '_id','Id'.. can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true. + # By default, this parameter is false. + # # +titleize+ is also aliased as +titlecase+. # - # 'man from the boondocks'.titleize # => "Man From The Boondocks" - # 'x-men: the last stand'.titleize # => "X Men: The Last Stand" - def titleize - ActiveSupport::Inflector.titleize(self) + # 'man from the boondocks'.titleize # => "Man From The Boondocks" + # 'x-men: the last stand'.titleize # => "X Men: The Last Stand" + # 'string_ending_with_id'.titleize(keep_id_suffix: true) # => "String Ending With Id" + def titleize(keep_id_suffix: false) + ActiveSupport::Inflector.titleize(self, keep_id_suffix: keep_id_suffix) end alias_method :titlecase, :titleize @@ -202,7 +207,7 @@ class String ActiveSupport::Inflector.classify(self) end - # Capitalizes the first word, turns underscores into spaces, and strips a + # Capitalizes the first word, turns underscores into spaces, and (by default)strips a # trailing '_id' if present. # Like +titleize+, this is meant for creating pretty output. # @@ -210,12 +215,17 @@ class String # optional parameter +capitalize+ to false. # By default, this parameter is true. # - # 'employee_salary'.humanize # => "Employee salary" - # 'author_id'.humanize # => "Author" - # 'author_id'.humanize(capitalize: false) # => "author" - # '_id'.humanize # => "Id" - def humanize(options = {}) - ActiveSupport::Inflector.humanize(self, options) + # The trailing '_id' can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true. + # By default, this parameter is false. + # + # 'employee_salary'.humanize # => "Employee salary" + # 'author_id'.humanize # => "Author" + # 'author_id'.humanize(capitalize: false) # => "author" + # '_id'.humanize # => "Id" + # 'author_id'.humanize(keep_id_suffix: true) # => "Author Id" + def humanize(capitalize: true, keep_id_suffix: false) + ActiveSupport::Inflector.humanize(self, capitalize: capitalize, keep_id_suffix: keep_id_suffix) end # Converts just the first character to uppercase. diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb index 8e0dc71dca..f59f5d17dc 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -125,6 +125,11 @@ module ActiveSupport dtw.compact! dtw.uniq! + normalized_gem_paths = Gem.path.map { |path| File.join path, "" } + dtw = dtw.reject do |path| + normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) } + end + @ph.filter_out_descendants(dtw) end diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 1927cddf34..1e1d6d3aac 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -225,11 +225,13 @@ module ActiveSupport def reverse_merge(other_hash) super(self.class.new(other_hash)) end + alias_method :with_defaults, :reverse_merge # Same semantics as +reverse_merge+ but modifies the receiver in-place. def reverse_merge!(other_hash) replace(reverse_merge(other_hash)) end + alias_method :with_defaults!, :reverse_merge! # Replaces the contents of this hash with other_hash. # diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 51c221ac0e..1b089a7538 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -108,33 +108,38 @@ module ActiveSupport # * Replaces underscores with spaces, if any. # * Downcases all words except acronyms. # * Capitalizes the first word. - # # The capitalization of the first word can be turned off by setting the # +:capitalize+ option to false (default is true). # - # humanize('employee_salary') # => "Employee salary" - # humanize('author_id') # => "Author" - # humanize('author_id', capitalize: false) # => "author" - # humanize('_id') # => "Id" + # The trailing '_id' can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true (default is false). + # + # humanize('employee_salary') # => "Employee salary" + # humanize('author_id') # => "Author" + # humanize('author_id', capitalize: false) # => "author" + # humanize('_id') # => "Id" + # humanize('author_id', keep_id_suffix: true) # => "Author Id" # # If "SSL" was defined to be an acronym: # # humanize('ssl_error') # => "SSL error" # - def humanize(lower_case_and_underscored_word, options = {}) + def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false) result = lower_case_and_underscored_word.to_s.dup inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) } result.sub!(/\A_+/, "".freeze) - result.sub!(/_id\z/, "".freeze) + unless keep_id_suffix + result.sub!(/_id\z/, "".freeze) + end result.tr!("_".freeze, " ".freeze) result.gsub!(/([a-z\d]*)/i) do |match| "#{inflections.acronyms[match] || match.downcase}" end - if options.fetch(:capitalize, true) + if capitalize result.sub!(/\A\w/) { |match| match.upcase } end @@ -154,14 +159,21 @@ module ActiveSupport # create a nicer looking title. +titleize+ is meant for creating pretty # output. It is not used in the Rails internals. # + # The trailing '_id','Id'.. can be kept and capitalized by setting the + # optional parameter +keep_id_suffix+ to true. + # By default, this parameter is false. + # # +titleize+ is also aliased as +titlecase+. # - # titleize('man from the boondocks') # => "Man From The Boondocks" - # titleize('x-men: the last stand') # => "X Men: The Last Stand" - # titleize('TheManWithoutAPast') # => "The Man Without A Past" - # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark" - def titleize(word) - humanize(underscore(word)).gsub(/\b(?<!\w['’`])[a-z]/) { |match| match.capitalize } + # titleize('man from the boondocks') # => "Man From The Boondocks" + # titleize('x-men: the last stand') # => "X Men: The Last Stand" + # titleize('TheManWithoutAPast') # => "The Man Without A Past" + # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark" + # titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id" + def titleize(word, keep_id_suffix: false) + humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`])[a-z]/) do |match| + match.capitalize + end end # Creates the name of a table like Rails does for models to table names. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 18477b9f6b..ce5207546d 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -250,14 +250,21 @@ module ActiveSupport # for time zones in the country specified by its ISO 3166-1 Alpha2 code. def country_zones(country_code) code = country_code.to_s.upcase - @country_zones[code] ||= - TZInfo::Country.get(code).zone_identifiers.map do |tz_id| - name = MAPPING.key(tz_id) - name && self[name] - end.compact.sort! + @country_zones[code] ||= load_country_zones(code) end private + def load_country_zones(code) + country = TZInfo::Country.get(code) + country.zone_identifiers.map do |tz_id| + if MAPPING.value?(tz_id) + self[MAPPING.key(tz_id)] + else + create(tz_id, nil, TZInfo::Timezone.new(tz_id)) + end + end.sort! + end + def zones_map @zones_map ||= begin MAPPING.each_key { |place| self[place] } # load all the zones diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 525ea08abd..18da5fcf5f 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -8,33 +8,6 @@ require "active_support/core_ext/object/deep_dup" require "active_support/inflections" class HashExtTest < ActiveSupport::TestCase - HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess - - class IndifferentHash < ActiveSupport::HashWithIndifferentAccess - end - - class SubclassingArray < Array - end - - class SubclassingHash < Hash - end - - class NonIndifferentHash < Hash - def nested_under_indifferent_access - self - end - end - - class HashByConversion - def initialize(hash) - @hash = hash - end - - def to_hash - @hash - end - end - def setup @strings = { "a" => 1, "b" => 2 } @nested_strings = { "a" => { "b" => { "c" => 3 } } } @@ -264,471 +237,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal({ "a" => { b: { "c" => 3 } } }, @nested_mixed) end - def test_symbolize_keys_for_hash_with_indifferent_access - assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys - assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys - assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys - assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys - end - - def test_deep_symbolize_keys_for_hash_with_indifferent_access - assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys - assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys - assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys - assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys - end - - def test_symbolize_keys_bang_for_hash_with_indifferent_access - assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! } - assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! } - assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! } - end - - def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access - assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } - assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! } - assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! } - end - - def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access - assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys - assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! } - end - - def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access - assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys - assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } - end - - def test_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access - assert_equal @integers, @integers.with_indifferent_access.symbolize_keys - assert_raise(NoMethodError) { @integers.with_indifferent_access.dup.symbolize_keys! } - end - - def test_deep_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access - assert_equal @nested_integers, @nested_integers.with_indifferent_access.deep_symbolize_keys - assert_raise(NoMethodError) { @nested_integers.with_indifferent_access.deep_dup.deep_symbolize_keys! } - end - - def test_stringify_keys_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys - assert_equal @strings, @symbols.with_indifferent_access.stringify_keys - assert_equal @strings, @strings.with_indifferent_access.stringify_keys - assert_equal @strings, @mixed.with_indifferent_access.stringify_keys - end - - def test_deep_stringify_keys_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys - assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys - assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys - assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys - end - - def test_stringify_keys_bang_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys! - assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys! - assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys! - assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! - end - - def test_deep_stringify_keys_bang_for_hash_with_indifferent_access - assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys! - assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys! - assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys! - assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys! - end - - def test_nested_under_indifferent_access - foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access - assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"] - - foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access - assert_kind_of NonIndifferentHash, foo["foo"] - - foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access - assert_kind_of IndifferentHash, foo["foo"] - end - - def test_indifferent_assorted - @strings = @strings.with_indifferent_access - @symbols = @symbols.with_indifferent_access - @mixed = @mixed.with_indifferent_access - - assert_equal "a", @strings.__send__(:convert_key, :a) - - assert_equal 1, @strings.fetch("a") - assert_equal 1, @strings.fetch(:a.to_s) - assert_equal 1, @strings.fetch(:a) - - hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed } - method_map = { '[]': 1, fetch: 1, values_at: [1], - has_key?: true, include?: true, key?: true, - member?: true } - - hashes.each do |name, hash| - method_map.sort_by(&:to_s).each do |meth, expected| - assert_equal(expected, hash.__send__(meth, "a"), - "Calling #{name}.#{meth} 'a'") - assert_equal(expected, hash.__send__(meth, :a), - "Calling #{name}.#{meth} :a") - end - end - - assert_equal [1, 2], @strings.values_at("a", "b") - assert_equal [1, 2], @strings.values_at(:a, :b) - assert_equal [1, 2], @symbols.values_at("a", "b") - assert_equal [1, 2], @symbols.values_at(:a, :b) - assert_equal [1, 2], @mixed.values_at("a", "b") - assert_equal [1, 2], @mixed.values_at(:a, :b) - end - - def test_indifferent_reading - hash = HashWithIndifferentAccess.new - hash["a"] = 1 - hash["b"] = true - hash["c"] = false - hash["d"] = nil - - assert_equal 1, hash[:a] - assert_equal true, hash[:b] - assert_equal false, hash[:c] - assert_nil hash[:d] - assert_nil hash[:e] - end - - def test_indifferent_reading_with_nonnil_default - hash = HashWithIndifferentAccess.new(1) - hash["a"] = 1 - hash["b"] = true - hash["c"] = false - hash["d"] = nil - - assert_equal 1, hash[:a] - assert_equal true, hash[:b] - assert_equal false, hash[:c] - assert_nil hash[:d] - assert_equal 1, hash[:e] - end - - def test_indifferent_writing - hash = HashWithIndifferentAccess.new - hash[:a] = 1 - hash["b"] = 2 - hash[3] = 3 - - assert_equal 1, hash["a"] - assert_equal 2, hash["b"] - assert_equal 1, hash[:a] - assert_equal 2, hash[:b] - assert_equal 3, hash[3] - end - - def test_indifferent_update - hash = HashWithIndifferentAccess.new - hash[:a] = "a" - hash["b"] = "b" - - updated_with_strings = hash.update(@strings) - updated_with_symbols = hash.update(@symbols) - updated_with_mixed = hash.update(@mixed) - - assert_equal 1, updated_with_strings[:a] - assert_equal 1, updated_with_strings["a"] - assert_equal 2, updated_with_strings["b"] - - assert_equal 1, updated_with_symbols[:a] - assert_equal 2, updated_with_symbols["b"] - assert_equal 2, updated_with_symbols[:b] - - assert_equal 1, updated_with_mixed[:a] - assert_equal 2, updated_with_mixed["b"] - - assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 } - end - - def test_update_with_to_hash_conversion - hash = HashWithIndifferentAccess.new - hash.update HashByConversion.new(a: 1) - assert_equal 1, hash["a"] - end - - def test_indifferent_merging - hash = HashWithIndifferentAccess.new - hash[:a] = "failure" - hash["b"] = "failure" - - other = { "a" => 1, :b => 2 } - - merged = hash.merge(other) - - assert_equal HashWithIndifferentAccess, merged.class - assert_equal 1, merged[:a] - assert_equal 2, merged["b"] - - hash.update(other) - - assert_equal 1, hash[:a] - assert_equal 2, hash["b"] - end - - def test_merge_with_to_hash_conversion - hash = HashWithIndifferentAccess.new - merged = hash.merge HashByConversion.new(a: 1) - assert_equal 1, merged["a"] - end - - def test_indifferent_replace - hash = HashWithIndifferentAccess.new - hash[:a] = 42 - - replaced = hash.replace(b: 12) - - assert hash.key?("b") - assert !hash.key?(:a) - assert_equal 12, hash[:b] - assert_same hash, replaced - end - - def test_replace_with_to_hash_conversion - hash = HashWithIndifferentAccess.new - hash[:a] = 42 - - replaced = hash.replace(HashByConversion.new(b: 12)) - - assert hash.key?("b") - assert !hash.key?(:a) - assert_equal 12, hash[:b] - assert_same hash, replaced - end - - def test_indifferent_merging_with_block - hash = HashWithIndifferentAccess.new - hash[:a] = 1 - hash["b"] = 3 - - other = { "a" => 4, :b => 2, "c" => 10 } - - merged = hash.merge(other) { |key, old, new| old > new ? old : new } - - assert_equal HashWithIndifferentAccess, merged.class - assert_equal 4, merged[:a] - assert_equal 3, merged["b"] - assert_equal 10, merged[:c] - - other_indifferent = HashWithIndifferentAccess.new("a" => 9, :b => 2) - - merged = hash.merge(other_indifferent) { |key, old, new| old + new } - - assert_equal HashWithIndifferentAccess, merged.class - assert_equal 10, merged[:a] - assert_equal 5, merged[:b] - end - - def test_indifferent_reverse_merging - hash = HashWithIndifferentAccess.new key: :old_value - hash.reverse_merge! key: :new_value - assert_equal :old_value, hash[:key] - - hash = HashWithIndifferentAccess.new("some" => "value", "other" => "value") - hash.reverse_merge!(some: "noclobber", another: "clobber") - assert_equal "value", hash[:some] - assert_equal "clobber", hash[:another] - end - - def test_indifferent_deleting - get_hash = proc { { a: "foo" }.with_indifferent_access } - hash = get_hash.call - assert_equal "foo", hash.delete(:a) - assert_nil hash.delete(:a) - hash = get_hash.call - assert_equal "foo", hash.delete("a") - assert_nil hash.delete("a") - end - - def test_indifferent_select - hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| v == 1 } - - assert_equal({ "a" => 1 }, hash) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash - end - - def test_indifferent_select_returns_enumerator - enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).select - assert_instance_of Enumerator, enum - end - - def test_indifferent_select_returns_a_hash_when_unchanged - hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| true } - - assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash - end - - def test_indifferent_select_bang - indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) - indifferent_strings.select! { |k, v| v == 1 } - - assert_equal({ "a" => 1 }, indifferent_strings) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings - end - - def test_indifferent_reject - hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject { |k, v| v != 1 } - - assert_equal({ "a" => 1 }, hash) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash - end - - def test_indifferent_reject_returns_enumerator - enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject - assert_instance_of Enumerator, enum - end - - def test_indifferent_reject_bang - indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) - indifferent_strings.reject! { |k, v| v != 1 } - - assert_equal({ "a" => 1 }, indifferent_strings) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings - end - - def test_indifferent_compact - hash_contain_nil_value = @strings.merge("z" => nil) - hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) - compacted_hash = hash.compact - - assert_equal(@strings, compacted_hash) - assert_equal(hash_contain_nil_value, hash) - assert_instance_of ActiveSupport::HashWithIndifferentAccess, compacted_hash - - empty_hash = ActiveSupport::HashWithIndifferentAccess.new - compacted_hash = empty_hash.compact - - assert_equal compacted_hash, empty_hash - - non_empty_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: :bar) - compacted_hash = non_empty_hash.compact - - assert_equal compacted_hash, non_empty_hash - end - - def test_indifferent_to_hash - # Should convert to a Hash with String keys. - assert_equal @strings, @mixed.with_indifferent_access.to_hash - - # Should preserve the default value. - mixed_with_default = @mixed.dup - mixed_with_default.default = "1234" - roundtrip = mixed_with_default.with_indifferent_access.to_hash - assert_equal @strings, roundtrip - assert_equal "1234", roundtrip.default - - # Ensure nested hashes are not HashWithIndiffereneAccess - new_to_hash = @nested_mixed.with_indifferent_access.to_hash - assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) - assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) - assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess) - end - - def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access - hash = HashWithIndifferentAccess.new { |h, k| h[k] = [] } - hash[:a] << 1 - - assert_equal [1], hash[:a] - end - - def test_with_indifferent_access_has_no_side_effects_on_existing_hash - hash = { content: [{ :foo => :bar, "bar" => "baz" }] } - hash.with_indifferent_access - - assert_equal [:foo, "bar"], hash[:content].first.keys - end - - def test_indifferent_hash_with_array_of_hashes - hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] } }.with_indifferent_access - assert_equal "1", hash[:urls][:url].first[:address] - - hash = hash.to_hash - assert_not hash.instance_of?(HashWithIndifferentAccess) - assert_not hash["urls"].instance_of?(HashWithIndifferentAccess) - assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess) - end - - def test_should_preserve_array_subclass_when_value_is_array - array = SubclassingArray.new - array << { "address" => "1" } - hash = { "urls" => { "url" => array } }.with_indifferent_access - assert_equal SubclassingArray, hash[:urls][:url].class - end - - def test_should_preserve_array_class_when_hash_value_is_frozen_array - array = SubclassingArray.new - array << { "address" => "1" } - hash = { "urls" => { "url" => array.freeze } }.with_indifferent_access - assert_equal SubclassingArray, hash[:urls][:url].class - end - - def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash - h = HashWithIndifferentAccess.new - h[:first] = 1 - h = h.stringify_keys - assert_equal 1, h["first"] - h = HashWithIndifferentAccess.new - h["first"] = 1 - h = h.symbolize_keys - assert_equal 1, h[:first] - end - - def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash - h = HashWithIndifferentAccess.new - h[:first] = 1 - h = h.deep_stringify_keys - assert_equal 1, h["first"] - h = HashWithIndifferentAccess.new - h["first"] = 1 - h = h.deep_symbolize_keys - assert_equal 1, h[:first] - end - - def test_to_options_on_indifferent_preserves_hash - h = HashWithIndifferentAccess.new - h["first"] = 1 - h.to_options! - assert_equal 1, h["first"] - end - - def test_to_options_on_indifferent_preserves_works_as_hash_with_dup - h = HashWithIndifferentAccess.new(a: { b: "b" }) - dup = h.dup - - dup[:a][:c] = "c" - assert_equal "c", h[:a][:c] - end - - def test_indifferent_sub_hashes - h = { "user" => { "id" => 5 } }.with_indifferent_access - ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } - - h = { user: { id: 5 } }.with_indifferent_access - ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } - end - - def test_indifferent_duplication - # Should preserve default value - h = HashWithIndifferentAccess.new - h.default = "1234" - assert_equal h.default, h.dup.default - - # Should preserve class for subclasses - h = IndifferentHash.new - assert_equal h.class, h.dup.class - end - - def test_nested_dig_indifferent_access - skip if RUBY_VERSION < "2.3.0" - data = { "this" => { "views" => 1234 } }.with_indifferent_access - assert_equal 1234, data.dig(:this, :views) - end - def test_assert_valid_keys assert_nothing_raised do { failure: "stuff", funny: "business" }.assert_valid_keys([ :failure, :funny ]) @@ -761,12 +269,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message end - def test_assorted_keys_not_stringified - original = { Object.new => 2, 1 => 2, [] => true } - indiff = original.with_indifferent_access - assert(!indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!") - end - def test_deep_merge hash_1 = { a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } } } hash_2 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } @@ -797,37 +299,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, hash_1 end - def test_deep_merge_on_indifferent_access - hash_1 = HashWithIndifferentAccess.new(a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } }) - hash_2 = HashWithIndifferentAccess.new(a: 1, c: { c1: 2, c3: { d2: "d2" } }) - hash_3 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } - expected = { "a" => 1, "b" => "b", "c" => { "c1" => 2, "c2" => "c2", "c3" => { "d1" => "d1", "d2" => "d2" } } } - assert_equal expected, hash_1.deep_merge(hash_2) - assert_equal expected, hash_1.deep_merge(hash_3) - - hash_1.deep_merge!(hash_2) - assert_equal expected, hash_1 - end - - def test_store_on_indifferent_access - hash = HashWithIndifferentAccess.new - hash.store(:test1, 1) - hash.store("test1", 11) - hash[:test2] = 2 - hash["test2"] = 22 - expected = { "test1" => 11, "test2" => 22 } - assert_equal expected, hash - end - - def test_constructor_on_indifferent_access - hash = HashWithIndifferentAccess[:foo, 1] - assert_equal 1, hash[:foo] - assert_equal 1, hash["foo"] - hash[:foo] = 3 - assert_equal 3, hash[:foo] - assert_equal 3, hash["foo"] - end - def test_reverse_merge defaults = { a: "x", b: "y", c: 10 }.freeze options = { a: 1, b: 2 } @@ -848,6 +319,21 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, merged end + def test_with_defaults_aliases_reverse_merge + defaults = { a: "x", b: "y", c: 10 }.freeze + options = { a: 1, b: 2 } + expected = { a: 1, b: 2, c: 10 } + + # Should be an alias for reverse_merge + assert_equal expected, options.with_defaults(defaults) + assert_not_equal expected, options + + # Should be an alias for reverse_merge! + merged = options.dup + assert_equal expected, merged.with_defaults!(defaults) + assert_equal expected, merged + end + def test_slice original = { a: "x", b: "y", c: 10 } expected = { a: "x", b: "y" } @@ -890,38 +376,6 @@ class HashExtTest < ActiveSupport::TestCase assert_equal expected, original.slice(*[:a, :b]) end - def test_indifferent_slice - original = { a: "x", b: "y", c: 10 }.with_indifferent_access - expected = { a: "x", b: "y" }.with_indifferent_access - - [["a", "b"], [:a, :b]].each do |keys| - # Should return a new hash with only the given keys. - assert_equal expected, original.slice(*keys), keys.inspect - assert_not_equal expected, original - end - end - - def test_indifferent_slice_inplace - original = { a: "x", b: "y", c: 10 }.with_indifferent_access - expected = { c: 10 }.with_indifferent_access - - [["a", "b"], [:a, :b]].each do |keys| - # Should replace the hash with only the given keys. - copy = original.dup - assert_equal expected, copy.slice!(*keys) - end - end - - def test_indifferent_slice_access_with_symbols - original = { "login" => "bender", "password" => "shiny", "stuff" => "foo" } - original = original.with_indifferent_access - - slice = original.slice(:login, :password) - - assert_equal "bender", slice[:login] - assert_equal "bender", slice["login"] - end - def test_slice_bang_does_not_override_default hash = Hash.new(0) hash.update(a: 1, b: 2) @@ -959,18 +413,6 @@ class HashExtTest < ActiveSupport::TestCase assert_nil extracted[:x] end - def test_indifferent_extract - original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access - expected = { a: 1, b: 2 }.with_indifferent_access - remaining = { c: 3, d: 4 }.with_indifferent_access - - [["a", "b"], [:a, :b]].each do |keys| - copy = original.dup - assert_equal expected, copy.extract!(*keys) - assert_equal remaining, copy - end - end - def test_except original = { a: "x", b: "y", c: 10 } expected = { a: "x", b: "y" } @@ -1042,78 +484,6 @@ class HashExtTest < ActiveSupport::TestCase assert_nil(h.compact!) assert_equal(@symbols, h) end - - def test_new_with_to_hash_conversion - hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1)) - assert hash.key?("a") - assert_equal 1, hash[:a] - end - - def test_dup_with_default_proc - hash = HashWithIndifferentAccess.new - hash.default_proc = proc { |h, v| raise "walrus" } - assert_nothing_raised { hash.dup } - end - - def test_dup_with_default_proc_sets_proc - hash = HashWithIndifferentAccess.new - hash.default_proc = proc { |h, k| k + 1 } - new_hash = hash.dup - - assert_equal 3, new_hash[2] - - new_hash.default = 2 - assert_equal 2, new_hash[:non_existent] - end - - def test_to_hash_with_raising_default_proc - hash = HashWithIndifferentAccess.new - hash.default_proc = proc { |h, k| raise "walrus" } - - assert_nothing_raised { hash.to_hash } - end - - def test_new_with_to_hash_conversion_copies_default - normal_hash = Hash.new(3) - normal_hash[:a] = 1 - - hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) - assert_equal 1, hash[:a] - assert_equal 3, hash[:b] - end - - def test_new_with_to_hash_conversion_copies_default_proc - normal_hash = Hash.new { 1 + 2 } - normal_hash[:a] = 1 - - hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) - assert_equal 1, hash[:a] - assert_equal 3, hash[:b] - end - - def test_inheriting_from_top_level_hash_with_indifferent_access_preserves_ancestors_chain - klass = Class.new(::HashWithIndifferentAccess) - assert_equal ActiveSupport::HashWithIndifferentAccess, klass.ancestors[1] - end - - def test_inheriting_from_hash_with_indifferent_access_properly_dumps_ivars - klass = Class.new(::HashWithIndifferentAccess) do - def initialize(*) - @foo = "bar" - super - end - end - - yaml_output = klass.new.to_yaml - - # `hash-with-ivars` was introduced in 2.0.9 (https://git.io/vyUQW) - if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("2.0.9") - assert_includes yaml_output, "hash-with-ivars" - assert_includes yaml_output, "@foo: bar" - else - assert_includes yaml_output, "hash" - end - end end class IWriteMyOwnXML @@ -1159,8 +529,6 @@ class HashExtToParamTests < ActiveSupport::TestCase end class HashToXmlTest < ActiveSupport::TestCase - HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess - def setup @xml_options = { root: :person, skip_instruct: true, indent: 0 } end @@ -1636,61 +1004,6 @@ class HashToXmlTest < ActiveSupport::TestCase assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>') end - def test_should_use_default_proc_for_unknown_key - hash_wia = HashWithIndifferentAccess.new { 1 + 2 } - assert_equal 3, hash_wia[:new_key] - end - - def test_should_return_nil_if_no_key_is_supplied - hash_wia = HashWithIndifferentAccess.new { 1 + 2 } - assert_nil hash_wia.default - end - - def test_should_use_default_value_for_unknown_key - hash_wia = HashWithIndifferentAccess.new(3) - assert_equal 3, hash_wia[:new_key] - end - - def test_should_use_default_value_if_no_key_is_supplied - hash_wia = HashWithIndifferentAccess.new(3) - assert_equal 3, hash_wia.default - end - - def test_should_nil_if_no_default_value_is_supplied - hash_wia = HashWithIndifferentAccess.new - assert_nil hash_wia.default - end - - def test_should_return_dup_for_with_indifferent_access - hash_wia = HashWithIndifferentAccess.new - assert_equal hash_wia, hash_wia.with_indifferent_access - assert_not_same hash_wia, hash_wia.with_indifferent_access - end - - def test_allows_setting_frozen_array_values_with_indifferent_access - value = [1, 2, 3].freeze - hash = HashWithIndifferentAccess.new - hash[:key] = value - assert_equal hash[:key], value - end - - def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access - hash = Hash.new(3) - hash_wia = hash.with_indifferent_access - assert_equal 3, hash_wia.default - end - - def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access - hash = Hash.new do - 2 + 1 - end - assert_equal 3, hash[:foo] - - hash_wia = hash.with_indifferent_access - assert_equal 3, hash_wia[:foo] - assert_equal 3, hash_wia[:bar] - end - # The XML builder seems to fail miserably when trying to tag something # with the same name as a Kernel method (throw, test, loop, select ...) def test_kernel_method_names_to_xml diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index a17438bf4d..085fd6592d 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -70,7 +70,23 @@ Product = Struct.new(:name) do end end +module ExtraMissing + def method_missing(sym, *args) + if sym == :extra_missing + 42 + else + super + end + end + + def respond_to_missing?(sym, priv = false) + sym == :extra_missing || super + end +end + DecoratedTester = Struct.new(:client) do + include ExtraMissing + delegate_missing_to :client end @@ -356,6 +372,22 @@ class ModuleTest < ActiveSupport::TestCase assert_match(/undefined method `my_fake_method' for/, e.message) end + def test_delegate_to_missing_affects_respond_to + assert DecoratedTester.new(@david).respond_to?(:name) + assert_not DecoratedTester.new(@david).respond_to?(:private_name) + assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method) + + assert DecoratedTester.new(@david).respond_to?(:name, true) + assert_not DecoratedTester.new(@david).respond_to?(:private_name, true) + assert_not DecoratedTester.new(@david).respond_to?(:my_fake_method, true) + end + + def test_delegate_to_missing_respects_superclass_missing + assert_equal 42, DecoratedTester.new(@david).extra_missing + + assert_respond_to DecoratedTester.new(@david), :extra_missing + end + def test_delegate_with_case event = Event.new(Tester.new) assert_equal 1, event.foo diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 41cc9888c6..a98951e889 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -78,6 +78,12 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_titleize_with_keep_id_suffix + MixtureToTitleCaseWithKeepIdSuffix.each do |before, titleized| + assert_equal(titleized, before.titleize(keep_id_suffix: true)) + end + end + def test_upcase_first assert_equal "What a Lovely Day", "what a Lovely Day".upcase_first end @@ -199,6 +205,12 @@ class StringInflectionsTest < ActiveSupport::TestCase end end + def test_humanize_with_keep_id_suffix + UnderscoreToHumanWithKeepIdSuffix.each do |underscore, human| + assert_equal(human, underscore.humanize(keep_id_suffix: true)) + end + end + def test_humanize_with_html_escape assert_equal "Hello", ERB::Util.html_escape("hello").humanize end diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb new file mode 100644 index 0000000000..b1177c6c15 --- /dev/null +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -0,0 +1,733 @@ +require "abstract_unit" +require "active_support/core_ext/hash" +require "bigdecimal" +require "active_support/core_ext/string/access" +require "active_support/ordered_hash" +require "active_support/core_ext/object/conversions" +require "active_support/core_ext/object/deep_dup" +require "active_support/inflections" + +class HashWithIndifferentAccessTest < ActiveSupport::TestCase + HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess + + class IndifferentHash < ActiveSupport::HashWithIndifferentAccess + end + + class SubclassingArray < Array + end + + class SubclassingHash < Hash + end + + class NonIndifferentHash < Hash + def nested_under_indifferent_access + self + end + end + + class HashByConversion + def initialize(hash) + @hash = hash + end + + def to_hash + @hash + end + end + + def setup + @strings = { "a" => 1, "b" => 2 } + @nested_strings = { "a" => { "b" => { "c" => 3 } } } + @symbols = { a: 1, b: 2 } + @nested_symbols = { a: { b: { c: 3 } } } + @mixed = { :a => 1, "b" => 2 } + @nested_mixed = { "a" => { b: { "c" => 3 } } } + @integers = { 0 => 1, 1 => 2 } + @nested_integers = { 0 => { 1 => { 2 => 3 } } } + @illegal_symbols = { [] => 3 } + @nested_illegal_symbols = { [] => { [] => 3 } } + end + + def test_symbolize_keys_for_hash_with_indifferent_access + assert_instance_of Hash, @symbols.with_indifferent_access.symbolize_keys + assert_equal @symbols, @symbols.with_indifferent_access.symbolize_keys + assert_equal @symbols, @strings.with_indifferent_access.symbolize_keys + assert_equal @symbols, @mixed.with_indifferent_access.symbolize_keys + end + + def test_deep_symbolize_keys_for_hash_with_indifferent_access + assert_instance_of Hash, @nested_symbols.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_symbols.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_strings.with_indifferent_access.deep_symbolize_keys + assert_equal @nested_symbols, @nested_mixed.with_indifferent_access.deep_symbolize_keys + end + + def test_symbolize_keys_bang_for_hash_with_indifferent_access + assert_raise(NoMethodError) { @symbols.with_indifferent_access.dup.symbolize_keys! } + assert_raise(NoMethodError) { @strings.with_indifferent_access.dup.symbolize_keys! } + assert_raise(NoMethodError) { @mixed.with_indifferent_access.dup.symbolize_keys! } + end + + def test_deep_symbolize_keys_bang_for_hash_with_indifferent_access + assert_raise(NoMethodError) { @nested_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } + assert_raise(NoMethodError) { @nested_strings.with_indifferent_access.deep_dup.deep_symbolize_keys! } + assert_raise(NoMethodError) { @nested_mixed.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + + def test_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access + assert_equal @illegal_symbols, @illegal_symbols.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @illegal_symbols.with_indifferent_access.dup.symbolize_keys! } + end + + def test_deep_symbolize_keys_preserves_keys_that_cant_be_symbolized_for_hash_with_indifferent_access + assert_equal @nested_illegal_symbols, @nested_illegal_symbols.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_illegal_symbols.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + + def test_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access + assert_equal @integers, @integers.with_indifferent_access.symbolize_keys + assert_raise(NoMethodError) { @integers.with_indifferent_access.dup.symbolize_keys! } + end + + def test_deep_symbolize_keys_preserves_integer_keys_for_hash_with_indifferent_access + assert_equal @nested_integers, @nested_integers.with_indifferent_access.deep_symbolize_keys + assert_raise(NoMethodError) { @nested_integers.with_indifferent_access.deep_dup.deep_symbolize_keys! } + end + + def test_stringify_keys_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.stringify_keys + assert_equal @strings, @symbols.with_indifferent_access.stringify_keys + assert_equal @strings, @strings.with_indifferent_access.stringify_keys + assert_equal @strings, @mixed.with_indifferent_access.stringify_keys + end + + def test_deep_stringify_keys_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_stringify_keys + assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_stringify_keys + end + + def test_stringify_keys_bang_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @symbols.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @symbols.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @strings.with_indifferent_access.dup.stringify_keys! + assert_equal @strings, @mixed.with_indifferent_access.dup.stringify_keys! + end + + def test_deep_stringify_keys_bang_for_hash_with_indifferent_access + assert_instance_of ActiveSupport::HashWithIndifferentAccess, @nested_symbols.with_indifferent_access.dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_symbols.with_indifferent_access.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_strings.with_indifferent_access.deep_dup.deep_stringify_keys! + assert_equal @nested_strings, @nested_mixed.with_indifferent_access.deep_dup.deep_stringify_keys! + end + + def test_nested_under_indifferent_access + foo = { "foo" => SubclassingHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of ActiveSupport::HashWithIndifferentAccess, foo["foo"] + + foo = { "foo" => NonIndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of NonIndifferentHash, foo["foo"] + + foo = { "foo" => IndifferentHash.new.tap { |h| h["bar"] = "baz" } }.with_indifferent_access + assert_kind_of IndifferentHash, foo["foo"] + end + + def test_indifferent_assorted + @strings = @strings.with_indifferent_access + @symbols = @symbols.with_indifferent_access + @mixed = @mixed.with_indifferent_access + + assert_equal "a", @strings.__send__(:convert_key, :a) + + assert_equal 1, @strings.fetch("a") + assert_equal 1, @strings.fetch(:a.to_s) + assert_equal 1, @strings.fetch(:a) + + hashes = { :@strings => @strings, :@symbols => @symbols, :@mixed => @mixed } + method_map = { '[]': 1, fetch: 1, values_at: [1], + has_key?: true, include?: true, key?: true, + member?: true } + + hashes.each do |name, hash| + method_map.sort_by(&:to_s).each do |meth, expected| + assert_equal(expected, hash.__send__(meth, "a"), + "Calling #{name}.#{meth} 'a'") + assert_equal(expected, hash.__send__(meth, :a), + "Calling #{name}.#{meth} :a") + end + end + + assert_equal [1, 2], @strings.values_at("a", "b") + assert_equal [1, 2], @strings.values_at(:a, :b) + assert_equal [1, 2], @symbols.values_at("a", "b") + assert_equal [1, 2], @symbols.values_at(:a, :b) + assert_equal [1, 2], @mixed.values_at("a", "b") + assert_equal [1, 2], @mixed.values_at(:a, :b) + end + + def test_indifferent_reading + hash = HashWithIndifferentAccess.new + hash["a"] = 1 + hash["b"] = true + hash["c"] = false + hash["d"] = nil + + assert_equal 1, hash[:a] + assert_equal true, hash[:b] + assert_equal false, hash[:c] + assert_nil hash[:d] + assert_nil hash[:e] + end + + def test_indifferent_reading_with_nonnil_default + hash = HashWithIndifferentAccess.new(1) + hash["a"] = 1 + hash["b"] = true + hash["c"] = false + hash["d"] = nil + + assert_equal 1, hash[:a] + assert_equal true, hash[:b] + assert_equal false, hash[:c] + assert_nil hash[:d] + assert_equal 1, hash[:e] + end + + def test_indifferent_writing + hash = HashWithIndifferentAccess.new + hash[:a] = 1 + hash["b"] = 2 + hash[3] = 3 + + assert_equal 1, hash["a"] + assert_equal 2, hash["b"] + assert_equal 1, hash[:a] + assert_equal 2, hash[:b] + assert_equal 3, hash[3] + end + + def test_indifferent_update + hash = HashWithIndifferentAccess.new + hash[:a] = "a" + hash["b"] = "b" + + updated_with_strings = hash.update(@strings) + updated_with_symbols = hash.update(@symbols) + updated_with_mixed = hash.update(@mixed) + + assert_equal 1, updated_with_strings[:a] + assert_equal 1, updated_with_strings["a"] + assert_equal 2, updated_with_strings["b"] + + assert_equal 1, updated_with_symbols[:a] + assert_equal 2, updated_with_symbols["b"] + assert_equal 2, updated_with_symbols[:b] + + assert_equal 1, updated_with_mixed[:a] + assert_equal 2, updated_with_mixed["b"] + + assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 } + end + + def test_update_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + hash.update HashByConversion.new(a: 1) + assert_equal 1, hash["a"] + end + + def test_indifferent_merging + hash = HashWithIndifferentAccess.new + hash[:a] = "failure" + hash["b"] = "failure" + + other = { "a" => 1, :b => 2 } + + merged = hash.merge(other) + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 1, merged[:a] + assert_equal 2, merged["b"] + + hash.update(other) + + assert_equal 1, hash[:a] + assert_equal 2, hash["b"] + end + + def test_merge_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + merged = hash.merge HashByConversion.new(a: 1) + assert_equal 1, merged["a"] + end + + def test_indifferent_replace + hash = HashWithIndifferentAccess.new + hash[:a] = 42 + + replaced = hash.replace(b: 12) + + assert hash.key?("b") + assert !hash.key?(:a) + assert_equal 12, hash[:b] + assert_same hash, replaced + end + + def test_replace_with_to_hash_conversion + hash = HashWithIndifferentAccess.new + hash[:a] = 42 + + replaced = hash.replace(HashByConversion.new(b: 12)) + + assert hash.key?("b") + assert !hash.key?(:a) + assert_equal 12, hash[:b] + assert_same hash, replaced + end + + def test_indifferent_merging_with_block + hash = HashWithIndifferentAccess.new + hash[:a] = 1 + hash["b"] = 3 + + other = { "a" => 4, :b => 2, "c" => 10 } + + merged = hash.merge(other) { |key, old, new| old > new ? old : new } + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 4, merged[:a] + assert_equal 3, merged["b"] + assert_equal 10, merged[:c] + + other_indifferent = HashWithIndifferentAccess.new("a" => 9, :b => 2) + + merged = hash.merge(other_indifferent) { |key, old, new| old + new } + + assert_equal HashWithIndifferentAccess, merged.class + assert_equal 10, merged[:a] + assert_equal 5, merged[:b] + end + + def test_indifferent_reverse_merging + hash = HashWithIndifferentAccess.new key: :old_value + hash.reverse_merge! key: :new_value + assert_equal :old_value, hash[:key] + + hash = HashWithIndifferentAccess.new("some" => "value", "other" => "value") + hash.reverse_merge!(some: "noclobber", another: "clobber") + assert_equal "value", hash[:some] + assert_equal "clobber", hash[:another] + end + + def test_indifferent_with_defaults_aliases_reverse_merge + hash = HashWithIndifferentAccess.new key: :old_value + actual = hash.with_defaults key: :new_value + assert_equal :old_value, actual[:key] + + hash = HashWithIndifferentAccess.new key: :old_value + hash.with_defaults! key: :new_value + assert_equal :old_value, hash[:key] + end + + def test_indifferent_deleting + get_hash = proc { { a: "foo" }.with_indifferent_access } + hash = get_hash.call + assert_equal "foo", hash.delete(:a) + assert_nil hash.delete(:a) + hash = get_hash.call + assert_equal "foo", hash.delete("a") + assert_nil hash.delete("a") + end + + def test_indifferent_select + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| v == 1 } + + assert_equal({ "a" => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_select_returns_enumerator + enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).select + assert_instance_of Enumerator, enum + end + + def test_indifferent_select_returns_a_hash_when_unchanged + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select { |k, v| true } + + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_select_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.select! { |k, v| v == 1 } + + assert_equal({ "a" => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + + def test_indifferent_reject + hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject { |k, v| v != 1 } + + assert_equal({ "a" => 1 }, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash + end + + def test_indifferent_reject_returns_enumerator + enum = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject + assert_instance_of Enumerator, enum + end + + def test_indifferent_reject_bang + indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings) + indifferent_strings.reject! { |k, v| v != 1 } + + assert_equal({ "a" => 1 }, indifferent_strings) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings + end + + def test_indifferent_compact + hash_contain_nil_value = @strings.merge("z" => nil) + hash = ActiveSupport::HashWithIndifferentAccess.new(hash_contain_nil_value) + compacted_hash = hash.compact + + assert_equal(@strings, compacted_hash) + assert_equal(hash_contain_nil_value, hash) + assert_instance_of ActiveSupport::HashWithIndifferentAccess, compacted_hash + + empty_hash = ActiveSupport::HashWithIndifferentAccess.new + compacted_hash = empty_hash.compact + + assert_equal compacted_hash, empty_hash + + non_empty_hash = ActiveSupport::HashWithIndifferentAccess.new(foo: :bar) + compacted_hash = non_empty_hash.compact + + assert_equal compacted_hash, non_empty_hash + end + + def test_indifferent_to_hash + # Should convert to a Hash with String keys. + assert_equal @strings, @mixed.with_indifferent_access.to_hash + + # Should preserve the default value. + mixed_with_default = @mixed.dup + mixed_with_default.default = "1234" + roundtrip = mixed_with_default.with_indifferent_access.to_hash + assert_equal @strings, roundtrip + assert_equal "1234", roundtrip.default + + # Ensure nested hashes are not HashWithIndiffereneAccess + new_to_hash = @nested_mixed.with_indifferent_access.to_hash + assert_not new_to_hash.instance_of?(HashWithIndifferentAccess) + assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess) + assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess) + end + + def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access + hash = HashWithIndifferentAccess.new { |h, k| h[k] = [] } + hash[:a] << 1 + + assert_equal [1], hash[:a] + end + + def test_with_indifferent_access_has_no_side_effects_on_existing_hash + hash = { content: [{ :foo => :bar, "bar" => "baz" }] } + hash.with_indifferent_access + + assert_equal [:foo, "bar"], hash[:content].first.keys + end + + def test_indifferent_hash_with_array_of_hashes + hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] } }.with_indifferent_access + assert_equal "1", hash[:urls][:url].first[:address] + + hash = hash.to_hash + assert_not hash.instance_of?(HashWithIndifferentAccess) + assert_not hash["urls"].instance_of?(HashWithIndifferentAccess) + assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess) + end + + def test_should_preserve_array_subclass_when_value_is_array + array = SubclassingArray.new + array << { "address" => "1" } + hash = { "urls" => { "url" => array } }.with_indifferent_access + assert_equal SubclassingArray, hash[:urls][:url].class + end + + def test_should_preserve_array_class_when_hash_value_is_frozen_array + array = SubclassingArray.new + array << { "address" => "1" } + hash = { "urls" => { "url" => array.freeze } }.with_indifferent_access + assert_equal SubclassingArray, hash[:urls][:url].class + end + + def test_stringify_and_symbolize_keys_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h[:first] = 1 + h = h.stringify_keys + assert_equal 1, h["first"] + h = HashWithIndifferentAccess.new + h["first"] = 1 + h = h.symbolize_keys + assert_equal 1, h[:first] + end + + def test_deep_stringify_and_deep_symbolize_keys_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h[:first] = 1 + h = h.deep_stringify_keys + assert_equal 1, h["first"] + h = HashWithIndifferentAccess.new + h["first"] = 1 + h = h.deep_symbolize_keys + assert_equal 1, h[:first] + end + + def test_to_options_on_indifferent_preserves_hash + h = HashWithIndifferentAccess.new + h["first"] = 1 + h.to_options! + assert_equal 1, h["first"] + end + + def test_to_options_on_indifferent_preserves_works_as_hash_with_dup + h = HashWithIndifferentAccess.new(a: { b: "b" }) + dup = h.dup + + dup[:a][:c] = "c" + assert_equal "c", h[:a][:c] + end + + def test_indifferent_sub_hashes + h = { "user" => { "id" => 5 } }.with_indifferent_access + ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } + + h = { user: { id: 5 } }.with_indifferent_access + ["user", :user].each { |user| [:id, "id"].each { |id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5" } } + end + + def test_indifferent_duplication + # Should preserve default value + h = HashWithIndifferentAccess.new + h.default = "1234" + assert_equal h.default, h.dup.default + + # Should preserve class for subclasses + h = IndifferentHash.new + assert_equal h.class, h.dup.class + end + + def test_nested_dig_indifferent_access + skip if RUBY_VERSION < "2.3.0" + data = { "this" => { "views" => 1234 } }.with_indifferent_access + assert_equal 1234, data.dig(:this, :views) + end + + def test_assorted_keys_not_stringified + original = { Object.new => 2, 1 => 2, [] => true } + indiff = original.with_indifferent_access + assert(!indiff.keys.any? { |k| k.kind_of? String }, "A key was converted to a string!") + end + + def test_deep_merge_on_indifferent_access + hash_1 = HashWithIndifferentAccess.new(a: "a", b: "b", c: { c1: "c1", c2: "c2", c3: { d1: "d1" } }) + hash_2 = HashWithIndifferentAccess.new(a: 1, c: { c1: 2, c3: { d2: "d2" } }) + hash_3 = { a: 1, c: { c1: 2, c3: { d2: "d2" } } } + expected = { "a" => 1, "b" => "b", "c" => { "c1" => 2, "c2" => "c2", "c3" => { "d1" => "d1", "d2" => "d2" } } } + assert_equal expected, hash_1.deep_merge(hash_2) + assert_equal expected, hash_1.deep_merge(hash_3) + + hash_1.deep_merge!(hash_2) + assert_equal expected, hash_1 + end + + def test_store_on_indifferent_access + hash = HashWithIndifferentAccess.new + hash.store(:test1, 1) + hash.store("test1", 11) + hash[:test2] = 2 + hash["test2"] = 22 + expected = { "test1" => 11, "test2" => 22 } + assert_equal expected, hash + end + + def test_constructor_on_indifferent_access + hash = HashWithIndifferentAccess[:foo, 1] + assert_equal 1, hash[:foo] + assert_equal 1, hash["foo"] + hash[:foo] = 3 + assert_equal 3, hash[:foo] + assert_equal 3, hash["foo"] + end + + def test_indifferent_slice + original = { a: "x", b: "y", c: 10 }.with_indifferent_access + expected = { a: "x", b: "y" }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + # Should return a new hash with only the given keys. + assert_equal expected, original.slice(*keys), keys.inspect + assert_not_equal expected, original + end + end + + def test_indifferent_slice_inplace + original = { a: "x", b: "y", c: 10 }.with_indifferent_access + expected = { c: 10 }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + # Should replace the hash with only the given keys. + copy = original.dup + assert_equal expected, copy.slice!(*keys) + end + end + + def test_indifferent_slice_access_with_symbols + original = { "login" => "bender", "password" => "shiny", "stuff" => "foo" } + original = original.with_indifferent_access + + slice = original.slice(:login, :password) + + assert_equal "bender", slice[:login] + assert_equal "bender", slice["login"] + end + + def test_indifferent_extract + original = { :a => 1, "b" => 2, :c => 3, "d" => 4 }.with_indifferent_access + expected = { a: 1, b: 2 }.with_indifferent_access + remaining = { c: 3, d: 4 }.with_indifferent_access + + [["a", "b"], [:a, :b]].each do |keys| + copy = original.dup + assert_equal expected, copy.extract!(*keys) + assert_equal remaining, copy + end + end + + def test_new_with_to_hash_conversion + hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1)) + assert hash.key?("a") + assert_equal 1, hash[:a] + end + + def test_dup_with_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, v| raise "walrus" } + assert_nothing_raised { hash.dup } + end + + def test_dup_with_default_proc_sets_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| k + 1 } + new_hash = hash.dup + + assert_equal 3, new_hash[2] + + new_hash.default = 2 + assert_equal 2, new_hash[:non_existent] + end + + def test_to_hash_with_raising_default_proc + hash = HashWithIndifferentAccess.new + hash.default_proc = proc { |h, k| raise "walrus" } + + assert_nothing_raised { hash.to_hash } + end + + def test_new_with_to_hash_conversion_copies_default + normal_hash = Hash.new(3) + normal_hash[:a] = 1 + + hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) + assert_equal 1, hash[:a] + assert_equal 3, hash[:b] + end + + def test_new_with_to_hash_conversion_copies_default_proc + normal_hash = Hash.new { 1 + 2 } + normal_hash[:a] = 1 + + hash = HashWithIndifferentAccess.new(HashByConversion.new(normal_hash)) + assert_equal 1, hash[:a] + assert_equal 3, hash[:b] + end + + def test_inheriting_from_top_level_hash_with_indifferent_access_preserves_ancestors_chain + klass = Class.new(::HashWithIndifferentAccess) + assert_equal ActiveSupport::HashWithIndifferentAccess, klass.ancestors[1] + end + + def test_inheriting_from_hash_with_indifferent_access_properly_dumps_ivars + klass = Class.new(::HashWithIndifferentAccess) do + def initialize(*) + @foo = "bar" + super + end + end + + yaml_output = klass.new.to_yaml + + # `hash-with-ivars` was introduced in 2.0.9 (https://git.io/vyUQW) + if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("2.0.9") + assert_includes yaml_output, "hash-with-ivars" + assert_includes yaml_output, "@foo: bar" + else + assert_includes yaml_output, "hash" + end + end + + def test_should_use_default_proc_for_unknown_key + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_equal 3, hash_wia[:new_key] + end + + def test_should_return_nil_if_no_key_is_supplied + hash_wia = HashWithIndifferentAccess.new { 1 + 2 } + assert_nil hash_wia.default + end + + def test_should_use_default_value_for_unknown_key + hash_wia = HashWithIndifferentAccess.new(3) + assert_equal 3, hash_wia[:new_key] + end + + def test_should_use_default_value_if_no_key_is_supplied + hash_wia = HashWithIndifferentAccess.new(3) + assert_equal 3, hash_wia.default + end + + def test_should_nil_if_no_default_value_is_supplied + hash_wia = HashWithIndifferentAccess.new + assert_nil hash_wia.default + end + + def test_should_return_dup_for_with_indifferent_access + hash_wia = HashWithIndifferentAccess.new + assert_equal hash_wia, hash_wia.with_indifferent_access + assert_not_same hash_wia, hash_wia.with_indifferent_access + end + + def test_allows_setting_frozen_array_values_with_indifferent_access + value = [1, 2, 3].freeze + hash = HashWithIndifferentAccess.new + hash[:key] = value + assert_equal hash[:key], value + end + + def test_should_copy_the_default_value_when_converting_to_hash_with_indifferent_access + hash = Hash.new(3) + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia.default + end + + def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access + hash = Hash.new do + 2 + 1 + end + assert_equal 3, hash[:foo] + + hash_wia = hash.with_indifferent_access + assert_equal 3, hash_wia[:foo] + assert_equal 3, hash_wia[:bar] + end +end diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 03d7b3fe94..14bc10513b 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -119,6 +119,13 @@ class InflectorTest < ActiveSupport::TestCase end end + MixtureToTitleCaseWithKeepIdSuffix.each_with_index do |(before, titleized), index| + define_method "test_titleize_with_keep_id_suffix_mixture_to_title_case_#{index}" do + assert_equal(titleized, ActiveSupport::Inflector.titleize(before, keep_id_suffix: true), + "mixture to TitleCase with keep_id_suffix failed for #{before}") + end + end + def test_camelize CamelToUnderscore.each do |camel, underscore| assert_equal(camel, ActiveSupport::Inflector.camelize(underscore)) @@ -324,6 +331,12 @@ class InflectorTest < ActiveSupport::TestCase end end + def test_humanize_with_keep_id_suffix + UnderscoreToHumanWithKeepIdSuffix.each do |underscore, human| + assert_equal(human, ActiveSupport::Inflector.humanize(underscore, keep_id_suffix: true)) + end + end + def test_humanize_by_rule ActiveSupport::Inflector.inflections do |inflect| inflect.human(/_cnt$/i, '\1_count') diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb index f3352e3301..d61ca3fc18 100644 --- a/activesupport/test/inflector_test_cases.rb +++ b/activesupport/test/inflector_test_cases.rb @@ -248,12 +248,27 @@ module InflectorTestCases "_external_id" => "External" } + UnderscoreToHumanWithKeepIdSuffix = { + "this_is_a_string_ending_with_id" => "This is a string ending with id", + "employee_id" => "Employee id", + "employee_id_something_else" => "Employee id something else", + "underground" => "Underground", + "_id" => "Id", + "_external_id" => "External id" + } + UnderscoreToHumanWithoutCapitalize = { "employee_salary" => "employee salary", "employee_id" => "employee", "underground" => "underground" } + MixtureToTitleCaseWithKeepIdSuffix = { + "this_is_a_string_ending_with_id" => "This Is A String Ending With Id", + "EmployeeId" => "Employee Id", + "Author Id" => "Author Id" + } + MixtureToTitleCase = { "active_record" => "Active Record", "ActiveRecord" => "Active Record", diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 1615d8fdb2..de111cc40e 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -714,6 +714,10 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes ActiveSupport::TimeZone.country_zones(:ru), ActiveSupport::TimeZone["Kuala Lumpur"] end + def test_country_zones_without_mappings + assert_includes ActiveSupport::TimeZone.country_zones(:sv), ActiveSupport::TimeZone["America/El_Salvador"] + end + def test_to_yaml assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Pacific/Honolulu\n", ActiveSupport::TimeZone["Hawaii"].to_yaml) assert_equal("--- !ruby/object:ActiveSupport::TimeZone\nname: Europe/London\n", ActiveSupport::TimeZone["Europe/London"].to_yaml) |