Today I Learned

18 posts about #ruby

Skipp model instance callbacks in Sequel

In ActiveRecord there are several ways to update a record. Each technique / method is described in the documentation, so you are aware of which methods will trigger the callbacks and which ones will skip them. In Sequel it’s not that obvious. And the update, update_all and save - callbacks are triggered in all cases.

It turns out that you need to drop down on a dataset level. Either by:

HuddleScore.where(id: 14900).update(updated_at: DateTime.parse("04/11/2019 14:53"))

or in the following manner:

model_instance.this.update(updated_at: DateTime.parse("04/11/2019 14:53"))

Accessing helpers and paths in Ruby On Rails console

You can access path/url in two ways:

1) using app, for instance: app.game_path(1)

2) including Rails.application.routes.url_helpers, for instance:

include Rails.application.routes.url_helpers
game_path(1)

both will result with a string => "/games/1"

The above knowledge will be handy if you are going to use helpers with paths.

Accessing helpers in rails console

The easiest way to do this is through helper object, for instance

helper.number_to_currency(100) => "$100.00"

helper.link_to("test", app.game_path(1), method: :get) =>

"<a data-method=\"get\" href=\"/games/1\">test</a>"

The nice thing about is that you can use your helpers in the same fashion:

Some real life usage:

module EventsHelper
  def event_form_header(event)
    content_tag :h2, class: 'text-center pull-left' do
      if event.game
        "#{event.category.capitalize} for #{event.game.title}"
      else
        "Global #{event.category.capitalize}"
      end
    end
  end
end

=>

"<h2 class=\"text-center pull-left\">Cash_ladder for Gentleman Test Game</h2>"

Unfortunately, if you want to use your helper in console and this helper uses path inside you will have to first include Rails.application.routes.url_helpers in this helper (which is of course not needed if you are using it in views)

module PlayerHelper
  include Rails.application.routes.url_helpers

  def user_player_info(player)
    if player.user.removed_by_user?
      'User removed his account'
    else
      content_tag(:div, player.user.username) +
        link_to(player.reference,
                game_player_path(player.game, player),
                class: 'highlight')
    end
  end
end

helper.user_player_info(Player.last) =>

"<div>Varian</div><a class=\"highlight\" href=\"/games/2-hots/players/3\">Thrall</a>"

Ruby On Rails console sandbox mode

If you want to perform some data migration/modification on production and you want to be sure that it will not break anything you can always perform a test run using rails console with --sandbox flag.

After entering console with

rails console production --sandbox

you will get following information

Loading production environment in sandbox (Rails X.X.X)
Any modifications you make will be rolled back on exit

You can now safely perform data migration and check if everything is OK.

Truncate database in Ruby on Rails 6

Insteaed of chaining rails db:drop, rails db:create and rails db:migrate we are finally able to use one commend that will clean databse in case you have to seed it one more time:

rails db:truncate_all

NOTE: In older version of rails you can always use database_cleaner gem. Thanks to this gem you can run DatabaseCleaner.clean_with :truncation in rails console or create a rake task on your own.

Ruby on Rails with RSpec - yielding multiple arguments

If you are yielding many things in one yield statement, for instance:

def each
  author.books.each do |book|
    yield BookDecorator.new(book), EpicBookDecorator.new(book)
  end
end

you are able to test it in RSpec using yield_successive_args with arguments packed within an array:

expect { |b| book_collection.each(&b) }.to yield_successive_args(
  [book_decorator1, epic_book_decorator1],
  [book_decorator2, epic_book_decorator2]
)

** operator as a way to merge hashes && symbolize_keys

array.group_by(&:something).map do |scheduled_at, form_submissions|
  { scheduled_at: scheduled_at, **form_submissions.first.payload.symbolize_keys }
end

What’s happening here?

The double splat operator acts as a handy way to merge two hashes. But since the payload contains a json with keys being strings, it will throw an exception of TypeError: hash key "name" is not a Symbol

This is because of the Ruby’s implementation internal detail: The keyword arguments, which the double-splat operator is supposed to capture, are expressed in Ruby as Symbols. Hence, the symbolize_keys call.

Arel basic use case

veg = Arel::Table.new(:vegetables)

query = veg[:created_at].gteq( 5.days.ago ).and(
  veg[:color].eq("green").or(
    veg[:gardener].eq("Susan")
  )
)

query.to_sql
#  "vegetables"."created_at" >= '2016-12-13 03:54:28.575342'
#    AND ("vegetables"."color" = 'green' OR "vegetables"."gardener" = 'Susan')

