├── .document ├── bin ├── setup └── console ├── test ├── lib │ └── helper.rb └── test_delegate.rb ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ ├── sync-ruby.yml │ └── push_gem.yml ├── Gemfile ├── Rakefile ├── delegate.gemspec ├── BSDL ├── README.md ├── COPYING └── lib └── delegate.rb /.document: -------------------------------------------------------------------------------- 1 | BSDL 2 | COPYING 3 | README.md 4 | lib/ 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "delegate" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /Gemfile.lock 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@v6 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 | -------------------------------------------------------------------------------- /.github/workflows/sync-ruby.yml: -------------------------------------------------------------------------------- 1 | name: Sync ruby 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | sync: 7 | name: Sync ruby 8 | runs-on: ubuntu-latest 9 | if: ${{ github.repository_owner == 'ruby' }} 10 | steps: 11 | - uses: actions/checkout@v6 12 | 13 | - name: Create GitHub App token 14 | id: app-token 15 | uses: actions/create-github-app-token@v2 16 | with: 17 | app-id: 2060836 18 | private-key: ${{ secrets.RUBY_SYNC_DEFAULT_GEMS_PRIVATE_KEY }} 19 | owner: ruby 20 | repositories: ruby 21 | 22 | - name: Sync to ruby/ruby 23 | uses: convictional/trigger-workflow-and-wait@v1.6.5 24 | with: 25 | owner: ruby 26 | repo: ruby 27 | workflow_file_name: sync_default_gems.yml 28 | github_token: ${{ steps.app-token.outputs.token }} 29 | ref: master 30 | client_payload: | 31 | {"gem":"${{ github.event.repository.name }}","before":"${{ github.event.before }}","after":"${{ github.event.after }}"} 32 | propagate_failure: true 33 | wait_interval: 10 34 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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@df199fb7be9f65074067a9eb93f12bb4c5547cf2 # v2.13.3 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@d5126b9b3579e429dd52e51e68624dda2e05be25 # v1.267.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@1c162a739e8b4cb21a676e97b087e8268d8fc40b # v1.1.2 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.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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_equal((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_predicate(d.clone, :frozen?) 185 | assert_not_predicate(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 | -------------------------------------------------------------------------------- /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 | # The version string 43 | VERSION = "0.4.0" 44 | 45 | kernel = ::Kernel.dup 46 | kernel.class_eval do 47 | alias __raise__ raise 48 | [:to_s, :inspect, :!~, :===, :<=>, :hash].each do |m| 49 | undef_method m 50 | end 51 | private_instance_methods.each do |m| 52 | if /\Ablock_given\?\z|\Aiterator\?\z|\A__.*__\z/ =~ m 53 | next 54 | end 55 | undef_method m 56 | end 57 | end 58 | include kernel 59 | 60 | # :stopdoc: 61 | def self.const_missing(n) 62 | ::Object.const_get(n) 63 | end 64 | # :startdoc: 65 | 66 | ## 67 | # :method: raise 68 | # Use #__raise__ if your Delegator does not have a object to delegate the 69 | # #raise method call. 70 | # 71 | 72 | # 73 | # Pass in the _obj_ to delegate method calls to. All methods supported by 74 | # _obj_ will be delegated to. 75 | # 76 | def initialize(obj) 77 | __setobj__(obj) 78 | end 79 | 80 | # 81 | # Handles the magic of delegation through +__getobj__+. 82 | # 83 | ruby2_keywords def method_missing(m, *args, &block) 84 | r = true 85 | target = self.__getobj__ {r = false} 86 | 87 | if r && target_respond_to?(target, m, false) 88 | target.__send__(m, *args, &block) 89 | elsif ::Kernel.method_defined?(m) || ::Kernel.private_method_defined?(m) 90 | ::Kernel.instance_method(m).bind_call(self, *args, &block) 91 | else 92 | super(m, *args, &block) 93 | end 94 | end 95 | 96 | # 97 | # Checks for a method provided by this the delegate object by forwarding the 98 | # call through +__getobj__+. 99 | # 100 | def respond_to_missing?(m, include_private) 101 | r = true 102 | target = self.__getobj__ {r = false} 103 | r &&= target_respond_to?(target, m, include_private) 104 | if r && include_private && !target_respond_to?(target, m, false) 105 | warn "delegator does not forward private method \##{m}", uplevel: 3 106 | return false 107 | end 108 | r 109 | end 110 | 111 | KERNEL_RESPOND_TO = ::Kernel.instance_method(:respond_to?) # :nodoc: 112 | private_constant :KERNEL_RESPOND_TO 113 | 114 | # Handle BasicObject instances 115 | private def target_respond_to?(target, m, include_private) 116 | case target 117 | when Object 118 | target.respond_to?(m, include_private) 119 | else 120 | if KERNEL_RESPOND_TO.bind_call(target, :respond_to?) 121 | target.respond_to?(m, include_private) 122 | else 123 | KERNEL_RESPOND_TO.bind_call(target, m, include_private) 124 | end 125 | end 126 | end 127 | 128 | # 129 | # Returns the methods available to this delegate object as the union 130 | # of this object's and +__getobj__+ methods. 131 | # 132 | def methods(all=true) 133 | __getobj__.methods(all) | super 134 | end 135 | 136 | # 137 | # Returns the methods available to this delegate object as the union 138 | # of this object's and +__getobj__+ public methods. 139 | # 140 | def public_methods(all=true) 141 | __getobj__.public_methods(all) | super 142 | end 143 | 144 | # 145 | # Returns the methods available to this delegate object as the union 146 | # of this object's and +__getobj__+ protected methods. 147 | # 148 | def protected_methods(all=true) 149 | __getobj__.protected_methods(all) | super 150 | end 151 | 152 | # Note: no need to specialize private_methods, since they are not forwarded 153 | 154 | # 155 | # Returns true if two objects are considered of equal value. 156 | # 157 | def ==(obj) 158 | return true if obj.equal?(self) 159 | self.__getobj__ == obj 160 | end 161 | 162 | # 163 | # Returns true if two objects are not considered of equal value. 164 | # 165 | def !=(obj) 166 | return false if obj.equal?(self) 167 | __getobj__ != obj 168 | end 169 | 170 | # 171 | # Returns true if two objects are considered of equal value. 172 | # 173 | def eql?(obj) 174 | return true if obj.equal?(self) 175 | obj.eql?(__getobj__) 176 | end 177 | 178 | # 179 | # Delegates ! to the +__getobj__+ 180 | # 181 | def ! 182 | !__getobj__ 183 | end 184 | 185 | # 186 | # This method must be overridden by subclasses and should return the object 187 | # method calls are being delegated to. 188 | # 189 | def __getobj__ 190 | __raise__ ::NotImplementedError, "need to define '__getobj__'" 191 | end 192 | 193 | # 194 | # This method must be overridden by subclasses and change the object delegate 195 | # to _obj_. 196 | # 197 | def __setobj__(obj) 198 | __raise__ ::NotImplementedError, "need to define '__setobj__'" 199 | end 200 | 201 | # 202 | # Serialization support for the object returned by +__getobj__+. 203 | # 204 | def marshal_dump 205 | ivars = instance_variables.reject {|var| /\A@delegate_/ =~ var} 206 | [ 207 | :__v2__, 208 | ivars, ivars.map {|var| instance_variable_get(var)}, 209 | __getobj__ 210 | ] 211 | end 212 | 213 | # 214 | # Reinitializes delegation from a serialized object. 215 | # 216 | def marshal_load(data) 217 | version, vars, values, obj = data 218 | if version == :__v2__ 219 | vars.each_with_index {|var, i| instance_variable_set(var, values[i])} 220 | __setobj__(obj) 221 | else 222 | __setobj__(data) 223 | end 224 | end 225 | 226 | def initialize_clone(obj, freeze: nil) # :nodoc: 227 | self.__setobj__(obj.__getobj__.clone(freeze: freeze)) 228 | end 229 | def initialize_dup(obj) # :nodoc: 230 | self.__setobj__(obj.__getobj__.dup) 231 | end 232 | private :initialize_clone, :initialize_dup 233 | 234 | ## 235 | # :method: freeze 236 | # Freeze both the object returned by +__getobj__+ and self. 237 | # 238 | def freeze 239 | __getobj__.freeze 240 | super() 241 | end 242 | 243 | @delegator_api = self.public_instance_methods 244 | def self.public_api # :nodoc: 245 | @delegator_api 246 | end 247 | end 248 | 249 | ## 250 | # A concrete implementation of Delegator, this class provides the means to 251 | # delegate all supported method calls to the object passed into the constructor 252 | # and even to change the object being delegated to at a later time with 253 | # #__setobj__. 254 | # 255 | # class User 256 | # def born_on 257 | # Date.new(1989, 9, 10) 258 | # end 259 | # end 260 | # 261 | # require 'delegate' 262 | # 263 | # class UserDecorator < SimpleDelegator 264 | # def birth_year 265 | # born_on.year 266 | # end 267 | # end 268 | # 269 | # decorated_user = UserDecorator.new(User.new) 270 | # decorated_user.birth_year #=> 1989 271 | # decorated_user.__getobj__ #=> # 272 | # 273 | # A SimpleDelegator instance can take advantage of the fact that SimpleDelegator 274 | # is a subclass of +Delegator+ to call super to have methods called on 275 | # the object being delegated to. 276 | # 277 | # class SuperArray < SimpleDelegator 278 | # def [](*args) 279 | # super + 1 280 | # end 281 | # end 282 | # 283 | # SuperArray.new([1])[0] #=> 2 284 | # 285 | # Here's a simple example that takes advantage of the fact that 286 | # SimpleDelegator's delegation object can be changed at any time. 287 | # 288 | # class Stats 289 | # def initialize 290 | # @source = SimpleDelegator.new([]) 291 | # end 292 | # 293 | # def stats(records) 294 | # @source.__setobj__(records) 295 | # 296 | # "Elements: #{@source.size}\n" + 297 | # " Non-Nil: #{@source.compact.size}\n" + 298 | # " Unique: #{@source.uniq.size}\n" 299 | # end 300 | # end 301 | # 302 | # s = Stats.new 303 | # puts s.stats(%w{James Edward Gray II}) 304 | # puts 305 | # puts s.stats([1, 2, 3, nil, 4, 5, 1, 2]) 306 | # 307 | # Prints: 308 | # 309 | # Elements: 4 310 | # Non-Nil: 4 311 | # Unique: 4 312 | # 313 | # Elements: 8 314 | # Non-Nil: 7 315 | # Unique: 6 316 | # 317 | class SimpleDelegator < Delegator 318 | # Returns the current object method calls are being delegated to. 319 | def __getobj__ 320 | unless defined?(@delegate_sd_obj) 321 | return yield if block_given? 322 | __raise__ ::ArgumentError, "not delegated" 323 | end 324 | @delegate_sd_obj 325 | end 326 | 327 | # 328 | # Changes the delegate object to _obj_. 329 | # 330 | # It's important to note that this does *not* cause SimpleDelegator's methods 331 | # to change. Because of this, you probably only want to change delegation 332 | # to objects of the same type as the original delegate. 333 | # 334 | # Here's an example of changing the delegation object. 335 | # 336 | # names = SimpleDelegator.new(%w{James Edward Gray II}) 337 | # puts names[1] # => Edward 338 | # names.__setobj__(%w{Gavin Sinclair}) 339 | # puts names[1] # => Sinclair 340 | # 341 | def __setobj__(obj) 342 | __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj) 343 | @delegate_sd_obj = obj 344 | end 345 | end 346 | 347 | def Delegator.delegating_block(mid) # :nodoc: 348 | lambda do |*args, &block| 349 | target = self.__getobj__ 350 | target.__send__(mid, *args, &block) 351 | end.ruby2_keywords 352 | end 353 | 354 | # 355 | # The primary interface to this library. Use to setup delegation when defining 356 | # your class. 357 | # 358 | # class MyClass < DelegateClass(ClassToDelegateTo) # Step 1 359 | # def initialize 360 | # super(obj_of_ClassToDelegateTo) # Step 2 361 | # end 362 | # end 363 | # 364 | # or: 365 | # 366 | # MyClass = DelegateClass(ClassToDelegateTo) do # Step 1 367 | # def initialize 368 | # super(obj_of_ClassToDelegateTo) # Step 2 369 | # end 370 | # end 371 | # 372 | # Here's a sample of use from Tempfile which is really a File object with a 373 | # few special rules about storage location and when the File should be 374 | # deleted. That makes for an almost textbook perfect example of how to use 375 | # delegation. 376 | # 377 | # class Tempfile < DelegateClass(File) 378 | # # constant and class member data initialization... 379 | # 380 | # def initialize(basename, tmpdir=Dir::tmpdir) 381 | # # build up file path/name in var tmpname... 382 | # 383 | # @tmpfile = File.open(tmpname, File::RDWR|File::CREAT|File::EXCL, 0600) 384 | # 385 | # # ... 386 | # 387 | # super(@tmpfile) 388 | # 389 | # # below this point, all methods of File are supported... 390 | # end 391 | # 392 | # # ... 393 | # end 394 | # 395 | def DelegateClass(superclass, &block) 396 | klass = Class.new(Delegator) 397 | ignores = [*::Delegator.public_api, :to_s, :inspect, :=~, :!~, :===] 398 | protected_instance_methods = superclass.protected_instance_methods 399 | protected_instance_methods -= ignores 400 | public_instance_methods = superclass.public_instance_methods 401 | public_instance_methods -= ignores 402 | klass.module_eval do 403 | def __getobj__ # :nodoc: 404 | unless defined?(@delegate_dc_obj) 405 | return yield if block_given? 406 | __raise__ ::ArgumentError, "not delegated" 407 | end 408 | @delegate_dc_obj 409 | end 410 | def __setobj__(obj) # :nodoc: 411 | __raise__ ::ArgumentError, "cannot delegate to self" if self.equal?(obj) 412 | @delegate_dc_obj = obj 413 | end 414 | protected_instance_methods.each do |method| 415 | define_method(method, Delegator.delegating_block(method)) 416 | protected method 417 | end 418 | public_instance_methods.each do |method| 419 | define_method(method, Delegator.delegating_block(method)) 420 | end 421 | end 422 | klass.define_singleton_method :public_instance_methods do |all=true| 423 | super(all) | superclass.public_instance_methods 424 | end 425 | klass.define_singleton_method :protected_instance_methods do |all=true| 426 | super(all) | superclass.protected_instance_methods 427 | end 428 | klass.define_singleton_method :instance_methods do |all=true| 429 | super(all) | superclass.instance_methods 430 | end 431 | klass.define_singleton_method :public_instance_method do |name| 432 | super(name) 433 | rescue NameError 434 | raise unless self.public_instance_methods.include?(name) 435 | superclass.public_instance_method(name) 436 | end 437 | klass.define_singleton_method :instance_method do |name| 438 | super(name) 439 | rescue NameError 440 | raise unless self.instance_methods.include?(name) 441 | superclass.instance_method(name) 442 | end 443 | klass.module_eval(&block) if block 444 | return klass 445 | end 446 | --------------------------------------------------------------------------------