├── .codeclimate.yml ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .rubocop.yml ├── .simplecov ├── .standard.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── casting.gemspec ├── examples ├── activation.rb ├── casting_forwardable.rb ├── dci.rb ├── delegation_dci.rb ├── json-generator.rb └── roles_over_objects.rb ├── lib ├── casting.rb └── casting │ ├── client.rb │ ├── context.rb │ ├── delegation.rb │ ├── enum.rb │ ├── method_consolidator.rb │ ├── missing_method_client.rb │ ├── missing_method_client_class.rb │ ├── null.rb │ ├── super_delegate.rb │ └── version.rb └── test ├── casting_enum_test.rb ├── casting_test.rb ├── class_refinement_test.rb ├── client_test.rb ├── context_test.rb ├── delegation_test.rb ├── frozen_client_test.rb ├── method_consolidator_test.rb ├── missing_method_client_test.rb ├── module_cleanup_test.rb ├── null_module_test.rb ├── super_test.rb └── test_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | engines: 3 | duplication: 4 | enabled: true 5 | config: 6 | languages: 7 | - ruby 8 | reek: 9 | enabled: true 10 | checks: 11 | Attribute/Attribute: 12 | enabled: false 13 | BooleanParameter: 14 | enabled: false 15 | ControlCouple/BooleanParameter: 16 | enabled: false 17 | ControlCouple/ControlParameter: 18 | enabled: false 19 | 20 | rubocop: 21 | enabled: true 22 | ratings: 23 | paths: 24 | - "**.rb" 25 | - "lib/**/*.rb" 26 | exclude_paths: 27 | - examples/* 28 | - test/* 29 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: ['**'] 8 | 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | ruby: [ '2.7', '3.0', '3.1', '3.2' ] 16 | name: Ruby ${{ matrix.ruby }} 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up Ruby 20 | uses: ruby/setup-ruby@v1 21 | with: 22 | ruby-version: ${{ matrix.ruby }} 23 | bundler-cache: true 24 | - name: Install dependencies 25 | run: bundle install --jobs 4 --retry 3 26 | - uses: amancevice/setup-code-climate@v1 27 | with: 28 | cc_test_reporter_id: ${{ secrets.CC_TEST_REPORTER_ID }} 29 | - run: cc-test-reporter before-build 30 | - name: Run tests 31 | run: bundle exec rake 32 | - run: cc-test-reporter after-build 33 | if: ${{ github.event_name != 'pull_request' }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | coverage.data 19 | coverage -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: standard 2 | 3 | inherit_gem: 4 | standard: config/base.yml 5 | -------------------------------------------------------------------------------- /.simplecov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saturnflyer/casting/d9c896a6091c255d8960d83e83d42bc997ae2fa8/.simplecov -------------------------------------------------------------------------------- /.standard.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saturnflyer/casting/d9c896a6091c255d8960d83e83d42bc997ae2fa8/.standard.yml -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 3 | ## 1.0.2 4 | 5 | - Allow use of Casting::Enum.enum 6 | - Remove unused local variable in enum. 7 | - Added CODE_OF_CONDUCT.md 8 | 9 | ## 1.0.1 10 | 11 | - Fix to properly include Enum files 12 | - 1.0.0 actually dropped 2.6 and below. 13 | - Fix changelog which had 0.7.3 notes that are actually 1.0 notes 14 | 15 | ## 1.0.0 16 | 17 | - Drop Ruby 2.5 and below 18 | - Add Casting::Enum to return enumerators which apply a set of behaviors 19 | - Remove Casting::PreparedDelegation class and move all features into Casting::Delegation 20 | 21 | ## 0.7.2 2016-07-28 22 | 23 | - Return defined __delegates__ or empty array, allowing frozen client objects. 24 | Previous implementation raised an error when accessing uninitialized collection 25 | of __delegates__ 26 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | jim@saturnflyer.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem "codeclimate-test-reporter", require: nil 6 | gem "minitest" 7 | gem "rake" 8 | gem "simplecov" 9 | gem "standard" 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2022 Jim Gay 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 | # Casting 2 | 3 | [](https://codeclimate.com/github/saturnflyer/casting) 4 | [](https://codeclimate.com/github/saturnflyer/casting/coverage) 5 | [](http://badge.fury.io/rb/casting) 6 | 7 | ## Add behavior to your objects without using extend 8 | Do it for the life of the object or only for the life of a block of code. 9 | 10 | Casting gives you real delegation that flattens your object structure compared to libraries 11 | like Delegate or Forwardable. With casting, you can implement your own decorators that 12 | will be so much simpler than using wrappers. 13 | 14 | Here's a quick example that you might try in a Rails project: 15 | 16 | ```ruby 17 | # implement a module that contains information for the request response 18 | # and apply it to an object in your system. 19 | def show 20 | @user = user.cast_as(UserRepresenter) 21 | end 22 | ``` 23 | 24 | To use proper delegation, your approach should preserve `self` as a reference 25 | to the original object receiving a method. When the object receiving the forwarded 26 | message has its own and separate notion of `self`, you're working with a wrapper (also called 27 | consultation) and not using delegation. 28 | 29 | The Ruby standard library includes a library called "delegate", but it is 30 | a consultation approach. With that "delegate", all messages are forwarded to 31 | another object, but the attendant object maintains its own identity. 32 | 33 | With Casting, your defined methods may reference `self` and during 34 | execution it will refer to the original client object. 35 | 36 | Casting was created while exploring ideas for [cleaning up ruby programs](http://clean-ruby.com). 37 | 38 | ## Usage 39 | 40 | To use Casting, you must first extend an object as the delegation client: 41 | 42 | ```ruby 43 | actor = Object.new 44 | actor.extend(Casting::Client) 45 | ``` 46 | 47 | Or you may include the module in a particular class: 48 | 49 | ```ruby 50 | class Actor 51 | include Casting::Client 52 | end 53 | actor = Actor.new 54 | ``` 55 | 56 | Your objects will have a few additional methods: `delegation`, `cast`, and if you do not *already* have it defined (from another library, for example): `delegate`. The `delegate` method is aliased to `cast`. 57 | 58 | Then you may delegate a method to an attendant object: 59 | 60 | ```ruby 61 | actor.delegate(:hello_world, other_actor) 62 | ``` 63 | 64 | Or you may create an object to manage the delegation of methods to an attendant object: 65 | 66 | ```ruby 67 | actor.delegation(:hello_world).to(other_actor).call 68 | ``` 69 | 70 | You may also delegate methods without an explicit attendant instance, but provide 71 | a module containing the behavior you need to use: 72 | 73 | ```ruby 74 | module GreetingModule 75 | def hello_world 76 | "hello world" 77 | end 78 | end 79 | 80 | actor.delegate(:hello_world, GreetingModule) 81 | # or 82 | actor.delegation(:hello_world).to(GreetingModule).call 83 | ``` 84 | 85 | Pass arguments to your delegated method: 86 | 87 | ```ruby 88 | actor.delegate(:verbose_method, another_actor, arg1, arg2) 89 | 90 | actor.delegation(:verbose_method).to(another_actor).with(arg1, arg2).call 91 | 92 | actor.delegation(:verbose_method).to(another_actor).call(arg1, arg2) 93 | ``` 94 | 95 | _That's great, but why do I need to do these extra steps? I just want to run the method._ 96 | 97 | Casting gives you the option to do what you want. You can run just a single method once, or alter your object to always delegate. Even better, you can alter your object to delegate temporarily... 98 | 99 | ### Temporary Behavior 100 | 101 | Casting also provides an option to temporarily apply behaviors to an object. 102 | 103 | Once your class or object is a `Casting::Client` you may send the `delegate_missing_methods` message to it and your object will use `method_missing` to delegate methods to a stored attendant. 104 | 105 | ```ruby 106 | class Actor 107 | include Casting::Client 108 | delegate_missing_methods 109 | end 110 | actor = Actor.new 111 | 112 | actor.hello_world #=> NoMethodError 113 | 114 | Casting.delegating(actor => GreetingModule) do 115 | actor.hello_world #=> output the value / perform the method 116 | end 117 | 118 | actor.hello_world #=> NoMethodError 119 | ``` 120 | 121 | The use of `method_missing` is opt-in. If you don't want that mucking up your method calls, just don't tell it to `delegate_missing_methods`. 122 | 123 | Before the block is run in `Casting.delegating`, a collection of delegate objects is set in the current Thread for the provided attendant. Then the block yields, and an `ensure` block cleans up the stored attendant. 124 | 125 | This allows you to nest your `delegating` blocks as well: 126 | 127 | ```ruby 128 | actor.hello_world #=> NoMethodError 129 | 130 | Casting.delegating(actor => GreetingModule) do 131 | actor.hello_world #=> output the value / perform the method 132 | 133 | Casting.delegating(actor => OtherModule) do 134 | actor.hello_world #=> still works! 135 | actor.other_method # values/operations from the OtherModule 136 | end 137 | 138 | actor.other_method #=> NoMethodError 139 | actor.hello_world #=> still works! 140 | end 141 | 142 | actor.hello_world #=> NoMethodError 143 | ``` 144 | 145 | Currently, by using `delegate_missing_methods` you forever mark that object or class to use `method_missing`. This may change in the future. 146 | 147 | ### Manual Delegate Management 148 | 149 | If you'd rather not wrap things in the `delegating` block, you can control the delegation yourself. 150 | For example, you can `cast_as` and `uncast` an object with a given module: 151 | 152 | ```ruby 153 | actor.cast_as(GreetingModule) 154 | actor.hello_world # all subsequent calls to this method run from the module 155 | actor.uncast # manually cleanup the delegate 156 | actor.hello_world # => NoMethodError 157 | ``` 158 | 159 | These methods are only defined on your `Casting::Client` object when you tell it to `delegate_missing_methods`. Because these require `method_missing`, they do not exist until you opt-in. 160 | 161 | ### Duck-typing with NullObject-like behavior 162 | 163 | Casting has a few modules built in to help with treating your objects like null objects. 164 | Take a look at the following example: 165 | 166 | ```ruby 167 | module SpecialStuff 168 | def special_link 169 | # some link code 170 | end 171 | end 172 | 173 | special_user.cast_as(SpecialStuff) 174 | special_user.special_link # outputs your link 175 | ``` 176 | 177 | If your app, for example, generates a list of info for a collection of users, how do you manage the objects which don't have the expected behavior? 178 | 179 | ```ruby 180 | [normal_user, other_user, special_user].each do |user| 181 | user.special_link #=> blows up for normal_user or other_user 182 | end 183 | ``` 184 | 185 | You can cast the other objects with `Casting::Null` or `Casting::Blank`: 186 | 187 | ```ruby 188 | normal_user.cast_as(Casting::Null) 189 | other_user.cast_as(Casting::Blank) 190 | special_user.cast_as(SpecialStuff) 191 | 192 | [normal_user, other_user, special_user].each do |user| 193 | user.special_link #=> normal_user yields nil, other_user yields "", and special_user yields the special_link 194 | end 195 | ``` 196 | 197 | ## I have a Rails app, how does this help me? 198 | 199 | Well, a common use for this behavior would be in using decorators. 200 | 201 | When using a wrapper, your forms can behave unexpectedly 202 | 203 | ```ruby 204 | class UsersController 205 | def edit 206 | @user = UserDecorator.new(User.find(params[:id])) 207 | end 208 | end 209 | 210 | <%= form_for(@user) do |f| %> #=>