Blog:

Five Small Hacks for your Ruby Projects

Avatar
Lucas Caton
May 26, 2018

Five Small Hacks for your Ruby Projects

Here are a few small hacks I have discovered over the years to streamline your Ruby projects:

1. Valid Date Method

Many projects require a method that takes a date as a parameter and returns the date if it is valid and false if it is not. This method should also not raise an exception. Regex is the obvious go to but that is not a great idea. Here is an example of the sort of code you would need to write to check a date:

ruby def valid_date(potential_date) potential_date.match?( /\A(?:(?:(?:(?:0?[13578])|(1[02]))/31/(19|20)? \d\d)|(?:(?:(?:0?[13-9])|(?:1[0-2]))/(?:29|30)/(?: 19|20)?\d\d)|(?:0?2/29/(?:19|20)(?:(?:[02468] [048])|(?:[13579][26])))|(?:(?:(?:0?[1-9])|(?: 1[0-2]))/(?:(?:0?[1-9])|(?:1\d)|(?:2[0-8]))/(?:19| 20)?\d\d))\Z/ ) ? potential_date : false end

Once upon a time, I would have used the Date.parse method to validate dates. However, I soon learned that it comes with a number of gotchas, as illustrated below:

ruby def valid_date(potential_date) Date.parse(potential_date) rescue ArgumentError false end

The examples below behave as expected:

sh puts valid_date("2018-05-04") #=> <Date: 2018-05-04> puts valid_date("something else") #=> false

However, the following example can catch you out:

```sh

Date.parse “hel10 world” #=> #<Date: 2018-05-10> ```

This is because Ruby sees the 10 and assumes that you mean the tenth day of the current month and year. Go figure!

You really want to pass in a format, but there are many valid date formats. This can be achieved by providing a list of formats using ||=. As we loop through each format, Ruby may raise an exception if the format is not a match. We need to capture that exception and find a way to resume the code. This is achieved by the following method:

ruby def valid_date(potential_date) formats ||= ['%Y/%m/%d', '%d/%m/%Y', '%m/%d', '%B %d', '%b %d', '%d %b'] format = formats.shift Date.strptime(potential_date, format).tap do |date| confirmation = date.strftime(format) raise ArgumentError if potential_date != confirmation end rescue ArgumentError formats.any? ? retry : false end

2. Using hashes in RSpec to check different variable/value combinations

Often in RSpec, you want to check all different combinations of values for a number of variables, such as in the following example:

ruby RSpec.describe RealStateWorker do context "when active is true and rented is true" do it { expect(something).to eq(something) } end context "when active is true and rented is false" do it { expect(something).to eq(something) } end context "when active is false and rented is true" do it { expect(something).to eq(something) } end context "when active is false and rented is false" do it { expect(something).to eq(something) } end end

Because each variable has two possible values, there are four combinations to test. But, what if we had more variables? This can very quickly grow out of hand. Using hashes is a concise way of managing this:

ruby RSpec.describe RealStateWorker do scenarios = [ { active: true, rented: true, expected_result: :something }, { active: true, rented: true, expected_result: :something_else }, { active: true, rented: false, expected_result: :something }, { active: true, rented: false, expected_result: :something_else }, { active: false, rented: true, expected_result: :something }, { active: false, rented: true, expected_result: :something_else }, { active: false, rented: false, expected_result: :something }, { active: false, rented: false, expected_result: :something_else } ] scenarios.each do |scenario| context "when active is #{scenario[:active]} and rented is #{scenario[:rented]}" do it "is #{expected_result}" do expect(something).to eq(scenarios[:expected_result]) end end end end

3. Manage White Labelling a Website Using a YAML file

White labelling a website can get very messy very quickly. I streamline the process by creating a Brandable concern (see below) and storing the details for each website in a YAML file.

app/controllers/concerns/brandable.rb

ruby module Brandable extend ActiveSupport::Concern included do helper_method :brand end private def brand OpenStruct.new(YAML.load_file("config/brands.yml")[request.url]) end end

Then I include it in the ApplicationController so it will be available throughout the project:

app/controllers/application_controller.rb

