Page 40 of 170

Upgrading to Ruby on Rails 5.0 from Rails 4.2 – application use case

In this article, I’ll try to cover all the issues that I had, when I was upgrading one of my Rails app from Ruby on Rails 4.2 to Ruby on Rails 5.0.

Note: This is not a complete, covering every possible case tutorial. You might encounter issues that I didn't. If so, feel free to comment and I will try to update the post with other issues and solutions.

Preparations – What should we do before upgrading to Rails 5?

  • Upgrade your Ruby version at least to 2.2.2 (I would recommend 2.3, since it is going to be released soon)
  • Upgrade bundler
  • Upgrade your application to the most recent Rails 4.2 version
  • Check gems compatibility (you may want to be on edge with few gems (or use Rails 5 branches if available))
  • Write more tests if you don’t have a decent code coverage

The last point is the most important. If you don’t have a good code coverage level and you lack tests, upgrading from Rails 4.2 to Rails 5 might be a big problem.

Upgrading to Rails 5 – Gemfile

Currently there are some gems incompatibilities (I will cover them later) and with some, you need to go edge. Here's a list of gems that I had to switch to edge (or use a specific version) in order to get things running:

gem 'rails', '5.0.0.beta1'
gem 'devise', github: 'plataformatec/devise'
gem 'responders', github: 'plataformatec/responders'
gem 'ransack', github: 'activerecord-hackery/ransack'
gem 'kaminari', github: 'amatsuda/kaminari'
gem 'mailboxer', github: 'mailboxer/mailboxer'

group :test do
  gem 'rspec', github: 'rspec/rspec'
  gem 'rspec-mocks', github: 'rspec/rspec-mocks'
  gem 'rspec-expectations', github: 'rspec/rspec-expectations'
  gem 'rspec-support', github: 'rspec/rspec-support'
  gem 'rspec-core', github: 'rspec/rspec-core'
  gem 'rspec-rails', github: 'rspec/rspec-rails', branch: 'rails-5-support-patches'
  gem 'rails-controller-testing'
end

Note that I've added a rails-controller-testing gem.This gem brings back assigns to your controller tests as well as assert_template to both controller and integration tests. If you use those methods, after upgrading to Ruby on Rails 5, you will have to add it.

After updating your Gemfile you can do a

bundle install

and hopefully you’re ready for the upgrade!

Configuration files – rake rails:update

There are some changes in configuration files (that I will cover), but I would strongly recommend running:

rake rails:update

allow it to overwrite your files and then just add the stuff that was gone (git diff). IMHO it is way less complex approach, that trying to add all new config options manually.

In my case, following files were affected:

On branch rails5
Changes not staged for commit:
    modified:   bin/rails
    modified:   bin/rake
    modified:   config/application.rb
    modified:   config/boot.rb
    modified:   config/environments/development.rb
    modified:   config/environments/production.rb
    modified:   config/environments/test.rb
    modified:   config/initializers/assets.rb
    modified:   config/initializers/cookies_serializer.rb
    modified:   config/initializers/session_store.rb
    modified:   config/initializers/wrap_parameters.rb

Untracked files:
    bin/setup
    bin/update
    config/initializers/application_controller_renderer.rb
    config/initializers/backtrace_silencers.rb
    config/initializers/cors.rb
    config/initializers/inflections.rb
    config/initializers/mime_types.rb
    config/initializers/request_forgery_protection.rb
    config/redis/

However not all the changes are worth being mentioned (sometimes there were just comments changes).

bin/rails and bin/rake

If you didn't have any custom stuff there, just go with the flow ;)

config/application.rb and config/boot.rb

I'll just quote the comment from application.rb (although it is not a new feature it is still worth reminding):

