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