ruby class ApplicationController < ActionController::Base include Brandable end

The brand method returns an OpenStruct object from the data you have in config/brands.yml. Here is an example of config/brands.yml: yaml "https://reinteractive.com/": title: "reinteractive" blog_url: "https://reinteractive.com/blog" "https://envisage.io/": title: "Envisage" blog_url: "https://envisage.io/blog_posts"

Within any view, you can call either brand.title or brand.address and it is correctly rendered according to the domain you are accessing:

app/views/pages/index.html.erb

```erb

<%= brand.title %>

<%= link_to ‘Blog’, brand.blog_url %> ```

4. Customise your CI using the massa gem

When it comes to CI, sometimes you simply want to check only Rubocop and RSpec pass, and other times you want to go the whole way and check erblint, brakeman, and rails_best_practices as well. Here is an example using travis.yml:

ruby language: ruby rvm: 2.5.1 sudo: false cache: bundler services: - postgresql addons: chrome: stable postgresql: "9.6" before_install: - export TZ=Australia/Sydney - gem update --system - gem install bundler before_script: - bin/setup script: - bundle exec rubocop && bundle exec erblint app/views/**/*.html{+*,}.erb && bundle exec brakeman -Aqz5 && bundle exec rails_best_practices && RAILS_ENV=test bundle exec rspec

If anyone of these fail, the build won’t pass. To switch the different tools on or off involves modifying the line bundle exec rubocop && bundle exec erblint app/views/**/*.html{+*,}.erb && bundle exec brakeman -Aqz5 && bundle exec rails_best_practices && RAILS_ENV=test bundle exec rspec over and over again. I got tired of having to do this, so I have begun working on a gem to manage this more simply. Using the massa gem, you can define a YAML file that will specify all the tools you want:

ruby rubocop: description: 'Rubocop' command: 'bundle exec rubocop' required: true haml_lint: description: 'HAML lint' command: 'bundle exec haml-lint app/views' required: true brakeman: description: 'Brakeman (security vulnerability scanner)' command: 'bundle exec brakeman -Aqz5' required: true rails_best_practices: description: 'Rails Best Practices' command: 'bundle exec rails_best_practices' required: true i18n-tasks: description: 'I18n translations' command: 'bundle exec i18n-tasks missing' required: false rspec-rails: description: 'RSpec' command: 'RAILS_ENV=test bundle exec rspec' required: true

You can turn your tools on or off as required, by simply changing required: true to required: false in the config/massa.yml file.

5. Testing your gems against different versions of Ruby/Rails

When you are maintaining a gem, it is a good idea to test it against many different versions of Ruby and Rails. I have combined a couple of things that allow me to maintain my gems with ease.

The first part of the solution comes from Travis: a feature called [Build Matrix] (https://docs.travis-ci.com/user/customizing-the-build#Build-Matrix). When it’s set up, it runs specs for each combination of Ruby/Rails/whatever you want to check. If any fails, the build fails.

The second part comes from Thoughtbot: they created an awesome gem called Appraisal (https://github.com/thoughtbot/appraisal), which integrates with Travis Build Matrix. > Appraisal integrates with bundler and rake to test your library against different versions of dependencies in repeatable scenarios called “appraisals.”

The last part is my hack: Instead of writing a static Appraisals file, I created a dynamic one. This is possible because Appraisals is interpreted as a normal Ruby file.

Here is an example of how I do it in the enumerate_it gem:

ruby require 'net/http' require 'json' rails_versions = JSON.parse(Net::HTTP.get(URI('https://rubygems.org/api/v1/versions/rails.json'))) .group_by { |version| version['number'] }.keys.reject { |key| key =~ /rc|racecar|beta|pre/ } %w[3.0 3.1 3.2 4.0 4.1 4.2 5.0 5.1 5.2].each do |version| appraise "rails_#{version}" do current_version = rails_versions .select { |key| key.match(/\A#{version}/) } .sort { |a, b| Gem::Version.new(a) <=> Gem::Version.new(b) } .last gem 'activesupport', "~> #{current_version}" gem 'activerecord', "~> #{current_version}" gem 'sqlite3' end end