├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .rspec ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── bin ├── rake └── rspec ├── dumb_delegator.gemspec ├── lib ├── dumb_delegator.rb └── dumb_delegator │ ├── triple_equal_ext.rb │ └── version.rb └── spec ├── dumb_delegator_spec.rb └── spec_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | continue-on-error: ${{matrix.experimental}} 10 | env: 11 | CC_TEST_REPORTER_ID: ${{secrets.CC_TEST_REPORTER_ID}} 12 | JRUBY_OPTS: '-X+O' 13 | 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | ruby: 18 | - 2.4 19 | - 2.5 20 | - 2.6 21 | - 2.7 22 | - 3.0 23 | - 3.1 24 | - 3.2 25 | - 3.3 26 | - jruby 27 | - truffleruby 28 | experimental: [false] 29 | include: 30 | - ruby: jruby-head 31 | experimental: true 32 | - ruby: head 33 | experimental: true 34 | 35 | steps: 36 | - uses: actions/checkout@v4 37 | 38 | - name: Setup Ruby 39 | uses: ruby/setup-ruby@v1 40 | with: 41 | ruby-version: ${{ matrix.ruby }} 42 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 43 | 44 | - name: Build and test with RSpec 45 | run: bin/rspec 46 | 47 | - name: Publish code coverage 48 | uses: paambaati/codeclimate-action@v2.7.5 # Locking to specific version b/c: https://github.com/paambaati/codeclimate-action/issues/142 49 | if: matrix.ruby == '3.3' # Ruby 2.4 breaks the CC uploader. Also, we only need to upload one report. 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore all logfiles and tempfiles 2 | /tmp/ 3 | /tags 4 | 5 | # Environment normalisation 6 | /.env 7 | /.rspec-local 8 | /.yardoc 9 | /lib/bundler/man/ 10 | 11 | # Gem packaging stuff 12 | /*.gem 13 | /pkg/ 14 | 15 | # Dependencies and such 16 | /.bundle 17 | /.ruby-version 18 | /Gemfile.lock 19 | 20 | # Spec and coverage housekeeping 21 | /coverage/ 22 | /spec/rspec-status.txt 23 | /spec/reports 24 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require spec_helper 2 | 3 | --color 4 | --format progress 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [Unreleased] 7 | 8 | ## [1.1.0] 2024-12-17 9 | ### Changed 10 | - Use `__getobj__` to be compatible with the `Delegator` "interface." [@cervantn [14](https://github.com/stevenharman/dumb_delegator/pull/14)] 11 | 12 | ## [1.0.0] 2020-01-27 13 | ### Changed 14 | - Require Ruby >= 2.4. We may still work with older Rubies, but no promises. 15 | - Basic introspection support for a DumbDelegator instance: `#inspect`, `#method`, and `#methods`. [[13](https://github.com/stevenharman/dumb_delegator/pull/13)] 16 | 17 | ### Added 18 | - Optional support for using a DumbDelegator instance in a `case` statement. [[12](https://github.com/stevenharman/dumb_delegator/pull/12)] 19 | 20 | ## [0.8.1] 2020-01-25 21 | ### Changed 22 | - Explicitly Require Ruby >= 1.9.3 23 | 24 | ### Added 25 | - This CHANGELOG file. 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # Specify your gem's dependencies in dumb_delegator.gemspec 6 | gemspec 7 | 8 | gem "rake", "~> 13.0" 9 | gem "rspec", "~> 3.9" 10 | gem "simplecov", group: :test, require: nil 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Andy Lindeman 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DumbDelegator 2 | 3 | [![Gem Version](https://badge.fury.io/rb/dumb_delegator.svg?icon=si%3Arubygems&icon_color=%23ff2600)](https://badge.fury.io/rb/dumb_delegator) 4 | [![CI](https://github.com/stevenharman/dumb_delegator/actions/workflows/ci.yml/badge.svg)](https://github.com/stevenharman/dumb_delegator/actions/workflows/ci.yml) 5 | [![Maintainability](https://api.codeclimate.com/v1/badges/b684cbe08af745cbe957/maintainability)](https://codeclimate.com/github/stevenharman/dumb_delegator/maintainability) 6 | [![Test Coverage](https://api.codeclimate.com/v1/badges/b684cbe08af745cbe957/test_coverage)](https://codeclimate.com/github/stevenharman/dumb_delegator/test_coverage) 7 | 8 | Ruby provides the `delegate` standard library. 9 | However, we found that it is not appropriate for cases that require nearly every call to be proxied. 10 | 11 | For instance, Rails uses `#class` and `#instance_of?` to introspect on Model classes when generating forms and URL helpers. 12 | These methods are not forwarded when using `Delegator` or `SimpleDelegator`. 13 | 14 | ```ruby 15 | require "delegate" 16 | 17 | class MyAwesomeClass 18 | # ... 19 | end 20 | 21 | o = MyAwesomeClass.new 22 | d = SimpleDelegator.new(o) 23 | 24 | d.class #=> SimpleDelegator 25 | d.is_a? MyAwesomeClass #=> false 26 | ``` 27 | 28 | `DumbDelegator`, on the other hand, forwards almost ALL THE THINGS: 29 | 30 | ```ruby 31 | require "dumb_delegator" 32 | 33 | class MyAwesomeClass 34 | # ... 35 | end 36 | 37 | o = MyAwesomeClass.new 38 | d = DumbDelegator.new(o) 39 | 40 | d.class #=> MyAwesomeClass 41 | d.is_a? MyAwesomeClass #=> true 42 | ``` 43 | 44 | ## Installation 45 | 46 | Add this line to your Gemfile: 47 | 48 | ```ruby 49 | gem "dumb_delegator" 50 | ``` 51 | 52 | And then install: 53 | 54 | ```bash 55 | $ bundle 56 | ``` 57 | 58 | Or install it yourself: 59 | 60 | ```bash 61 | $ gem install dumb_delegator 62 | ``` 63 | 64 | ### Versioning 65 | 66 | This project adheres to [Semantic Versioning][semver]. 67 | 68 | #### Version `0.8.x` 69 | 70 | The `0.8.0` release was downloaded 1.2MM times before the `1.0.0` work began. 71 | Which is great! 🎉 72 | But, we wanted to clean up some cruft, fix a few small things, and improve ergonomics. 73 | And we wanted to do all of that while, hopefully, not breaking existing usage. 74 | 75 | To that end, `1.0.0` dropped support for all [EoL'd Rubies][ruby-releases] and only officially supported Ruby `2.4` - `2.7` when it was released. 76 | However, most older Rubies, _should_ still work. 77 | Maybe… Shmaybe? 78 | Except for Ruby 1.9, which probably _does not work_ with `DumbDelegator` `> 1.0.0`. 79 | If you're on an EoL'd Ruby, please try the `0.8.x` versions of this gem. 80 | 81 | ## Usage 82 | 83 | `DumbDelegator`'s API and usage patters were inspired by Ruby stdlib's `SimpleDelegator`. 84 | So the usage and ergonomics are quite similar. 85 | 86 | ```ruby 87 | require "dumb_delegator" 88 | 89 | class Coffee 90 | def cost 91 | 2 92 | end 93 | 94 | def origin 95 | "Colombia" 96 | end 97 | end 98 | 99 | class Milk < DumbDelegator 100 | def cost 101 | super + 0.4 102 | end 103 | end 104 | 105 | class Sugar < DumbDelegator 106 | def cost 107 | super + 0.2 108 | end 109 | end 110 | 111 | coffee = Coffee.new 112 | 113 | cup_o_coffee = Sugar.new(Milk.new(coffee)) 114 | cup_o_coffee.origin #=> Colombia 115 | cup_o_coffee.cost #=> 2.6 116 | 117 | # Introspection 118 | cup_o_coffee.class #=> Coffee 119 | cup_o_coffee.__getobj__ #=> # 120 | cup_o_coffee.inspect #=> "#>>" 121 | cup_o_coffee.is_a?(Coffee) #=> true 122 | cup_o_coffee.is_a?(Milk) #=> true 123 | cup_o_coffee.is_a?(Sugar) #=> true 124 | ``` 125 | 126 | ### Rails Model Decorator 127 | 128 | There are [many decorator implementations](http://robots.thoughtbot.com/post/14825364877/evaluating-alternative-decorator-implementations-in) in Ruby. 129 | One of the simplest is "`SimpleDelegator` + `super` + `__getobj__`," but it has the drawback of confusing Rails. 130 | It is necessary to redefine `#class`, at a minimum. 131 | If you're relying on Rails' URL Helpers with a delegated object, you also need to redefine `#instance_of?`. 132 | We've also observed the need to redefine other Rails-y methods to get various bits of 🧙 Rails Magic 🧙 to work as expected. 133 | 134 | With `DumbDelegator`, there's not a need for redefining these things because nearly every possible method is delegated. 135 | 136 | ### Optional `case` statement support 137 | 138 | Instances of `DumbDelegator` will delegate `#===` out of the box. 139 | Meaning an instance can be used in a `case` statement so long as the `when` clauses rely on instance comparison. 140 | For example, when using a `case` with a regular expression, range, etc... 141 | 142 | It's also common to use Class/Module in the `where` clauses. 143 | In such usage, it's the Class/Module's `::===` method that gets called, rather than the `#===` method on the `DumbDelegator` instance. 144 | That means we need to override each Class/Module's `::===` method, or even monkey-patch `::Module::===`. 145 | 146 | `DumbDelegator` ships with an optional extension to override a Class/Module's `::===` method. 147 | But you need to extend each Class/Module you use in a `where` clause. 148 | 149 | ```ruby 150 | def try_a_case(thing) 151 | case thing 152 | when MyAwesomeClass 153 | "thing is a MyAwesomeClass." 154 | when DumbDelegator 155 | "thing is a DumbDelegator." 156 | else 157 | "Bad. This is bad." 158 | end 159 | end 160 | 161 | target = MyAwesomeClass.new 162 | dummy = DumbDelegator.new(target) 163 | 164 | try_a_case(dummy) #=> thing is a DumbDelegator. 165 | 166 | MyAwesomeClass.extend(DumbDelegator::TripleEqualExt) 167 | 168 | try_a_case(dummy) #=> thing is a MyAwesomeClass. 169 | ``` 170 | 171 | #### Overriding `Module::===` 172 | If necessary, you could also override the base `Module::===`, though that's pretty invasive. 173 | 174 | 🐲 _There be dragons!_ 🐉 175 | 176 | ```ruby 177 | ::Module.extend(DumbDelegator::TripleEqualExt) 178 | ``` 179 | 180 | ## Contributing 181 | 182 | 1. Fork it 183 | 2. Create your feature branch (`git checkout -b my-new-feature`) 184 | 3. Commit your changes (`git commit -am 'Added some feature'`) 185 | 4. Push to the branch (`git push origin my-new-feature`) 186 | 5. Create new Pull Request 187 | 188 | ## Contribution Ideas/Needs 189 | 190 | 1. Ruby 1.8 support (use the `blankslate` gem?) 191 | 192 | 193 | [ruby-releases]: https://www.ruby-lang.org/en/downloads/branches/ "The current maintenance status of the various Ruby branches" 194 | [semver]: https://semver.org/spec/v2.0.0.html "Semantic Versioning 2.0.0" 195 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | require "rspec/core/rake_task" 5 | RSpec::Core::RakeTask.new(:spec) do |t| 6 | t.rspec_opts = "--tag ~objectspace" if RUBY_PLATFORM == "java" 7 | end 8 | 9 | task default: :spec 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rake' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rake", "rake") 28 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # This file was generated by Bundler. 6 | # 7 | # The application 'rspec' is installed as part of a gem, and 8 | # this file is here to facilitate running it. 9 | # 10 | 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) 12 | 13 | bundle_binstub = File.expand_path("bundle", __dir__) 14 | 15 | if File.file?(bundle_binstub) 16 | if File.read(bundle_binstub, 300).include?("This file was generated by Bundler") 17 | load(bundle_binstub) 18 | else 19 | abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run. 20 | Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.") 21 | end 22 | end 23 | 24 | require "rubygems" 25 | require "bundler/setup" 26 | 27 | load Gem.bin_path("rspec-core", "rspec") 28 | -------------------------------------------------------------------------------- /dumb_delegator.gemspec: -------------------------------------------------------------------------------- 1 | require File.expand_path("../lib/dumb_delegator/version", __FILE__) 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "dumb_delegator" 5 | spec.version = DumbDelegator::VERSION 6 | spec.required_ruby_version = ">= 2.4.0" 7 | spec.authors = ["Andy Lindeman", "Steven Harman"] 8 | spec.email = ["alindeman@gmail.com", "steven@harmanly.com"] 9 | spec.licenses = ["MIT"] 10 | spec.summary = "Delegator class that delegates ALL the things" 11 | spec.description = <<~EOD 12 | Delegator and SimpleDelegator in Ruby's stdlib are useful, but they pull in most of Kernel. 13 | This is not appropriate for many uses; for instance, delegation to Rails Models. 14 | DumbDelegator, on the other hand, delegates nearly everything to the wrapped object. 15 | EOD 16 | spec.homepage = "https://github.com/stevenharman/dumb_delegator" 17 | 18 | spec.metadata = { 19 | "changelog_uri" => "https://github.com/stevenharman/dumb_delegator/blob/master/CHANGELOG.md", 20 | "documentation_uri" => "https://rubydoc.info/gems/dumb_delegator", 21 | "homepage_uri" => spec.homepage, 22 | "source_code_uri" => "https://github.com/stevenharman/dumb_delegator" 23 | } 24 | 25 | # Specify which files should be added to the gem when it is released. 26 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 27 | spec.files = Dir.chdir(__dir__) do 28 | `git ls-files -z`.split("\x0").reject do |f| 29 | (File.expand_path(f) == __FILE__) || 30 | f.start_with?(*%w[bin/ spec/ .git .github Gemfile]) 31 | end 32 | end 33 | spec.bindir = "exe" 34 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 35 | spec.require_paths = ["lib"] 36 | end 37 | -------------------------------------------------------------------------------- /lib/dumb_delegator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "dumb_delegator/triple_equal_ext" 4 | require "dumb_delegator/version" 5 | 6 | ## 7 | # @example 8 | # class Coffee 9 | # def cost 10 | # 2 11 | # end 12 | # 13 | # def origin 14 | # "Colombia" 15 | # end 16 | # end 17 | # 18 | # class Milk < DumbDelegator 19 | # def cost 20 | # super + 0.4 21 | # end 22 | # end 23 | # 24 | # class Sugar < DumbDelegator 25 | # def cost 26 | # super + 0.2 27 | # end 28 | # end 29 | # 30 | # coffee = Coffee.new 31 | # Milk.new(coffee).origin #=> Colombia 32 | # Sugar.new(Sugar.new(coffee)).cost #=> 2.4 33 | # 34 | # cup_o_coffee = Sugar.new(Milk.new(coffee)) 35 | # cup_o_coffee.cost #=> 2.6 36 | # cup_o_coffee.class #=> Coffee 37 | # cup_o_coffee.is_a?(Coffee) #=> true 38 | # cup_o_coffee.is_a?(Milk) #=> true 39 | # cup_o_coffee.is_a?(Sugar) #=> true 40 | class DumbDelegator < ::BasicObject 41 | (::BasicObject.instance_methods - [:equal?, :__id__, :__send__, :method_missing]).each do |method| 42 | undef_method(method) 43 | end 44 | 45 | kernel = ::Kernel.dup 46 | (kernel.instance_methods - [:dup, :clone, :method, :methods, :respond_to?, :object_id]).each do |method| 47 | kernel.__send__(:undef_method, method) 48 | end 49 | include kernel 50 | 51 | def initialize(target) 52 | __setobj__(target) 53 | end 54 | 55 | def inspect 56 | "#<#{(class << self; self; end).superclass}:#{object_id} obj: #{__getobj__.inspect}>" 57 | end 58 | 59 | def methods(all = true) 60 | __getobj__.methods(all) | super 61 | end 62 | 63 | def method_missing(method, *args, &block) 64 | if __getobj__.respond_to?(method) 65 | __getobj__.__send__(method, *args, &block) 66 | else 67 | super 68 | end 69 | end 70 | 71 | def respond_to_missing?(method, include_private = false) 72 | __getobj__.respond_to?(method, include_private) || super 73 | end 74 | 75 | # @return [Object] The object calls are being delegated to 76 | def __getobj__ 77 | @__dumb_target__ 78 | end 79 | 80 | # @param obj [Object] Change the object delegate to +obj+. 81 | def __setobj__(obj) 82 | raise ::ArgumentError, "Delegation to self is not allowed." if obj.__id__ == __id__ 83 | @__dumb_target__ = obj 84 | end 85 | 86 | def marshal_dump 87 | [ 88 | :__v1__, 89 | __getobj__ 90 | ] 91 | end 92 | 93 | def marshal_load(data) 94 | version, obj = data 95 | case version 96 | when :__v1__ 97 | __setobj__(obj) 98 | end 99 | end 100 | 101 | private 102 | 103 | def initialize_dup(obj) 104 | __setobj__(obj.__getobj__.dup) 105 | end 106 | 107 | def initialize_clone(obj) 108 | __setobj__(obj.__getobj__.clone) 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/dumb_delegator/triple_equal_ext.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DumbDelegator < ::BasicObject 4 | ## 5 | # This optional extension enables a Class/Module to support +case+ statements. 6 | # 7 | # Specifically, it monkey-patches a Class/Module's +:===+ method to check if the +other+ argument is an instance of the extended Class/Module. 8 | # 9 | # @example Extending a Class/Module to handle class equality for a DumbDelegator instance. 10 | # 11 | # target = MyAwesomeClass.new 12 | # dummy = DumbDelegator.new(target) 13 | # 14 | # MyAwesomeClass === dummy #=> false 15 | # DumbDelegator === dummy #=> true 16 | # 17 | # MyAwesomeClass.extend(DumbDelegator::TripleEqualExt) 18 | # 19 | # MyAwesomeClass === dummy #=> true 20 | # DumbDelegator === dummy #=> true 21 | module TripleEqualExt 22 | # Case equality for the extended Class/Module and then given +other+. 23 | # 24 | # @param other [Object] An instance of any Object 25 | # 26 | # @return [Boolean] If the +other+ is an instance of the Class/Module. 27 | def ===(other) 28 | super || other.is_a?(self) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/dumb_delegator/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class DumbDelegator < ::BasicObject 4 | VERSION = "1.1.0" 5 | end 6 | -------------------------------------------------------------------------------- /spec/dumb_delegator_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe DumbDelegator do 2 | subject(:dummy) { Wrapper.new(target) } 3 | let(:target) { Target.new } 4 | let(:lazy) { LazyDelegator.new { target } } 5 | 6 | class Wrapper < DumbDelegator # rubocop:disable Lint/ConstantDefinitionInBlock 7 | def wrapper_method 8 | "Method only on wrapper." 9 | end 10 | 11 | def common_method 12 | ["Method on wrapper.", super].join(" ") 13 | end 14 | end 15 | 16 | class LazyDelegator < DumbDelegator 17 | def initialize(&block) 18 | @block = block 19 | end 20 | 21 | def __getobj__ 22 | @__dumb_target__ ||= @block.call 23 | end 24 | end 25 | 26 | class Target # rubocop:disable Lint/ConstantDefinitionInBlock 27 | def common_method 28 | "Method on target." 29 | end 30 | 31 | def target_method 32 | "Method only on target." 33 | end 34 | 35 | def query(*args) 36 | "queried with #{args}" 37 | end 38 | 39 | def with_block(&block) 40 | block.call 41 | end 42 | end 43 | 44 | it "delegates to the target object" do 45 | expect(dummy.target_method).to eq("Method only on target.") 46 | end 47 | 48 | it "delegates to the target object when subclasses override __getobj__" do 49 | expect(lazy.target_method).to eq("Method only on target.") 50 | end 51 | 52 | it "delegates to the target object with arguments" do 53 | result = dummy.query("some_key", 42) 54 | 55 | expect(result).to eq(%(queried with ["some_key", 42])) 56 | end 57 | 58 | it "delegates to the target object with a block" do 59 | result = dummy.with_block { "block called!" } 60 | 61 | expect(result).to eq("block called!") 62 | end 63 | 64 | it "errors if the method is not defined on the wrapper nor the target" do 65 | expect { 66 | dummy.no_such_method 67 | }.to raise_error(NoMethodError) 68 | end 69 | 70 | it "responds to methods defined by child classes" do 71 | expect(dummy.wrapper_method).to eq("Method only on wrapper.") 72 | end 73 | 74 | it "responds to methods defined by child classes, and can super up to target" do 75 | expect(dummy.common_method).to eq("Method on wrapper. Method on target.") 76 | end 77 | 78 | it "delegates methods defined on Object" do 79 | expect(dummy.class).to eq(Target) 80 | end 81 | 82 | it "delegates methods defined on Kernel" do 83 | expect(target).to receive(:nil?) 84 | dummy.nil? 85 | end 86 | 87 | it "delegates bang (!) operator" do 88 | allow(target).to receive(:!) { "bang!" } 89 | expect(!dummy).to eq("bang!") 90 | end 91 | 92 | it "delegates object inequivalence" do 93 | allow(target).to receive(:!=).and_call_original 94 | 95 | expect(dummy != target).to be false 96 | end 97 | 98 | it "delegates object equivalence" do 99 | aggregate_failures do 100 | expect(dummy).to eql(target) 101 | expect(dummy == target).to be true 102 | end 103 | end 104 | 105 | it "delegates #===" do 106 | expect(dummy === target).to be true 107 | end 108 | 109 | it "delegates class checks" do 110 | aggregate_failures do 111 | expect(dummy.is_a?(Target)).to be(true) 112 | expect(dummy.kind_of?(Target)).to be(true) # rubocop:disable Style/ClassCheck 113 | expect(dummy.instance_of?(Target)).to be(true) 114 | end 115 | end 116 | 117 | it "does not delegate ::=== to the target's class" do 118 | aggregate_failures do 119 | expect(Target === dummy).to be false 120 | expect(DumbDelegator === dummy).to be true 121 | end 122 | end 123 | 124 | context "with a Module/Class's ::=== overridden via extension" do 125 | let(:target) { TargetWithTripleEqualExt.new } 126 | 127 | class TargetWithTripleEqualExt # rubocop:disable Lint/ConstantDefinitionInBlock 128 | extend DumbDelegator::TripleEqualExt 129 | end 130 | 131 | it "delegates ::=== to the target's class" do 132 | aggregate_failures do 133 | expect(TargetWithTripleEqualExt === dummy).to be true 134 | expect(DumbDelegator === dummy).to be true 135 | end 136 | end 137 | end 138 | 139 | it "delegates instance_eval" do 140 | expect(target).to receive(:instance_eval) 141 | dummy.instance_eval { true } 142 | end 143 | 144 | it "delegates instance_exec" do 145 | expect(target).to receive(:instance_exec) 146 | dummy.instance_exec { true } 147 | end 148 | 149 | describe "#dup" do 150 | it "returns a shallow of itself, the delegator (not the underlying object)", objectspace: true do 151 | dupped = dummy.dup 152 | 153 | expect(ObjectSpace.each_object(DumbDelegator).map(&:__id__)).to include dupped.__id__ 154 | end 155 | end 156 | 157 | describe "#clone" do 158 | it "returns a shallow of itself, the delegator (not the underlying object)", objectspace: true do 159 | cloned = dummy.clone 160 | 161 | expect(ObjectSpace.each_object(DumbDelegator).map(&:__id__)).to include cloned.__id__ 162 | end 163 | end 164 | 165 | describe "marshaling" do 166 | let(:target) { Object.new } 167 | 168 | it "marshals and unmarshals itself, the delegator (not the underlying object)", objectspace: true do 169 | marshaled = Marshal.dump(dummy) 170 | unmarshaled = Marshal.load(marshaled) 171 | 172 | expect(ObjectSpace.each_object(DumbDelegator).map(&:__id__)).to include unmarshaled.__id__ 173 | end 174 | end 175 | 176 | describe "#respond_to?" do 177 | [:equal?, :__id__, :__send__, :dup, :clone, :__getobj__, :__setobj__, :marshal_dump, :marshal_load, :respond_to?].each do |method| 178 | it "responds to #{method}" do 179 | expect(dummy.respond_to?(method)).to be true 180 | end 181 | end 182 | 183 | context "subclasses of DumbDelegator" do 184 | it "respond to methods defined on the subclass" do 185 | expect(dummy).to respond_to(:target_method) 186 | end 187 | end 188 | end 189 | 190 | describe "#__getobj__" do 191 | it "returns the target object" do 192 | expect(dummy.__getobj__).to equal(target) 193 | end 194 | end 195 | 196 | describe "#__setobj__" do 197 | it "resets the target object to a different object" do 198 | new_target = Target.new.tap do |nt| 199 | def nt.a_new_thing 200 | true 201 | end 202 | end 203 | 204 | dummy.__setobj__(new_target) 205 | expect(dummy.a_new_thing).to be true 206 | end 207 | 208 | it "cannot delegate to itself" do 209 | expect { 210 | dummy.__setobj__(dummy) 211 | dummy.common_method 212 | }.to raise_error(ArgumentError, "Delegation to self is not allowed.") 213 | end 214 | end 215 | 216 | describe "introspection capabilities" do 217 | it "provides a human-friendly representation of the delegator and wrapped object" do 218 | expect(dummy.inspect).to match(/#/) 219 | end 220 | 221 | it "reports methods defined on the target" do 222 | expect(dummy.methods).to include(:target_method, :common_method) 223 | end 224 | 225 | it "reports methods defined on the wrapper" do 226 | expect(dummy.methods).to include(:wrapper_method, :common_method) 227 | end 228 | 229 | it "looks up a named method on the target" do 230 | method = dummy.method(:target_method) 231 | aggregate_failures do 232 | expect(method).not_to be_nil 233 | expect(method.receiver).to eq(target) 234 | end 235 | end 236 | 237 | it "looks up a named method on the wrapper" do 238 | method = dummy.method(:wrapper_method) 239 | aggregate_failures do 240 | expect(method).not_to be_nil 241 | expect(method.receiver).to equal(dummy) 242 | end 243 | end 244 | end 245 | end 246 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | if ENV["CC_TEST_REPORTER_ID"] || ENV["COVERAGE"] 2 | require "simplecov" 3 | SimpleCov.start 4 | end 5 | 6 | require File.expand_path("../lib/dumb_delegator", File.dirname(__FILE__)) 7 | 8 | RSpec.configure do |config| 9 | config.expect_with :rspec do |expectations| 10 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 11 | end 12 | 13 | config.mock_with :rspec do |mocks| 14 | mocks.verify_partial_doubles = true 15 | end 16 | 17 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 18 | # have no way to turn it off -- the option exists only for backwards 19 | # compatibility in RSpec 3). It causes shared context metadata to be 20 | # inherited by the metadata hash of host groups and examples, rather than 21 | # triggering implicit auto-inclusion in groups with matching metadata. 22 | config.shared_context_metadata_behavior = :apply_to_host_groups 23 | 24 | config.filter_run_when_matching :focus 25 | config.example_status_persistence_file_path = "spec/rspec-status.txt" 26 | config.disable_monkey_patching! 27 | config.warnings = true 28 | 29 | if config.files_to_run.one? 30 | # Use the documentation formatter for detailed output, 31 | # unless a formatter has already been configured 32 | # (e.g. via a command-line flag). 33 | config.default_formatter = "doc" 34 | end 35 | config.order = :random 36 | Kernel.srand config.seed 37 | end 38 | --------------------------------------------------------------------------------