├── .github ├── dependabot.yml └── workflows │ ├── push_gem.yml │ └── test.yml ├── .gitignore ├── BSDL ├── COPYING ├── Gemfile ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── delegate.gemspec ├── lib └── delegate.rb └── test ├── lib └── helper.rb └── test_delegate.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/push_gem.yml: -------------------------------------------------------------------------------- 1 | name: Publish gem to rubygems.org 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository == 'ruby/delegate' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/delegate 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.0 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.MATZBOT_GITHUB_WORKFLOW_TOKEN }} 47 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 3.0 11 | test: 12 | needs: ruby-versions 13 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 14 | strategy: 15 | matrix: 16 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 17 | os: [ ubuntu-latest, macos-latest ] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Ruby 22 | uses: ruby/setup-ruby@v1 23 | with: 24 | ruby-version: ${{ matrix.ruby }} 25 | bundler-cache: true # 'bundle install' and cache gems 26 | - name: Run test 27 | run: bundle exec rake test 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /Gemfile.lock 10 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | gem "test-unit-ruby-core" 10 | end 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delegator 2 | 3 | This library provides three different ways to delegate method calls to an 4 | object. The easiest to use is SimpleDelegator. Pass an object to the 5 | constructor and all methods supported by the object will be delegated. This 6 | object can be changed later. 7 | 8 | Going a step further, the top level DelegateClass method allows you to easily 9 | setup delegation through class inheritance. This is considerably more 10 | flexible and thus probably the most common use for this library. 11 | 12 | Finally, if you need full control over the delegation scheme, you can inherit 13 | from the abstract class Delegator and customize as needed. (If you find 14 | yourself needing this control, have a look at Forwardable which is also in 15 | the standard library. It may suit your needs better.) 16 | 17 | ## Installation 18 | 19 | Add this line to your application's Gemfile: 20 | 21 | ```ruby 22 | gem 'delegate' 23 | ``` 24 | 25 | And then execute: 26 | 27 | $ bundle 28 | 29 | Or install it yourself as: 30 | 31 | $ gem install delegate 32 | 33 | ## Usage 34 | 35 | SimpleDelegator's implementation serves as a nice example of the use of 36 | Delegator: 37 | 38 | ```ruby 39 | class SimpleDelegator < Delegator 40 | def __getobj__ 41 | @delegate_sd_obj # return object we are delegating to, required 42 | end 43 | 44 | def __setobj__(obj) 45 | @delegate_sd_obj = obj # change delegation object, 46 | # a feature we're providing 47 | end 48 | end 49 | ``` 50 | 51 | ## Development 52 | 53 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 54 | 55 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 56 | 57 | ## Contributing 58 | 59 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/delegate. 60 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "delegate" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /delegate.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-")+1, ".").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb")) do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Provides three abilities to delegate method calls to an object.} 17 | spec.description = %q{Provides three abilities to delegate method calls to an object.} 18 | spec.homepage = "https://github.com/ruby/delegate" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | 21 | spec.metadata["homepage_uri"] = spec.homepage 22 | spec.metadata["source_code_uri"] = spec.homepage 23 | 24 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 25 | `git ls-files -z 2>#{IO::NULL}`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 26 | end 27 | spec.require_paths = ["lib"] 28 | spec.required_ruby_version = '>= 3.0' 29 | end 30 | -------------------------------------------------------------------------------- /lib/delegate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # = delegate -- Support for the Delegation Pattern 3 | # 4 | # Documentation by James Edward Gray II and Gavin Sinclair 5 | 6 | ## 7 | # This library provides three different ways to delegate method calls to an 8 | # object. The easiest to use is SimpleDelegator. Pass an object to the 9 | # constructor and all methods supported by the object will be delegated. This 10 | # object can be changed later. 11 | # 12 | # Going a step further, the top level DelegateClass method allows you to easily 13 | # setup delegation through class inheritance. This is considerably more 14 | # flexible and thus probably the most common use for this library. 15 | # 16 | # Finally, if you need full control over the delegation scheme, you can inherit 17 | # from the abstract class Delegator and customize as needed. (If you find 18 | # yourself needing this control, have a look at Forwardable which is also in 19 | # the standard library. It may suit your needs better.) 20 | # 21 | # SimpleDelegator's implementation serves as a nice example of the use of 22 | # Delegator: 23 | # 24 | # require 'delegate' 25 | # 26 | # class SimpleDelegator < Delegator 27 | # def __getobj__ 28 | # @delegate_sd_obj # return object we are delegating to, required 29 | # end 30 | # 31 | # def __setobj__(obj) 32 | # @delegate_sd_obj = obj # change delegation object, 33 | # # a feature we're providing 34 | # end 35 | # end 36 | # 37 | # == Notes 38 | # 39 | # Be advised, RDoc will not detect delegated methods. 40 | # 41 | class Delegator < BasicObject 42 | VERSION = "0.4.0" 43 | 44 | kernel = ::Kernel.dup 45 | kernel.class_eval do 46 | alias __raise__ raise 47 | [:to_s, :inspect, :!~, :===, :<=>, :hash].each do |m| 48 | undef_method m 49 | end 50 | private_instance_methods.each do |m| 51 | if /\Ablock_given\?\z|\Aiterator\?\z|\A__.*__\z/ =~ m 52 | next 53 | end 54 | undef_method m 55 | end 56 | end 57 | include kernel 58 | 59 | # :stopdoc: 60 | def self.const_missing(n) 61 | ::Object.const_get(n) 62 | end 63 | # :startdoc: 64 | 65 | ## 66 | # :method: raise 67 | # Use #__raise__ if your Delegator does not have a object to delegate the 68 | # #raise method call. 69 | # 70 | 71 | # 72 | # Pass in the _obj_ to delegate method calls to. All methods supported by 73 | # _obj_ will be delegated to. 74 | # 75 | def initialize(obj) 76 | __setobj__(obj) 77 | end 78 | 79 | # 80 | # Handles the magic of delegation through \_\_getobj\_\_. 81 | # 82 | ruby2_keywords def method_missing(m, *args, &block) 83 | r = true 84 | target = self.__getobj__ {r = false} 85 | 86 | if r && target_respond_to?(target, m, false) 87 | target.__send__(m, *args, &block) 88 | elsif ::Kernel.method_defined?(m) || ::Kernel.private_method_defined?(m) 89 | ::Kernel.instance_method(m).bind_call(self, *args, &block) 90 | else 91 | super(m, *args, &block) 92 | end 93 | end 94 | 95 | # 96 | # Checks for a method provided by this the delegate object by forwarding the 97 | # call through \_\_getobj\_\_. 98 | # 99 | def respond_to_missing?(m, include_private) 100 | r = true 101 | target = self.__getobj__ {r = false} 102 | r &&= target_respond_to?(target, m, include_private) 103 | if r && include_private && !target_respond_to?(target, m, false) 104 | warn "delegator does not forward private method \##{m}", uplevel: 3 105 | return false 106 | end 107 | r 108 | end 109 | 110 | KERNEL_RESPOND_TO = ::Kernel.instance_method(:respond_to?) 111 | private_constant :KERNEL_RESPOND_TO 112 | 113 | # Handle BasicObject instances 114 | private def target_respond_to?(target, m, include_private) 115 | case target 116 | when Object 117 | target.respond_to?(m, include_private) 118 | else 119 | if KERNEL_RESPOND_TO.bind_call(target, :respond_to?) 120 | target.respond_to?(m, include_private) 121 | else 122 | KERNEL_RESPOND_TO.bind_call(target, m, include_private) 123 | end 124 | end 125 | end 126 | 127 | # 128 | # Returns the methods available to this delegate object as the union 129 | # of this object's and \_\_getobj\_\_ methods. 130 | # 131 | def methods(all=true) 132 | __getobj__.methods(all) | super 133 | end 134 | 135 | # 136 | # Returns the methods available to this delegate object as the union 137 | # of this object's and \_\_getobj\_\_ public methods. 138 | # 139 | def public_methods(all=true) 140 | __getobj__.public_methods(all) | super 141 | end 142 | 143 | # 144 | # Returns the methods available to this delegate object as the union 145 | # of this object's and \_\_getobj\_\_ protected methods. 146 | # 147 | def protected_methods(all=true) 148 | __getobj__.protected_methods(all) | super 149 | end 150 | 151 | # Note: no need to specialize private_methods, since they are not forwarded 152 | 153 | # 154 | # Returns true if two objects are considered of equal value. 155 | # 156 | def ==(obj) 157 | return true if obj.equal?(self) 158 | self.__getobj__ == obj 159 | end 160 | 161 | # 162 | # Returns true if two objects are not considered of equal value. 163 | # 164 | def !=(obj) 165 | return false if obj.equal?(self) 166 | __getobj__ != obj 167 | end 168 | 169 | # 170 | # Returns true if two objects are considered of equal value. 171 | # 172 | def eql?(obj) 173 | return true if obj.equal?(self) 174 | obj.eql?(__getobj__) 175 | end 176 | 177 | # 178 | # Delegates ! to the \_\_getobj\_\_ 179 | # 180 | def ! 181 | !__getobj__ 182 | end 183 | 184 | # 185 | # This method must be overridden by subclasses and should return the object 186 | # method calls are being delegated to. 187 | # 188 | def __getobj__ 189 | __raise__ ::NotImplementedError, "need to define '__getobj__'" 190 | end 191 | 192 | # 193 | # This method must be overridden by subclasses and change the object delegate 194 | # to _obj_. 195 | # 196 | def __setobj__(obj) 197 | __raise__ ::NotImplementedError, "need to define '__setobj__'" 198 | end 199 | 200 | # 201 | # Serialization support for the object returned by \_\_getobj\_\_. 202 | # 203 | def marshal_dump 204 | ivars = instance_variables.reject {|var| /\A@delegate_/ =~ var} 205 | [ 206 | :__v2__, 207 | ivars, ivars.map {|var| instance_variable_get(var)}, 208 | __getobj__ 209 | ] 210 | end 211 | 212 | # 213 | # Reinitializes delegation from a serialized object. 214 | # 215 | def marshal_load(data) 216 | version, vars, values, obj = data 217 | if version == :__v2__ 218 | vars.each_with_index {|var, i| instance_variable_set(var, values[i])} 219 | __setobj__(obj) 220 | else 221 | __setobj__(data) 222 | end 223 | end 224 | 225 | def initialize_clone(obj, freeze: nil) # :nodoc: 226 | self.__setobj__(obj.__getobj__.clone(freeze: freeze)) 227 | end 228 | def initialize_dup(obj) # :nodoc: 229 | self.__setobj__(obj.__getobj__.dup) 230 | end 231 | private :initialize_clone, :initialize_dup 232 | 233 | ## 234 | # :method: freeze 235 | # Freeze both the object returned by \_\_getobj\_\_ and self. 236 | # 237 | def freeze 238 | __getobj__.freeze 239 | super() 240 | end 241 | 242 | @delegator_api = self.public_instance_methods 243 | def self.public_api # :nodoc: 244 | @delegator_api 245 | end 246 | end 247 | 248 | ## 249 | # A concrete implementation of Delegator, this class provides the means to 250 | # delegate all supported method calls to the object passed into the constructor 251 | # and even to change the object being delegated to at a later time with 252 | # #__setobj__. 253 | # 254 | # class User 255 | # def born_on 256 | # Date.new(1989, 9, 10) 257 | # end 258 | # end 259 | # 260 | # require 'delegate' 261 | # 262 | # class UserDecorator < SimpleDelegator 263 | # def birth_year 264 | # born_on.year 265 | # end 266 | # end 267 | # 268 | # decorated_user = UserDecorator.new(User.new) 269 | # decorated_user.birth_year #=> 1989 270 | # decorated_user.__getobj__ #=> # 271 | # 272 | # A SimpleDelegator instance can take advantage of the fact that SimpleDelegator 273 | # is a subclass of +Delegator+ to call super to have methods called on 274 | # the object being delegated to. 275 | # 276 | # class SuperArray < SimpleDelegator 277 | # def [](*args) 278 | # super + 1 279 | # end 280 | # end 281 | # 282 | # SuperArray.new([1])[0] #=> 2 283 | # 284 | # Here's a simple example that takes advantage of the fact that 285 | # SimpleDelegator's delegation object can be changed at any time. 286 | # 287 | # class Stats 288 | # def initialize 289 | # @source = SimpleDelegator.new([]) 290 | # end 291 | # 292 | # def stats(records) 293 | # @source.__setobj__(records) 294 | # 295 | # "Elements: #{@source.size}\n" + 296 | # " Non-Nil: #{@source.compact.size}\n" + 297 | # " Unique: #{@source.uniq.size}\n" 298 | # end 299 | # end 300 | # 301 | # s = Stats.new 302 | # puts s.stats(%w{James Edward Gray II}) 303 | # puts 304 | # puts s.stats([1, 2, 3, nil, 4, 5, 1, 2]) 305 | # 306 | # Prints: 307 | # 308 | # Elements: 4 309 | # Non-Nil: 4 310 | # Unique: 4 311 | # 312 | # Elements: 8 313 | # Non-Nil: 7 314 | # Unique: 6 315 | # 316 | class SimpleDelegator < Delegator 317 | # Returns the current object method calls are being delegated to. 318 | def __getobj__ 319 | unless defined?(@delegate_sd_obj) 320 | return yield if block_given? 321 | __raise__ ::ArgumentError, "not delegated" 322 | end 323 | @delegate_sd_obj 324 | end 325 | 326 | # 327 | # Changes the delegate object to _obj_. 328 | # 329 | # It's important to note that this does *not* cause SimpleDelegator's methods 330 | # to change. Because of this, you probably only want to change delegation 331 | # to objects of the same type as the original delegate. 332 | # 333 | # Here's an example of changing the delegation object. 334 | # 335 | # names = SimpleDelegator.new(%w{James Edward Gray II}) 336 | # puts names[1] # => Edward 337 | # names.__setobj__(%w{Gavin Sinclair}) 338 | # puts names[1] # => Sinclair 339 | # 340 | def __setobj__(obj) 341 | __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj) 342 | @delegate_sd_obj = obj 343 | end 344 | end 345 | 346 | def Delegator.delegating_block(mid) # :nodoc: 347 | lambda do |*args, &block| 348 | target = self.__getobj__ 349 | target.__send__(mid, *args, &block) 350 | end.ruby2_keywords 351 | end 352 | 353 | # 354 | # The primary interface to this library. Use to setup delegation when defining 355 | # your class. 356 | # 357 | # class MyClass < DelegateClass(ClassToDelegateTo) # Step 1 358 | # def initialize 359 | # super(obj_of_ClassToDelegateTo) # Step 2 360 | # end 361 | # end 362 | # 363 | # or: 364 | # 365 | # MyClass = DelegateClass(ClassToDelegateTo) do # Step 1 366 | # def initialize 367 | # super(obj_of_ClassToDelegateTo) # Step 2 368 | # end 369 | # end 370 | # 371 | # Here's a sample of use from Tempfile which is really a File object with a 372 | # few special rules about storage location and when the File should be 373 | # deleted. That makes for an almost textbook perfect example of how to use 374 | # delegation. 375 | # 376 | # class Tempfile < DelegateClass(File) 377 | # # constant and class member data initialization... 378 | # 379 | # def initialize(basename, tmpdir=Dir::tmpdir) 380 | # # build up file path/name in var tmpname... 381 | # 382 | # @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600) 383 | # 384 | # # ... 385 | # 386 | # super(@tmpfile) 387 | # 388 | # # below this point, all methods of File are supported... 389 | # end 390 | # 391 | # # ... 392 | # end 393 | # 394 | def DelegateClass(superclass, &block) 395 | klass = Class.new(Delegator) 396 | ignores = [*::Delegator.public_api, :to_s, :inspect, :=~, :!~, :===] 397 | protected_instance_methods = superclass.protected_instance_methods 398 | protected_instance_methods -= ignores 399 | public_instance_methods = superclass.public_instance_methods 400 | public_instance_methods -= ignores 401 | klass.module_eval do 402 | def __getobj__ # :nodoc: 403 | unless defined?(@delegate_dc_obj) 404 | return yield if block_given? 405 | __raise__ ::ArgumentError, "not delegated" 406 | end 407 | @delegate_dc_obj 408 | end 409 | def __setobj__(obj) # :nodoc: 410 | __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj) 411 | @delegate_dc_obj = obj 412 | end 413 | protected_instance_methods.each do |method| 414 | define_method(method, Delegator.delegating_block(method)) 415 | protected method 416 | end 417 | public_instance_methods.each do |method| 418 | define_method(method, Delegator.delegating_block(method)) 419 | end 420 | end 421 | klass.define_singleton_method :public_instance_methods do |all=true| 422 | super(all) | superclass.public_instance_methods 423 | end 424 | klass.define_singleton_method :protected_instance_methods do |all=true| 425 | super(all) | superclass.protected_instance_methods 426 | end 427 | klass.define_singleton_method :instance_methods do |all=true| 428 | super(all) | superclass.instance_methods 429 | end 430 | klass.define_singleton_method :public_instance_method do |name| 431 | super(name) 432 | rescue NameError 433 | raise unless self.public_instance_methods.include?(name) 434 | superclass.public_instance_method(name) 435 | end 436 | klass.define_singleton_method :instance_method do |name| 437 | super(name) 438 | rescue NameError 439 | raise unless self.instance_methods.include?(name) 440 | superclass.instance_method(name) 441 | end 442 | klass.module_eval(&block) if block 443 | return klass 444 | end 445 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /test/test_delegate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'test/unit' 3 | require 'delegate' 4 | 5 | class TestDelegateClass < Test::Unit::TestCase 6 | module M 7 | attr_reader :m 8 | end 9 | 10 | def test_extend 11 | obj = DelegateClass(Array).new([]) 12 | obj.instance_eval { @m = :m } 13 | obj.extend M 14 | assert_equal(:m, obj.m, "[ruby-dev:33116]") 15 | end 16 | 17 | def test_delegate_class_block 18 | klass = DelegateClass(Array) do 19 | alias foo first 20 | end 21 | assert_equal(1, klass.new([1]).foo) 22 | end 23 | 24 | def test_systemcallerror_eq 25 | e = SystemCallError.new(0) 26 | assert((SimpleDelegator.new(e) == e) == (e == SimpleDelegator.new(e)), "[ruby-dev:34808]") 27 | end 28 | 29 | class Myclass < DelegateClass(Array);end 30 | 31 | def test_delegateclass_class 32 | myclass=Myclass.new([]) 33 | assert_equal(Myclass,myclass.class) 34 | assert_equal(Myclass,myclass.dup.class,'[ruby-dev:40313]') 35 | assert_equal(Myclass,myclass.clone.class,'[ruby-dev:40313]') 36 | end 37 | 38 | def test_simpledelegator_class 39 | simple=SimpleDelegator.new([]) 40 | assert_equal(SimpleDelegator,simple.class) 41 | assert_equal(SimpleDelegator,simple.dup.class) 42 | assert_equal(SimpleDelegator,simple.clone.class) 43 | end 44 | 45 | def test_simpledelegator_clone 46 | simple=SimpleDelegator.new([]) 47 | simple.freeze 48 | 49 | clone = simple.clone 50 | assert_predicate clone, :frozen? 51 | assert_predicate clone.__getobj__, :frozen? 52 | assert_equal true, Kernel.instance_method(:frozen?).bind(clone).call 53 | 54 | clone = simple.clone(freeze: false) 55 | assert_not_predicate clone, :frozen? 56 | assert_not_predicate clone.__getobj__, :frozen? 57 | assert_equal false, Kernel.instance_method(:frozen?).bind(clone).call 58 | end 59 | 60 | class Object 61 | def m 62 | :o 63 | end 64 | private 65 | def delegate_test_m 66 | :o 67 | end 68 | end 69 | 70 | class Foo 71 | def m 72 | :m 73 | end 74 | def delegate_test_m 75 | :m 76 | end 77 | end 78 | 79 | class Bar < DelegateClass(Foo) 80 | end 81 | 82 | def test_override 83 | foo = Foo.new 84 | foo2 = SimpleDelegator.new(foo) 85 | bar = Bar.new(foo) 86 | assert_equal(:o, Object.new.m) 87 | assert_equal(:m, foo.m) 88 | assert_equal(:m, foo2.m) 89 | assert_equal(:m, bar.m) 90 | bug = '[ruby-dev:39154]' 91 | assert_equal(:m, foo2.send(:delegate_test_m), bug) 92 | assert_equal(:m, bar.send(:delegate_test_m), bug) 93 | end 94 | 95 | class Parent 96 | def parent_public; end 97 | 98 | protected 99 | 100 | def parent_protected; end 101 | 102 | private 103 | 104 | def parent_private; end 105 | end 106 | 107 | class Child < DelegateClass(Parent) 108 | end 109 | 110 | class Parent 111 | def parent_public_added; end 112 | 113 | protected 114 | 115 | def parent_protected_added; end 116 | 117 | private 118 | 119 | def parent_private_added; end 120 | end 121 | 122 | def test_public_instance_methods 123 | ignores = Object.public_instance_methods | Delegator.public_instance_methods 124 | assert_equal([:parent_public, :parent_public_added], (Child.public_instance_methods - ignores).sort) 125 | assert_equal([:parent_public, :parent_public_added], (Child.new(Parent.new).public_methods - ignores).sort) 126 | end 127 | 128 | def test_protected_instance_methods 129 | ignores = Object.protected_instance_methods | Delegator.protected_instance_methods 130 | assert_equal([:parent_protected, :parent_protected_added], (Child.protected_instance_methods - ignores).sort) 131 | assert_equal([:parent_protected, :parent_protected_added], (Child.new(Parent.new).protected_methods - ignores).sort) 132 | end 133 | 134 | def test_instance_methods 135 | ignores = Object.instance_methods | Delegator.instance_methods 136 | assert_equal([:parent_protected, :parent_protected_added, :parent_public, :parent_public_added], (Child.instance_methods - ignores).sort) 137 | assert_equal([:parent_protected, :parent_protected_added, :parent_public, :parent_public_added], (Child.new(Parent.new).methods - ignores).sort) 138 | end 139 | 140 | def test_DelegateClass_instance_method 141 | assert_instance_of UnboundMethod, Child.instance_method(:parent_public) 142 | assert_instance_of UnboundMethod, Child.instance_method(:parent_public_added) 143 | assert_instance_of UnboundMethod, Child.instance_method(:parent_protected) 144 | assert_instance_of UnboundMethod, Child.instance_method(:parent_protected_added) 145 | assert_raise(NameError) { Child.instance_method(:parent_private) } 146 | assert_raise(NameError) { Child.instance_method(:parent_private_added) } 147 | assert_instance_of UnboundMethod, Child.instance_method(:to_s) 148 | end 149 | 150 | def test_DelegateClass_public_instance_method 151 | assert_instance_of UnboundMethod, Child.public_instance_method(:parent_public) 152 | assert_instance_of UnboundMethod, Child.public_instance_method(:parent_public_added) 153 | assert_raise(NameError) { Child.public_instance_method(:parent_protected) } 154 | assert_raise(NameError) { Child.public_instance_method(:parent_protected_added) } 155 | assert_raise(NameError) { Child.instance_method(:parent_private) } 156 | assert_raise(NameError) { Child.instance_method(:parent_private_added) } 157 | assert_instance_of UnboundMethod, Child.public_instance_method(:to_s) 158 | end 159 | 160 | class IV < DelegateClass(Integer) 161 | attr_accessor :var 162 | 163 | def initialize 164 | @var = 1 165 | super(0) 166 | end 167 | end 168 | 169 | def test_marshal 170 | bug1744 = '[ruby-core:24211]' 171 | c = IV.new 172 | assert_equal(1, c.var) 173 | d = Marshal.load(Marshal.dump(c)) 174 | assert_equal(1, d.var, bug1744) 175 | end 176 | 177 | def test_copy_frozen 178 | bug2679 = '[ruby-dev:40242]' 179 | a = [42, :hello].freeze 180 | d = SimpleDelegator.new(a) 181 | assert_nothing_raised(bug2679) {d.dup[0] += 1} 182 | assert_raise(FrozenError) {d.clone[0] += 1} 183 | d.freeze 184 | assert(d.clone.frozen?) 185 | assert(!d.dup.frozen?) 186 | end 187 | 188 | def test_frozen 189 | d = SimpleDelegator.new([1, :foo]) 190 | d.freeze 191 | assert_raise(FrozenError, '[ruby-dev:40314]#1') {d.__setobj__("foo")} 192 | assert_equal([1, :foo], d) 193 | end 194 | 195 | def test_instance_method 196 | s = SimpleDelegator.new("foo") 197 | m = s.method("upcase") 198 | s.__setobj__([1,2,3]) 199 | assert_raise(NoMethodError, '[ruby-dev:40314]#3') {m.call} 200 | end 201 | 202 | def test_methods 203 | s = SimpleDelegator.new("foo") 204 | assert_equal([], s.methods(false)) 205 | def s.bar; end 206 | assert_equal([:bar], s.methods(false)) 207 | end 208 | 209 | def test_eql? 210 | s0 = SimpleDelegator.new("foo") 211 | s1 = SimpleDelegator.new("bar") 212 | s2 = SimpleDelegator.new("foo") 213 | assert_operator(s0, :eql?, s0) 214 | assert_operator(s0, :eql?, "foo") 215 | assert_operator(s0, :eql?, s2) 216 | assert_not_operator(s0, :eql?, s1) 217 | assert_not_operator(s0, :eql?, "bar") 218 | end 219 | 220 | def test_keyword_and_hash 221 | foo = Object.new 222 | def foo.bar(*args) 223 | args 224 | end 225 | def foo.foo(*args, **kw) 226 | [args, kw] 227 | end 228 | d = SimpleDelegator.new(foo) 229 | assert_equal([[], {}], d.foo) 230 | assert_equal([], d.bar) 231 | assert_equal([[], {:a=>1}], d.foo(:a=>1)) 232 | assert_equal([{:a=>1}], d.bar(:a=>1)) 233 | assert_equal([[{:a=>1}], {}], d.foo({:a=>1})) 234 | assert_equal([{:a=>1}], d.bar({:a=>1})) 235 | end 236 | 237 | class Foo 238 | private 239 | def delegate_test_private 240 | :m 241 | end 242 | end 243 | 244 | def test_private_method 245 | foo = Foo.new 246 | d = SimpleDelegator.new(foo) 247 | assert_raise(NoMethodError) {foo.delegate_test_private} 248 | assert_equal(:m, foo.send(:delegate_test_private)) 249 | assert_raise(NoMethodError, '[ruby-dev:40314]#4') {d.delegate_test_private} 250 | assert_raise(NoMethodError, '[ruby-dev:40314]#5') {d.send(:delegate_test_private)} 251 | end 252 | 253 | def test_global_function 254 | klass = Class.new do 255 | def open 256 | end 257 | end 258 | obj = klass.new 259 | d = SimpleDelegator.new(obj) 260 | assert_nothing_raised(ArgumentError) {obj.open} 261 | assert_nothing_raised(ArgumentError) {d.open} 262 | assert_nothing_raised(ArgumentError) {d.send(:open)} 263 | end 264 | 265 | def test_send_method_in_delegator 266 | d = Class.new(SimpleDelegator) do 267 | def foo 268 | "foo" 269 | end 270 | end.new(Object.new) 271 | assert_equal("foo", d.send(:foo)) 272 | end 273 | 274 | def test_unset_simple_delegator 275 | d = SimpleDelegator.allocate 276 | assert_raise_with_message(ArgumentError, /not delegated/) { 277 | d.__getobj__ 278 | } 279 | end 280 | 281 | def test_unset_delegate_class 282 | d = IV.allocate 283 | assert_raise_with_message(ArgumentError, /not delegated/) { 284 | d.__getobj__ 285 | } 286 | end 287 | 288 | class Bug9155 < DelegateClass(Integer) 289 | def initialize(value) 290 | super(Integer(value)) 291 | end 292 | end 293 | 294 | def test_global_method_if_no_target 295 | bug9155 = '[ruby-core:58572] [Bug #9155]' 296 | x = assert_nothing_raised(ArgumentError, bug9155) {break Bug9155.new(1)} 297 | assert_equal(1, x.to_i, bug9155) 298 | end 299 | 300 | class Bug9403 301 | Name = '[ruby-core:59718] [Bug #9403]' 302 | SD = SimpleDelegator.new(new) 303 | class << SD 304 | def method_name 305 | __method__ 306 | end 307 | def callee_name 308 | __callee__ 309 | end 310 | alias aliased_name callee_name 311 | def dir_name 312 | __dir__ 313 | end 314 | end 315 | dc = DelegateClass(self) 316 | dc.class_eval do 317 | def method_name 318 | __method__ 319 | end 320 | def callee_name 321 | __callee__ 322 | end 323 | alias aliased_name callee_name 324 | def dir_name 325 | __dir__ 326 | end 327 | end 328 | DC = dc.new(new) 329 | end 330 | 331 | def test_method_in_simple_delegator 332 | assert_equal(:method_name, Bug9403::SD.method_name, Bug9403::Name) 333 | end 334 | 335 | def test_callee_in_simple_delegator 336 | assert_equal(:callee_name, Bug9403::SD.callee_name, Bug9403::Name) 337 | assert_equal(:aliased_name, Bug9403::SD.aliased_name, Bug9403::Name) 338 | end 339 | 340 | def test_dir_in_simple_delegator 341 | assert_equal(__dir__, Bug9403::SD.dir_name, Bug9403::Name) 342 | end 343 | 344 | def test_method_in_delegator_class 345 | assert_equal(:method_name, Bug9403::DC.method_name, Bug9403::Name) 346 | end 347 | 348 | def test_callee_in_delegator_class 349 | assert_equal(:callee_name, Bug9403::DC.callee_name, Bug9403::Name) 350 | assert_equal(:aliased_name, Bug9403::DC.aliased_name, Bug9403::Name) 351 | end 352 | 353 | def test_dir_in_delegator_class 354 | assert_equal(__dir__, Bug9403::DC.dir_name, Bug9403::Name) 355 | end 356 | 357 | def test_module_methods_vs_kernel_methods 358 | delegate = SimpleDelegator.new(Object.new) 359 | assert_raise(NoMethodError) do 360 | delegate.constants 361 | end 362 | end 363 | 364 | def test_basicobject 365 | o = BasicObject.new 366 | def o.bar; 1; end 367 | delegate = SimpleDelegator.new(o) 368 | assert_equal(1, delegate.bar) 369 | assert_raise(NoMethodError, /undefined method `foo' for/) { delegate.foo } 370 | end 371 | 372 | def test_basicobject_respond_to 373 | o = BasicObject.new 374 | def o.bar 375 | nil 376 | end 377 | 378 | def o.respond_to?(method, include_private=false) 379 | return false if method == :bar 380 | ::Kernel.instance_method(:respond_to?).bind_call(self, method, include_private) 381 | end 382 | delegate = SimpleDelegator.new(o) 383 | refute delegate.respond_to?(:bar) 384 | end 385 | 386 | def test_keyword_argument 387 | k = EnvUtil.labeled_class("Target") do 388 | def test(a, k:) [a, k]; end 389 | end 390 | a = DelegateClass(k).new(k.new) 391 | assert_equal([1, 0], a.test(1, k: 0)) 392 | end 393 | end 394 | --------------------------------------------------------------------------------