Project

General

Profile

Feature #18951

Updated by byroot (Jean Boussier) over 1 year ago

### Use case 

 A very common pattern in Ruby, especially in testing is to save the value of an attribute, set a new value, and then restore the old value in an `ensure` clause. 

 e.g. in unit tests 

 ```ruby 
 def test_something_when_enabled 
   enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true 
   # test things 
 ensure 
   SomeLibrary.enabled = enabled_was 
 end 
 ``` 

 Or sometime in actual APIs: 

 ```ruby 
 def with_something_enabled 
   enabled_was = @enabled 
   @enabled = true 
   yield 
 ensure 
   @enabled = enabled_was 
 end 
 ``` 

 There is no inherent problem with this pattern, but it can be easy to make a mistake, for instance the unit test example: 

 ```ruby 
 def test_something_when_enabled 
   some_call_that_may_raise 
   enabled_was, SomeLibrary.enabled = SomeLibrary.enabled, true 
   # test things 
 ensure 
   SomeLibrary.enabled = enabled_was 
 end 
 ``` 

 In the above if `some_call_that_may_raise` actually raises, `SomeLibrary.enabled` is set back to `nil` rather than its original value. I've seen this mistake quite frequently. 

 ### Proposal 

 I think it would be very useful to have a method on Object to implement this pattern in a correct and easy to use way. The naive Ruby implementation would be: 

 ```ruby 
 class Object 
   def with(**attributes) 
     old_values = {} attributes.dup 
     attributes.each_key do |key| 
       old_values[key] = public_send(key) 
     end 
     begin 
       attributes.each do |key, value| 
         public_send("#{key}=", value) 
       end 
       yield 
     ensure 
       old_values.each do |key, old_value| 
         public_send("#{key}=", old_value) 
       end 
     end 
   end 
 end 
 ``` 

 NB: `public_send` is used because I don't think such method should be usable if the accessors are private. 

 With usage: 

 ```ruby 
 def test_something_when_enabled 
   SomeLibrary.with(enabled: true) do 
     # test things 
   end 
 end 
 ``` 

 ```ruby 
 GC.with(measure_total_time: true, auto_compact: false) do 
   # do something 
 end 
 ``` 

 ### Alternate names and signatures 

 If `#with` isn't good, I can also think of: 

   - `Object#set` 
   - `Object#apply` 

 But the `with_` prefix is by far the most used one when implementing methods that follow this pattern. 

 Also if accepting a Hash is dimmed too much, alternative signatures could be: 

   - `Object#set(attr_name, value)` 
   - `Object#set(attr1, value1, [attr2, value2], ...)` 

Back