diff options
author | Xavier Noria <fxn@hashref.com> | 2011-10-29 18:03:35 -0700 |
---|---|---|
committer | Xavier Noria <fxn@hashref.com> | 2011-10-29 18:10:45 -0700 |
commit | 11f6795b238172c4a13176062bd38b83285799b7 (patch) | |
tree | a70a883974bf91bbecb4ca11d26409243eaf9a30 | |
parent | 8ef1bd9b293678b3d9c07a3f63384781df78b6ad (diff) | |
download | rails-11f6795b238172c4a13176062bd38b83285799b7.tar.gz rails-11f6795b238172c4a13176062bd38b83285799b7.tar.bz2 rails-11f6795b238172c4a13176062bd38b83285799b7.zip |
defines Module#qualified_const_(defined?|get|set) and String#deconstantize
This commit also implements a faster version of #demodulize I was unable
to isolate with git add --patch.
Not a big fan of the name #deconstantize. It complements #demodulize
getting rid of the rightmost constant, hence the name, but it is
unrelated to the well-known #constantize. So unsure. Could not come
with anything better, please feel free to rename.
10 files changed, 311 insertions, 13 deletions
diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index d3a838cff0..8609198641 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,5 +1,11 @@ *Rails 3.2.0 (unreleased)* +* Module#qualified_const_(defined?|get|set) are analogous to the corresponding methods + in the standard API, but accept qualified constant names. [fxn] + +* Added inflection #deconstantize which complements #demodulize. This inflection + removes the righmost segment in a qualified constant name. [fxn] + * Added ActiveSupport:TaggedLogging that can wrap any standard Logger class to provide tagging capabilities [DHH] Logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb index 9fed346b7c..f399fce410 100644 --- a/activesupport/lib/active_support/core_ext/module.rb +++ b/activesupport/lib/active_support/core_ext/module.rb @@ -8,4 +8,5 @@ require 'active_support/core_ext/module/delegation' require 'active_support/core_ext/module/synchronization' require 'active_support/core_ext/module/deprecation' require 'active_support/core_ext/module/remove_method' -require 'active_support/core_ext/module/method_names'
\ No newline at end of file +require 'active_support/core_ext/module/method_names' +require 'active_support/core_ext/module/qualified_const'
\ No newline at end of file diff --git a/activesupport/lib/active_support/core_ext/module/qualified_const.rb b/activesupport/lib/active_support/core_ext/module/qualified_const.rb new file mode 100644 index 0000000000..d1a0ee2f83 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/module/qualified_const.rb @@ -0,0 +1,64 @@ +require 'active_support/core_ext/string/inflections' + +#-- +# Allows code reuse in the methods below without polluting Module. +#++ +module QualifiedConstUtils + def self.raise_if_absolute(path) + raise NameError, "wrong constant name #$&" if path =~ /\A::[^:]+/ + end + + def self.names(path) + path.split('::') + end +end + +## +# Extends the API for constants to be able to deal with qualified names. Arguments +# are assumed to be relative to the receiver. +# +#-- +# Qualified names are required to be relative because we are extending existing +# methods that expect constant names, ie, relative paths of length 1. For example, +# Object.const_get("::String") raises NameError and so does qualified_const_get. +#++ +class Module + if method(:const_defined?).arity == 1 + def qualified_const_defined?(path) + QualifiedConstUtils.raise_if_absolute(path) + + QualifiedConstUtils.names(path).inject(self) do |mod, name| + return unless mod.const_defined?(name) + mod.const_get(name) + end + return true + end + else + def qualified_const_defined?(path, search_parents=true) + QualifiedConstUtils.raise_if_absolute(path) + + QualifiedConstUtils.names(path).inject(self) do |mod, name| + return unless mod.const_defined?(name, search_parents) + mod.const_get(name) + end + return true + end + end + + def qualified_const_get(path) + QualifiedConstUtils.raise_if_absolute(path) + + QualifiedConstUtils.names(path).inject(self) do |mod, name| + mod.const_get(name) + end + end + + def qualified_const_set(path, value) + QualifiedConstUtils.raise_if_absolute(path) + + const_name = path.demodulize + mod_name = path.deconstantize + mod = mod_name.empty? ? self : qualified_const_get(mod_name) + mod.const_set(const_name, value) + end +end diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index fd91b3cacb..9356940650 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -117,10 +117,25 @@ class String # # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections" # "Inflections".demodulize # => "Inflections" + # + # See also +deconstatize+. def demodulize ActiveSupport::Inflector.demodulize(self) end + # Removes the rightmost segment from the constant expression in the string. + # + # "Net::HTTP".deconstantize # => "Net" + # "::Net::HTTP".deconstantize # => "::Net" + # "String".deconstantize # => "" + # "::String".deconstantize # => "" + # "".deconstantize # => "" + # + # See also +demodulize+. + def deconstantize + ActiveSupport::Inflector.deconstantize(self) + end + # Replaces special characters in a string so that it may be used as part of a 'pretty' URL. # # ==== Examples diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index c072a8e646..b3ac271778 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -5,6 +5,7 @@ require 'active_support/core_ext/module/aliasing' require 'active_support/core_ext/module/attribute_accessors' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/module/anonymous' +require 'active_support/core_ext/module/qualified_const' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/load_error' require 'active_support/core_ext/name_error' @@ -357,12 +358,13 @@ module ActiveSupport #:nodoc: end # Is the provided constant path defined? - def qualified_const_defined?(path) - names = path.sub(/^::/, '').to_s.split('::') - - names.inject(Object) do |mod, name| - return false unless local_const_defined?(mod, name) - mod.const_get name + if Module.method(:const_defined?).arity == 1 + def qualified_const_defined?(path) + Object.qualified_const_defined?(path.sub(/^::/, '')) + end + else + def qualified_const_defined?(path) + Object.qualified_const_defined?(path.sub(/^::/, ''), false) end end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 454637191e..e76ee60dd7 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -160,15 +160,32 @@ module ActiveSupport underscored_word.gsub(/_/, '-') end - # Removes the module part from the expression in the string. + # Removes the module part from the expression in the string: # - # Examples: # "ActiveRecord::CoreExtensions::String::Inflections".demodulize # => "Inflections" # "Inflections".demodulize # => "Inflections" - def demodulize(class_name_in_module) - # If you remove the module part of an empty string, you get an empty string. - # That's why the regexp uses the * quantifier. - class_name_in_module.to_s[/[^:]*\z/] + # + # See also +deconstantize+. + def demodulize(path) + path = path.to_s + if i = path.rindex('::') + path[(i+2)..-1] + else + path + end + end + + # Removes the rightmost segment from the constant expression in the string: + # + # "Net::HTTP".deconstantize # => "Net" + # "::Net::HTTP".deconstantize # => "::Net" + # "String".deconstantize # => "" + # "::String".deconstantize # => "" + # "".deconstantize # => "" + # + # See also +demodulize+. + def deconstantize(path) + path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename end # Creates a foreign key name from a class name. diff --git a/activesupport/test/core_ext/module/qualified_const_test.rb b/activesupport/test/core_ext/module/qualified_const_test.rb new file mode 100644 index 0000000000..8af0b9a023 --- /dev/null +++ b/activesupport/test/core_ext/module/qualified_const_test.rb @@ -0,0 +1,94 @@ +require 'abstract_unit' +require 'active_support/core_ext/module/qualified_const' + +module QualifiedConstTestMod + X = false + + module M + X = 1 + + class C + X = 2 + end + end + + module N + include M + end +end + +class QualifiedConstTest < ActiveSupport::TestCase + test "Object.qualified_const_defined?" do + assert Object.qualified_const_defined?("QualifiedConstTestMod") + assert !Object.qualified_const_defined?("NonExistingQualifiedConstTestMod") + + assert Object.qualified_const_defined?("QualifiedConstTestMod::X") + assert !Object.qualified_const_defined?("QualifiedConstTestMod::Y") + + assert Object.qualified_const_defined?("QualifiedConstTestMod::M::X") + assert !Object.qualified_const_defined?("QualifiedConstTestMod::M::Y") + + if Module.method(:const_defined?).arity == 1 + assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X") + else + assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X") + assert !Object.qualified_const_defined?("QualifiedConstTestMod::N::X", false) + assert Object.qualified_const_defined?("QualifiedConstTestMod::N::X", true) + end + end + + test "mod.qualified_const_defined?" do + assert QualifiedConstTestMod.qualified_const_defined?("M") + assert !QualifiedConstTestMod.qualified_const_defined?("NonExistingM") + + assert QualifiedConstTestMod.qualified_const_defined?("M::X") + assert !QualifiedConstTestMod.qualified_const_defined?("M::Y") + + assert QualifiedConstTestMod.qualified_const_defined?("M::C::X") + assert !QualifiedConstTestMod.qualified_const_defined?("M::C::Y") + + if Module.method(:const_defined?).arity == 1 + assert !QualifiedConstTestMod.qualified_const_defined?("QualifiedConstTestMod::N::X") + else + assert QualifiedConstTestMod.qualified_const_defined?("N::X") + assert !QualifiedConstTestMod.qualified_const_defined?("N::X", false) + assert QualifiedConstTestMod.qualified_const_defined?("N::X", true) + end + end + + test "qualified_const_get" do + assert_equal false, Object.qualified_const_get("QualifiedConstTestMod::X") + assert_equal false, QualifiedConstTestMod.qualified_const_get("X") + assert_equal 1, QualifiedConstTestMod.qualified_const_get("M::X") + assert_equal 1, QualifiedConstTestMod.qualified_const_get("N::X") + assert_equal 2, QualifiedConstTestMod.qualified_const_get("M::C::X") + + assert_raise(NameError) { QualifiedConstTestMod.qualified_const_get("M::C::Y")} + end + + test "qualified_const_set" do + m = Module.new + assert_equal m, Object.qualified_const_set("QualifiedConstTestMod2", m) + assert_equal m, ::QualifiedConstTestMod2 + + # We are going to assign to existing constants on purpose, so silence warnings. + silence_warnings do + assert_equal true, QualifiedConstTestMod.qualified_const_set("QualifiedConstTestMod::X", true) + assert_equal true, QualifiedConstTestMod::X + + assert_equal 10, QualifiedConstTestMod::M.qualified_const_set("X", 10) + assert_equal 10, QualifiedConstTestMod::M::X + end + end + + test "reject absolute paths" do + assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X")} + assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y")} + + assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X")} + assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y")} + + assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil)} + assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil)} + end +end diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 4d876954cf..ade09efc56 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -111,6 +111,10 @@ class StringInflectionsTest < Test::Unit::TestCase assert_equal "Account", "MyApplication::Billing::Account".demodulize end + def test_deconstantize + assert_equal "MyApplication::Billing", "MyApplication::Billing::Account".deconstantize + end + def test_foreign_key ClassNameToForeignKeyWithUnderscore.each do |klass, foreign_key| assert_equal(foreign_key, klass.foreign_key) diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index 7d15b3c7e5..6b7e839e43 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -198,6 +198,18 @@ class InflectorTest < Test::Unit::TestCase assert_equal "", ActiveSupport::Inflector.demodulize("") end + def test_deconstantize + assert_equal "MyApplication::Billing", ActiveSupport::Inflector.deconstantize("MyApplication::Billing::Account") + assert_equal "::MyApplication::Billing", ActiveSupport::Inflector.deconstantize("::MyApplication::Billing::Account") + + assert_equal "MyApplication", ActiveSupport::Inflector.deconstantize("MyApplication::Billing") + assert_equal "::MyApplication", ActiveSupport::Inflector.deconstantize("::MyApplication::Billing") + + assert_equal "", ActiveSupport::Inflector.deconstantize("Account") + assert_equal "", ActiveSupport::Inflector.deconstantize("::Account") + assert_equal "", ActiveSupport::Inflector.deconstantize("") + end + def test_foreign_key ClassNameToForeignKeyWithUnderscore.each do |klass, foreign_key| assert_equal(foreign_key, ActiveSupport::Inflector.foreign_key(klass)) diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index c04e49281e..0941953d1c 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -725,6 +725,64 @@ WARNING: This method returns precise results in Ruby 1.9. In older versions of R NOTE: Defined in +active_support/core_ext/module/introspection.rb+. +h5. Qualified Constant Names + +The standard methods +const_defined?+, +const_get+ , and +const_set+ accept +bare constant names. Active Support extends this API to be able to pass +relative qualified constant reference expressions. + +The new methods are +qualified_const_defined?+, +qualified_const_get+, and ++qualified_const_set+. Their arguments are assumed to be qualified constant +names relative to their receiver: + +<ruby> +Object.qualified_const_defined?("Math::PI") # => true +Object.qualified_const_get("Math::PI") # => 3.141592653589793 +Object.qualified_const_set("Math::Phi", 1.618034) # => 1.618034 +</ruby> + +Arguments may be bare constant names: + +<ruby> +Math.qualified_const_get("E") # => 2.718281828459045 +</ruby> + +These methods are analogous to their builtin counterparts. In particular, ++qualified_constant_defined?+ accepts an optional second argument in 1.9 +to be able to say whether you want the predicate to look in the ancestors. +This flag is taken into account for each constant in the expression while +walking down the path. + +For example, given + +<ruby> +module M + X = 1 +end + +module N + class C + include M + end +end +</ruby> + ++qualified_const_defined?+ behaves this way: + +<ruby> +N.qualified_const_defined?("C::X", false) # => false (1.9 only) +N.qualified_const_defined?("C::X", true) # => true (1.9 only) +N.qualified_const_defined?("C::X") # => false in 1.8, true in 1.9 +</ruby> + +As the last example implies, in 1.9 the second argument defaults to true, +as in +const_defined?+. + +For coherence with the builtin methods only relative paths are accepted. +Absolute qualified constant names like +::Math::PI+ raise +NameError+. + +NOTE: Defined in +active_support/core_ext/module/qualified_const.rb+. + h4. Synchronization The +synchronize+ macro declares a method to be synchronized: @@ -1620,6 +1678,31 @@ end NOTE: Defined in +active_support/core_ext/string/inflections.rb+. +h5. +deconstantize+ + +Given a string with a qualified constant reference expression, +deconstantize+ removes the rightmost segment, generally leaving the name of the constant's container: + +<ruby> +"Product".deconstantize # => "" +"Backoffice::UsersController".deconstantize # => "Backoffice" +"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel" +</ruby> + +Active Support for example uses this method in +Module#qualified_const_set+: + +<ruby> +def qualified_const_set(path, value) + QualifiedConstUtils.raise_if_absolute(path) + + const_name = path.demodulize + mod_name = path.deconstantize + mod = mod_name.empty? ? self : qualified_const_get(mod_name) + mod.const_set(const_name, value) +end +</ruby> + +NOTE: Defined in +active_support/core_ext/string/inflections.rb+. + h5. +parameterize+ The method +parameterize+ normalizes its receiver in a way that can be used in pretty URLs. |