From 06f067506854d9fbecf9694c210468323208d6bb Mon Sep 17 00:00:00 2001 From: Tomas Valent Date: Tue, 20 Feb 2018 10:38:19 +0100 Subject: [PATCH] add private: true option for ActiveSupport delegate --- activesupport/CHANGELOG.md | 37 ++++++++++++ .../core_ext/module/delegation.rb | 25 +++++++- activesupport/test/core_ext/module_test.rb | 57 +++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 66b7365916..190ec4e6de 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,5 +1,42 @@ ## Rails 6.0.0.alpha (Unreleased) ## +* Add `private: true` option to ActiveSupport's `delegate`. + + In order to delegate methods as private methods: + + class User < ActiveRecord::Base + has_one :profile + delegate :date_of_birth, to: :profile, private: true + + def age + Date.today.year - date_of_birth.year + end + end + + # User.new.age # => 29 + # User.new.date_of_birth + # => NoMethodError: private method `date_of_birth' called for # + + More information in #31944. + + *Tomas Valent* + +* Return all mappings for a timezone identifier in `country_zones` + + Some timezones like `Europe/London` have multiple mappings in + `ActiveSupport::TimeZone::MAPPING` so return all of them instead + of the first one found by using `Hash#value`. e.g: + + # Before + ActiveSupport::TimeZone.country_zones("GB") # => ["Edinburgh"] + + # After + ActiveSupport::TimeZone.country_zones("GB") # => ["Edinburgh", "London"] + + Fixes #31668. + + *Andrew White* + * `String#truncate_bytes` to truncate a string to a maximum bytesize without breaking multibyte characters or grapheme clusters like 👩‍👩‍👦‍👦. diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 4310df3024..426e34128f 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -114,6 +114,24 @@ class Module # invoice.customer_name # => 'John Doe' # invoice.customer_address # => 'Vimmersvej 13' # + # If you want the delegated method to be a private method, + # use the :private option. + # + # class User < ActiveRecord::Base + # has_one :profile + # delegate :first_name, to: :profile + # delegate :date_of_birth, :religion, to: :profile, private: true + # + # def age + # Date.today.year - date_of_birth.year + # end + # end + # + # User.new.age # 2 + # User.new.first_name # Tomas + # User.new.date_of_birth # NoMethodError: private method `date_of_birth' called for # + # User.new.religion # NoMethodError: private method `religion' called for # + # # If the target is +nil+ and does not respond to the delegated method a # +Module::DelegationError+ is raised. If you wish to instead return +nil+, # use the :allow_nil option. @@ -151,7 +169,7 @@ 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, to: nil, prefix: nil, allow_nil: nil) + def delegate(*methods, to: nil, prefix: nil, allow_nil: nil, private: nil) unless to raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter)." end @@ -173,7 +191,7 @@ class Module to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) - methods.map do |method| + method_names = methods.map do |method| # Attribute writer methods only accept one argument. Makes sure []= # methods still accept two arguments. definition = /[^\]]=$/.match?(method) ? "arg" : "*args, &block" @@ -213,6 +231,9 @@ class Module module_eval(method_def, file, line) end + + private(*method_names) if private + method_names end # When building decorators, a common pattern may emerge: diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index ebbe9c304c..36cfd885d7 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -440,4 +440,61 @@ class ModuleTest < ActiveSupport::TestCase assert_not_respond_to place, :the_city assert place.respond_to?(:the_city, true) end + + def test_private_delegate_with_private_option + location = Class.new do + def initialize(place) + @place = place + end + + delegate(:street, :city, to: :@place, private: true) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_not_respond_to place, :street + assert_not_respond_to place, :city + + assert place.respond_to?(:street, true) # Asking for private method + assert place.respond_to?(:city, true) + end + + def test_some_public_some_private_delegate_with_private_option + location = Class.new do + def initialize(place) + @place = place + end + + delegate(:street, to: :@place) + delegate(:city, to: :@place, private: true) + end + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_respond_to place, :street + assert_not_respond_to place, :city + + assert place.respond_to?(:city, true) # Asking for private method + end + + def test_private_delegate_prefixed_with_private_option + location = Class.new do + def initialize(place) + @place = place + end + end + + assert_equal %i(the_street the_city), + location.delegate(:street, :city, to: :@place, prefix: :the, private: true) + + place = location.new(Somewhere.new("Such street", "Sad city")) + + assert_not_respond_to place, :street + assert_not_respond_to place, :city + + assert_not_respond_to place, :the_street + assert place.respond_to?(:the_street, true) + assert_not_respond_to place, :the_city + assert place.respond_to?(:the_city, true) + end end