Settings in config/environments/* take precedence over those specified here. Application configuration should go into files in config/initializers all .rb files in that directory are automatically loaded.

And this is exactly what you should do. If you have any custom stuff in application.rb - just move it to initializers. And what about boot.rb? Leave it as it is.

config/environments/*.rb

development.rb - feature toggling and file watcher

There's something quite interesting in new development.rb: feature toggling. Thanks to some files that are in tmp/ you can disable/enable some features to test them in the dev mode, without having to switch to production mode (for example caching).

Rails.root.join('tmp/caching-dev.txt').exist?

There is also (commented) file watcher added. You can use it to have an auto-reloading based on your file changes:

# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
test.rb - random order

Tests now will run in a random order. If your tests rely on their execution order, they it is time to rethink them.

config.active_support.test_order = :random
production.rb - no more serve_static_files and Action Cable

Rack cache is out (config.action_dispatch.rack_cache). The config.serve_static_files = false flag is being replaced by:

config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?

There are also few configuration options for Action Cable available:

# Action Cable endpoint configuration
config.action_cable.url = 'wss://example.com/cable'
config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ]

config/initializers/*.rb

application_controller_renderer.rb

New initializer configuration for application controller renderer.  This is a really good feature that finally allows to drop some "bypass like" solutions. If you didn't render outside of controllers scope, you can leave this commented.

ApplicationController.renderer.defaults.merge!(
http_host: 'example.org',
https: false
)

cors.rb

Here are all the settings related to CORS. Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. If you don't know what that means, it means that you can remove this file.

config/redis/cable.yml

Redis settings for ActionCable. Nothing special except one thing: why do we have config/database.yml and config/redis/cable.yml instead of config/databases/redis.yml and config/databases/mysql.yml (or something similar)?

Starting your freshly updated Rails 5 application

As I said before, if you have a decent code coverage level and you know what you’re doing, upgrade should not be a big problem.

At this point, if you’re not using any fancy route settings or any other magic, you should be able to at least start your application:

rails s

Puma 2.15.3 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:3000

However don't expect it to work correctly... yet ;)

Monkey patching gems

We're just before Christmas, so I don't expect gem maintainers to fix them. That's why we will fix some of them on our own. Here's a list of gems with some incompatibilities (even when using edge) that I've encountered (not including Rails gems):

Kaminari

If you see this error:

Generating an URL from non sanitized request parameters is insecure!

Just use the master branch source code from Github:

gem 'kaminari', github: 'amatsuda/kaminari'

ActiveRecord Import - undefined method type_cast

/activerecord-import/import.rb:479→ block (2 levels) in values_sql_for_columns_and_attributes
/activerecord-import/import.rb:469→ each
/activerecord-import/import.rb:469→ each_with_index
/activerecord-import/import.rb:469→ each
/activerecord-import/import.rb:469→ map
/activerecord-import/import.rb:469→ block in values_sql_for_columns_and_attributes
/activerecord-import/import.rb:468→ map
/activerecord-import/import.rb:468→ values_sql_for_columns_and_attributes
/activerecord-import/import.rb:395→ import_without_validations_or_callbacks
/activerecord-import/import.rb:362→ import_with_validations
/activerecord-import/import.rb:304→ import_helper
/activerecord-import/import.rb:246→ import

To fix this, create an activerecord_import_patch.rb file in initializers and place this code:

class ActiveRecord::Base
  class << self
    def values_sql_for_columns_and_attributes(columns, array_of_attributes)   # :nodoc:
      connection_memo = connection
      array_of_attributes.map do |arr|
        my_values = arr.each_with_index.map do |val,j|
          column = columns[j]

          if val.nil? && column.name == primary_key && !sequence_name.blank?
             connection_memo.next_value_for_sequence(sequence_name)
          elsif column
            if column.respond_to?(:type_cast_from_user)
              connection_memo.quote(column.type_cast_from_user(val), column)
            elsif column.respond_to?(:type_cast)
              connection_memo.quote(column.type_cast(val), column)
            else
              connection_memo.quote(val)
            end
          end
        end
        "(#{my_values.join(',')})"
      end
    end
  end
end

ActsAsTaggableOn - Invalid Rails version detection

If you'll try using ActsAsTaggableOn with Rails 5, you'll end up with this exception

ArgumentError (Unknown key: :order. Valid keys are: :class_name, :anonymous_class,
:foreign_key, :validate, :autosave, :table_name, :before_add, :after_add, :before_remove,
:after_remove, :extend, :primary_key, :dependent, :as, :through, :source, :source_type,
:inverse_of, :counter_cache, :join_table, :foreign_type, :index_errors):
  app/models/concerns/taggable.rb:6:in `block in <module:Taggable>'
  app/models/text.rb:6:in `include'
  app/models/text.rb:6:in `<class:Text>'
  app/models/text.rb:2:in `<top (required)>'
  app/services/portal/main_service.rb:5:in `<class:MainService>'
  app/services/portal/main_service.rb:3:in `<module:Portal>'
  app/services/portal/main_service.rb:1:in `<top (required)>'
  app/controllers/portal/main_controller.rb:5:in `block in <class:MainController>'
  app/controllers/portal/main_controller.rb:6:in `block in <class:MainController>'
  app/controllers/portal/main_controller.rb:13:in `index'
  lib/middlewares/nofollow_anchors.rb:23:in `call'

it's directly related to this code:

def has_many_with_taggable_compatibility(name, options = {}, &extention)
  if ActsAsTaggableOn::Utils.active_record4?
    scope, opts = build_taggable_scope_and_options(options)
    has_many(name, scope, opts, &extention)
  else
    has_many(name, options, &extention)
  end
end

and as you've probably already guested, #active_record4? returns false and it will try to use Rails 3 method call. To fix this, create an acts_as_taggable_on.rb file in config/initializers and place there following code:

module ActsAsTaggableOn::Utils
  def self.active_record4?
    true
  end
end

after a restart, this problem should be solved.

Decent Exposure - methods that no longer exist

This should be considered as a temporary patch. It is not a solid solution (I just wanted to make it work - not to fix decent exposure).

decent_exposure/expose.rb:14:in `block in extended': undefined method `hide_action'
  for ActionController::Base:Class (NoMethodError)
  from /gems/decent_exposure-2.3.2/lib/decent_exposure/expose.rb:7:in `class_eval'
  from /gems/decent_exposure-2.3.2/lib/decent_exposure/expose.rb:7:in `extended'
  from /gems/decent_exposure-2.3.2/lib/decent_exposure.rb:5:in `extend'
  from /gems/decent_exposure-2.3.2/lib/decent_exposure.rb:5:in `block in <top (required)>'
  from /gems/activesupport-5.0.0.beta1/lib/active_support/lazy_load_hooks.rb:38:in `instance_eval'
  from /gems/activesupport-5.0.0.beta1/lib/active_support/lazy_load_hooks.rb:38:in `execute_hook'
  from /gems/activesupport-5.0.0.beta1/lib/active_support/lazy_load_hooks.rb:45:in `block in run_load_hooks'
  from /gems/activesupport-5.0.0.beta1/lib/active_support/lazy_load_hooks.rb:44:in `each'
  from /gems/activesupport-5.0.0.beta1/lib/active_support/lazy_load_hooks.rb:44:in `run_load_hooks'
  from /gems/actionpack-5.0.0.beta1/lib/action_controller/base.rb:262:in `<class:Base>'
  from /gems/actionpack-5.0.0.beta1/lib/action_controller/base.rb:164:in `<module:ActionController>'
  from /gems/actionpack-5.0.0.beta1/lib/action_controller/base.rb:5:in `<top (required)>'
  from /bundler/gems/ransack-2a3759317a44/lib/ransack.rb:38:in `<top (required)>'
  from /gems/bundler-1.10.6/lib/bundler/runtime.rb:76:in `require'
  from /gems/bundler-1.10.6/lib/bundler/runtime.rb:76:in `block (2 levels) in require'
  from /gems/bundler-1.10.6/lib/bundler/runtime.rb:72:in `each'
  from /gems/bundler-1.10.6/lib/bundler/runtime.rb:72:in `block in require'
  from /gems/bundler-1.10.6/lib/bundler/runtime.rb:61:in `each'
  from /gems/bundler-1.10.6/lib/bundler/runtime.rb:61:in `require'
  from /gems/bundler-1.10.6/lib/bundler.rb:134:in `require'
  from /home/mencio/Software/Senpuu/config/application.rb:7:in `<top (required)>

Decent Exposure gem uses some methods from ActionController::Base, that apparently have changed (they no longer exist). To fix that, we will just create dummy methods that will just play along with the Decent Exposure internal logic (config/initializers/decent_exposure.rb):

# Patches for Rails 5 and decent exposure
# Put the line below above the require rails line!
# require File.expand_path('../initializers/decent_exposure', __FILE__)
# require 'rails/all'
module DecentExposure
  module Expose
      def hide_action(action)
    end

    def protected_instance_variables
      []
    end
  end
end

If it goes about DecentExposure, we need to make one more change. We need to add a require on top of the application.rb file:

# The first and last line are already there - put the decent_exposure one in the middle
require File.expand_path('../boot', __FILE__)
require File.expand_path('../initializers/decent_exposure', __FILE__)
require 'rails/all'

We had to patch this gem before it is loaded, because on load it already performs some logic that requires non-existing methods.

Code base changes

If you're not planning to use any new Rails features and just (for now) stay with what you had, you won't have many things to change.

ApplicationRecord instead of ActiveRecord::Base

It is like an ApplicationController. Just an extra inheritance layer just in case you want to have some global (for all models) elements. Just add application_model.rb and change inheritance in all the models:

# Base AR class for all other
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end
class Comment < ApplicationRecord
end

Note: this is not a must be. Your app should work even if you don't change it.

ActionController::Parameters: params are no longer a Hash

Values that are being sent to params are no longer a hash internally. Now they are a XD, so type checking like this:

params[:q].is_a?(Hash)

have to be replaced with:

params[:q].is_a?(ActionController::Parameters)

ActiveRecord::ReadOnlyRecord: Picture is marked as readonly

Note: this is a dirty hack.

Sometimes I load objects with include and join in a default scope:

scope :with_picture, -> { joins(:picture).includes(:picture) }

default_scope lambda {
  self.require_picture? ? with_picture : where(false)
}

up until now I could update/remove the main object and its pictures. However now when I fetch object, its pictures are marked as readonly so I get this exception:

ActiveRecord::ReadOnlyRecord: Picture is marked as readonly

activerecord-5.0.0.beta1/lib/active_record/persistence.rb:532→ create_or_update
activerecord-5.0.0.beta1/lib/active_record/callbacks.rb:298→ block in create_or_update
activesupport-5.0.0.beta1/lib/active_support/callbacks.rb:126→ call
activesupport-5.0.0.beta1/lib/active_support/callbacks.rb:506→ block (2 levels) in compile
activesupport-5.0.0.beta1/lib/active_support/callbacks.rb:455→ call
activesupport-5.0.0.beta1/lib/active_support/callbacks.rb:101→ __run_callbacks__
activesupport-5.0.0.beta1/lib/active_support/callbacks.rb:750→ _run_save_callbacks
activerecord-5.0.0.beta1/lib/active_record/callbacks.rb:298→ create_or_update
activerecord-5.0.0.beta1/lib/active_record/suppressor.rb:41→ create_or_update
activerecord-5.0.0.beta1/lib/active_record/persistence.rb:125→ save
activerecord-5.0.0.beta1/lib/active_record/validations.rb:44→ save
activerecord-5.0.0.beta1/lib/active_record/attribute_methods/dirty.rb:22→ save

I wouldn't be surprised at all (I know this error) but for some reason it was working with 4.2. So if you want to migrate and deal with it later, add this to your dependent models (those for which this error happens):

before_save do
  instance_variable_set(:@readonly, false)
  true
end

before_destroy do
  instance_variable_set(:@readonly, false)
  true
end

ActiveRecord::Base default_scope lambda change

Note: I think this also applies to standard scopes.

Not sure if it was a bug or a feature - but it is gone. You can no longer return a nil from a lambda default_scope:

# Won't work anymore
default_scope lambda {
  self.require_file? ? with_file : nil
}

if you try using it and it will return nil, you will see:

ArgumentError (invalid argument: nil.):
  app/services/portal/main_service.rb:34:in `recent_texts_pictures'
  app/services/portal/main_service.rb:73:in `max_pictures'
  app/services/portal/main_service.rb:46:in `pictures'
  app/services/portal/main_service.rb:19:in `pictures_page'
  app/controllers/portal/main_controller.rb:6:in `block in <class:MainController>'
  app/controllers/portal/main_controller.rb:13:in `index'
  lib/middlewares/nofollow_anchors.rb:23:in `call'

instead of returning nil, just return where(false):

default_scope lambda {
  self.require_file? ? with_file : where(false)
}

RSpec changes

get/post/put/delete with format also for html

When I don't provide format: :html, it stopped assuming that is is a html request:

before { get :edit }
before { post :create }
before { put :update }
before { delete :destroy }

examples above fail with following message:

Failure/Error: super(resources, options)
      
  ActionController::UnknownFormat:
    ActionController::UnknownFormat

to fix that, just add format: :html to your requestes:

before { get :edit, format: :html }
before { post :create, format: :html }
before { put :update, format: :html }
before { delete :destroy, format: :html }

Note: This seems to be related to responders gem, however adding format fixes the issue.

Symbols to strings for some specs

When matching and expecting resources to receive request parameters, you must send them in string format:

# The first one was no longer working because params now convert it to strings
- let(:mailboxer_message_params) { { mailboxer_message: { body: '' } } }
+ let(:mailboxer_message_params) { { 'mailboxer_message' => { 'body' => '' } } }
before/after actions except/only are now blocks internally

I hade some custom extensions for RSpec, that are checking permissions. Up until now, there were (most of the times) symbols inside:

except: %i( index show )

If we wanted to get it back from the controller (outside of requests scopes) we were doing something like that:

before_actions = subject
  ._process_action_callbacks
  .select { |f| f.kind == :before }

unlesses = before_actions.map { |bf| bf.instance_variable_get(:'@unless') }

this part hasn't change. We still can obtain our unless/only that way. What did change is what is underneath (in what we get).

In Rails 4.2 we would get strings that we could compare with what we've expected:

expect(unlesses.flatten.compact.join).to include "action_name == '#{action}'"

Now each element is a block that accepts a stringified action name and responds with a boolean:

unlesses.flatten.compact.each do |callable|
  # callable.call('index') #=> true, etc
  expect(callable.call(double(action_name: action.to_s))).to eq true
end
RSpec config settings that are no longer valid

Remove all that I've listed below since they are no longer valid:

  • config.disable_monkey_patching!
  • expectations.include_chain_clauses_in_custom_matcher_descriptions = true
  • mocks.verify_partial_doubles = true

Conclusion

As I've stated before: If you have a decent code coverage level and you know what you’re doing, upgrade should not be a big problem. However, I still recommend waiting at least until all the most popular gems are up2date.

Good luck!

Ruby 2.3.0 changes and features

Ruby 2.3.0-preview1 has been released. Let's see what new features we're getting this time:

Table of content

Frozen string literals

If you like reviewing gems, you could often find something like that:

module MyGem
  VERSION = '1.0.2'.freeze
end

Did you ever wonder why people tend to freeze their strings? There are 2 benefits of doing so:

  1. First of all, you tell other programmers that this string should not change - never. It should remain as it is and it is intended (in general this is why freeze exists)
  2. Performance is a factor as well. Frozen strings won't be changed in the whole application, so Ruby can take advantage of it and reduce object allocation and memory usage

It's worth pointing out, that Ruby 3.0 might consider all strings immutable by default (1, 2). That means that you can use current version of this feature as a first step to prepare yourself for that.

How can you start using "frozen by default" strings without having to mark all of them with #freeze? For now, you will have to add following magic comment at the beginning of each Ruby file:

# frozen_string_literal: true

# The comment above will make all strings in a current file frozen

Luckily, as expected from Ruby community, there's already a gem that will add the magic comment to all the files from your Ruby projects: magic_frozen_string_literal.

Ok, so we will lose possibility to change strings (if we won't explicitly allow that), what we will get in return? Performance. Here are the performance results of creating 10 000 000 strings all over again (with GC disabled). In my opinion results are astonishing (you can find the benchmark here). With frozen strings a simple "string based" script runs 44% faster then the version without freezing.

frozen

If you wonder what will happen, if you try to modify frozen string, here's a simple example you can run:

# frozen_string_literal: true
'string' << 'new part'

# output:
frozen.rb:3:in `<main>': can't modify frozen String (RuntimeError)

This description is pretty straightforward for smaller applications, but if you deal with many strings, from many places, you might have a bit of trouble finding an exact place where this particular string was created. That's when --enable-frozen-string-literal-debug flag for Ruby becomes handy:

# frozen_string_literal: true
'string' << 'new part'

# Execute with --enable-frozen-string-literal-debug flag
# ruby --enable-frozen-string-literal-debug script.rb
# output:
frozen.rb:3:in `<main>': can't modify frozen String, created at frozen.rb:3 (RuntimeError)

it will tell you not only the place where you tried to modify a frozen string, but also a place where this string has been created.

Immutable strings make us also one step closer to understanding, introducing and using the concept of immutable objects in Ruby.

Safe navigation operator

Easy nil saving FTW! Finally a ready to go replacement for many #try use cases (however not for all of them). Small yet really sufficient feature, especially for non-Rails people. So what exactly Object#try was doing?

Object#try - Invokes the public method whose name goes as first argument just like public_send does, except that if the receiver does not respond to it the call returns nil rather than raising an exception.

This is how you can use it:

# Ruby 2.2.3
user = User.first

if user && user.profile
  puts "User: #{user.profile.nick}"
end

# Ruby 2.3.0
user = User.first

if user&.profile
  puts "User: #{user.profile.nick}"
end

At first that might not look too helpful but image a chain of checks similar to this one:

if user&.profile&.settings&.deliver?
# and so on...

Warning: It is worth pointing out that the Object#try from ActiveSupport and the safe navigator operation differs a bit in their behaviour so you need to closely consider each case in which you want to replace one with another (both ways). When we have a nil object on which we want to invoke the method (both via try or safe navigator), they behave the same:

user = User.first # no users - user contain nil
user.nil? #=> true

user.try(:name) #=> nil
user&.name #=> nil

However, their behaviour strongly differs when we have non-nil objects that may (or may not) respond to a given method:

class User
  def name
    'Foo'
  end
end

class Student < User
  def surname
    'Bar'
  end
end

user = User.new

# This wont fail - even when user exists but does not respond to a #surname method
if user.try(:surname)
  puts user.surname
end

user = Student.new

if user.try(:surname)
  puts user.surname
end

# With an object that has a #surname method the safe
# navigation operation will work exactly the same as try
 
if user&.surname
  puts user.surname
end

# However it will fail if object exists but does not have a #surname method

user = User.new

if user&.surname
  puts user.surname
end

NoMethodError: undefined method `surname' for #<User:0x000000053691d8>

Does this difference really matter? I strongly believe so! #try gives you way more freedom in terms of what you can do with it. You can have multiple objects of different types and you can just try if they are not nil and if they respond to a given method and based on that react. However it is not the case with the nil safe operator - it requires an object (except nil) to respond to a given method - otherwise it will raise an NoMethodError. This is how they look (more or less) when implemented in Ruby:

# try method simplified
# We skip arguments for simplicity
def try(method_name)
  return nil if nil?
  return nil unless respond_to?(method_name)
  send(method_name)
end

def safe_navigator(method_name)
  return nil if nil?
  # Does not care if an object does not respond to a given method
  send(method_name)
end

And that's why, when replacing #try with the safe navigation operator (or the other way around), you need to be extra cautious!

Did you mean

A small helper for debugging process directly built in into Ruby. In general it will help you detect any typos and misspellings that cause your code to fail. Will it become handy? Well I tell you once I'll use it for a while.

class User
  def name
  end
end

user = User.new
use.name

# Error you will receive:
user.rb:7:in `<main>': undefined local variable or method `use' for main:Object (NameError)
Did you mean?  user

Hash Comparison

This is a really nice feature. Comparing hashes like numbers. Up until now we had the #include? method on a Hash, however it could only check if a given key is included (we could not check if one hash contains a different one):

{ a: 1, b: 2 }.include?(:a) #=> true
{ a: 1, b: 2 }.include?(a: 1) #=> false

Now you can check it using comparisons:

{ a: 1, b: 2 } >= { a: 1 } #=> true
{ a: 1, b: 2 } >= { a: 2 } #=> false

That way can compare not only keys but also their values. Here are some examples that should help you understand how it goes:

Same keys and content

{ a: 1 } <= { a: 1 } #= true
{ a: 1 } >= { a: 1 } #= true
{ a: 1 } == { a: 1 } #= true
{ a: 1 } >  { a: 1 } #= false
{ a: 1 } <  { a: 1 } #= false

Same keys, different content

{ a: 1 } <= { a: 2 } #= false
{ a: 1 } >= { a: 2 } #= false
{ a: 1 } == { a: 2 } #= false
{ a: 1 } >  { a: 2 } #= false
{ a: 1 } <  { a: 2 } #= false

Same content, different keys

{ a: 1 } <= { b: 1 } #= false
{ a: 1 } >= { b: 1 } #= false
{ a: 1 } == { b: 1 } #= false
{ a: 1 } >  { b: 1 } #= false
{ a: 1 } <  { b: 1 } #= false

Hash containing different one (with same values)

{ a: 1, b: 2 } <= { a: 1 } #= false
{ a: 1, b: 2 } >= { a: 1 } #= true
{ a: 1, b: 2 } == { a: 1 } #= false
{ a: 1, b: 2 } >  { a: 1 } #= true
{ a: 1, b: 2 } <  { a: 1 } #= false

Hash containing different one (with different values)

{ a: 1, b: 2 } <= { a: 3 } #= false
{ a: 1, b: 2 } >= { a: 3 } #= false
{ a: 1, b: 2 } == { a: 3 } #= false
{ a: 1, b: 2 } >  { a: 3 } #= false
{ a: 1, b: 2 } <  { a: 3 } #= false

Number #negative? and #positive?

A simple example should be enough to describe this feature:

elements = (-10..10)

elements.select(&:positive?) #=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
elements.select(&:negative?) #=> [-10, -9, -8, -7, -6, -5, -4, -3, -2, -1]

Hash#dig and Array#dig

Ruby was always about doing things in a short and easy way. Now this can be also said about retrieving deeply nested data from hashes and arrays thanks to #dig. No more || {} tricks or anything like that!

# Nested hashes
settings = {
  api: {
    adwords: {
      url: 'url'
    }
  }
}

settings.dig(:api, :adwords, :url) # => 'url'
settings.dig(:api, :adwords, :key) # => nil

# Way better than this:
((settings[:api] || {})[:adwords] || {})[:url]
# Nested arrays
nested_data = [
  [
    %w( a b c )
  ],
  []
]

nested_data.dig(0, 0, 0) # => 'a'
nested_data.dig(0, 1, 0) # => nil

# Way better than this:
((nested_data[0] || [])[0] || [])[0]

Hash#to_proc

This feature allows to use hashes to iterate over enumerables

h = { a: 1, b: 2, c: 3 }
k = %i{ a b c d }

k.map(&h)

Hash#fetch_values

More strict version of Hash#values_at:

settings = {
  name: 'app_name',
  version: '1.0'
}

settings.values_at(:name, :key) #=> ['app_name', nil]
settings.fetch_values(:name, :key) #=> exception will be raised
KeyError: key not found: :key

Enumerable#grep_v

I could use this one few times. Instead of negating a regexp itself, we can now just specify that we want all the elements that does not match a given regexp. It also works for filtering by types.

# Regexp example
data = %w(
  maciej@mensfeld.pl
  notanemail
  thisisnotanemailaswell@
)

regexp = /.+\@.+\..+/

data.grep(regexp) #=> ['maciej@mensfeld.pl']
data.grep_v(regexp) #=> ['notanemail', 'thisisnotanemailaswell@']
# Type matching example
data = [nil, [], {}]

data.grep(Array) #=> [[]]
data.grep_v(Array) #=> [nil, {}]

Conclusions

It's been a while since we had a new Ruby version with some additional features. It's not a big step but it definitely brings something to the table. Nothing that we would not be able to do with an old plain Ruby, but now many things will require much less work. If we consider this release as a transitional one, it feels that things are heading in a good direction (towards 3.0).

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