/ruby3-backward-compatibility

Backward compatibility for Ruby 3 stdlib

Primary LanguageRubyMIT LicenseMIT

ruby3-backwards-compatibility

This gem provides a compatibility layer to Ruby 3 projects that still have legacy code written against Ruby 2's stdlib. Only use this if you upgrade to Ruby 3 but still depend on some third-party code that breaks on Ruby 3.

The gem will fix only some (but the most common) incompatibilities.

Installation

Add this line to your application's Gemfile:

gem 'ruby3-backward-compatibility'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install ruby3-backward-compatibility

Usage

You need to require the specific backports yourself, see below.

You can also require all included backports using

require 'ruby3-backward-compatibility/compatibility/all'

Note however that this will not patch anything that has not been required yet. You can always require single patches as shown below.

List of backports

Ruby 3 keyword arguments

One breaking change in Ruby 3 is that methods defined using keyword arguments have to be called with keyword arguments, not with option hashes. For example

class Ruby3Class
  def some_method(foo:)
    puts foo
  end
end

Ruby3Class.new.some_method(foo: 'bar') # works
Ruby3Class.new.some_method({ foo: 'bar' }) # raises an ArgumentError

To fix this for arbitrary methods, you can use the callable_with_hash method included in this gem, like this:

require 'ruby3_backward_compatibility'

class Ruby3Class
  # reopen the class

  extend Ruby3BackwardCompatibility::CallableWithHash

  callable_with_hash :some_method
end

Ruby3Class.new.some_method(foo: 'bar') # still works
Ruby3Class.new.some_method({ foo: 'bar' }) # now works as well

This will wrap the given method and convert a hash given as the last argument into keywords, similar to how it worked in Ruby 2.

Note: In case the method is also defined in a prepended module, you need to put the extend Ruby3BackwardCompatibility::CallableWithHash above the prepend.

ERB

ERB.new used to have the signature

ERB.new(string, safe_level, trim_mode, eoutvar = '_erbout')

but this was changed to

ERB.new(string, safe_level: nil, trim_mode: nil, eoutvar: '_erbout')

To allow both styles, use

require 'ruby3_backward_compatibility/compatibility/erb'

I18n

I18n has a few methods (translate, localize, etc.) that requires to be called with keywords.

To allow calling it with option hashes, too, use

require 'ruby3_backward_compatibility/compatibility/i18n'

Object

The methods Object#taint and Object#untaint were no-ops for a while but started to raise deprecation warnings.

To add them back as no-ops, use

require 'ruby3_backward_compatibility/compatibility/object'

YAML (Psych)

Psych version 4 (default for Ruby 3.1) has two changes:

  • Psych.load has been renamed to Psych.unsafe_load

  • The signature of Psych.safe_load has been changed from

    Psych.safe_load(yaml, permitted_classes = [], permitted_symbols = [], aliases = false, filename = nil)
    

    to

    Psych.safe_load(yaml, permitted_classes: [], permitted_symbols: [], aliases: false, filename: nil)
    

To allow both styles of calling Psych.safe_load, use

require 'ruby3_backward_compatibility/compatibility/psych'

Attention: There has been a very good reason why Psych renamed the .load method: You may never use .load on any external strings. It is possible to create valid YAML strings that lead to the execution of arbitrary code, so calling YAML.load on user input is a major security vulnerability. For that reason, this library does not change the safe behavior of YAML.load.

String

String#encode require keyword arguments. To allow it to be called with an options hash, use

require 'ruby3_backward_compatibility/compatibility/string'

URI

The URI module allows other libraries to register their own URI schemes. In the past, it was possible to do something like

module URI
  @schemes['MYSCHEME'] = MySchemeImplementation
end

In Ruby 3, you have to use

URI.register_scheme 'MYSCHEME', MySchemeImplementation

To add back the old behavior, use

require 'ruby3_backward_compatibility/compatibility/uri'

Things we cannot fix

Proc.new

In Ruby 2, it was possible to use Proc.new without giving a block. In this case, the Proc took the block from the current context. A common usecase worked like this:

def call_me(block = Proc.new)
  block.call
end

block = proc do
  puts "called"
end

call_me(block) # works
call_me(&block) # also works
call_me { puts "called" } # also works

This is no longer possible in Ruby 3 and we are not aware of a way to make a generic port for this.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

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 the created tag, and push the .gem file to rubygems.org.

License

The gem is available as open source under the terms of the MIT License.