Vegetable.where( query )

Triggered by birthday-cake-top-consumer!

FactoryBot: Constructing objects using Dry::Types

If you face error similar to the one below

Dry::Struct::Error: [YourClassName.new] :some_attribute_name is missing in Hash input

when building objects of the class using Dry::Types and FactoryBot, be advised, that

Although factory_bot is written to work with ActiveRecord out of the box, it can also work with any Ruby class. For maximum compatibility with ActiveRecord, the default initializer builds all instances by calling new on your build class without any arguments. It then calls attribute writer methods to assign all the attribute values. While that works fine for ActiveRecord, it actually doesn’t work for almost any other Ruby class.

The fix is to add following line to your factory definition

initialize_with  { new(attributes) }

How to change stubbed return value with another stub?

Simple - just re-define spy as a result of another stub

valid_token = instance_double(ValidToken)
allow(ValidToken).to receive(:new) { valid_token }
allow(valid_token).to receive(:to_s) { '123' }
allow(valid_token).to receive(:clear!) do
   allow(valid_token).to receive(:to_s) { '456' }
end
valid_token = ValidToken.new
valid_token.to_s # 123
valid_token.clear!
valid_token.to_s # 456

Stubbing responses from AWS services

We have started integration with Amazon SQS recently and did need to write some unit tests related to it. Unfortunately stubbing AWS client library the regular way turned out to be pretty cumbersome and challenging. Fortunately AWS SDK for ruby provides tools that make it pretty comfortable.

# Simple stubbing...
sqs_response_mock = Aws::SQS::Types::ReceiveMessageResult.new
sqs_response_mock.messages << Aws::SQS::Types::Message.new(body: 'abc')
Aws.config[:sqs] = {
    stub_responses: {
        receive_message: sqs_response_mock
    }
}

# ...allows properly polling the queue
poller = Aws::SQS::QueuePoller.new('https://sqs.us-east-2.amazonaws.com/123/some-queue')
poller.poll do |msg|
  puts msg.body
end

# => abc

Documentation can be found here

Passing multiple exceptions to rescue

To rescue multiple classes of exceptions, you have to pass them as a list to rescue method. Passing as an array won’t work, unless you prefix it with * (effectively breaking it into a list)

# Works
begin
  # some code here    
rescue ExceptionA, ExceptionB
  puts "uff, thanks!"
end

# Doesn't work
begin
  # some code here    
rescue [ExceptionA, ExceptionB]
  puts "uff, thanks!"
end

# Works
EXCEPTIONS = [ExceptionA, ExceptionB]
begin
  # some code here    
rescue *EXCEPTIONS
  puts "uff, thanks!"
end

Selenium::WebDriver::Error::NoSuchDriverError

We’ve recently experienced some peculiar errors when processing capybara-based automation scripts on Heroku. Most of the time, the error returned did not show anything useful…

Selenium::WebDriver::Error::NoSuchDriverError: no such session

yet for a brief period of time, following error was reported when attempting to access capybara session

Selenium::WebDriver::Error::UnknownError: unknown error: session deleted because of page crash
from tab crashed

Finally, after spotting this comment we’ve reduced chrome window size from 1920,1200 to 1440,900 and the problem is no longer present.

The root reason is unknown, but most likely it is at least partially related to running out of memory (reference). Most of recommendations when using docker in this scenario, was to increase shm-size, by providing --shm-size=2g to docker run. That was not an option for us though…

Hope it helps in case you run into similar situation.

Difference between <<~ and <<-

If you use heredoc in ruby you can choose one of two syntaxes the first <<- it takes all whitespace before stared of text so

def custom_html  
  <<-HTML
    <html>
      <body>
      </body>
    </html>
  HTML
end    

so if you will get string with all withspaces " <html>\n <body>\n </body>\n </html>\n" second <<~ will strip it so

def custom_html  
  <<~HTML
    <html>
      <body>
      </body>
    </html>
  HTML
end

will give you will get text "<html>\n <body>\n </body>\n</html>\n"

Capybara::Selenium

When you not only use Capybara for rspec, you would like to change the wait time to visit the page, so I think it can be useful.

   Capybara.register_driver :selenium do |app|
      client = Selenium::WebDriver::Remote::Http::Default.new
      client.read_timeout = 20
      Capybara::Selenium::Driver.new(app, browser: :chrome,  http_client: client)
    end