Brewing My First Ruby CLI Gem

Alicia Santiago
5 min readMar 16, 2019

A short guide to how I built and published a Ruby CLI Gem using Bundler and RubyGems.org

Like the beginning of any project, staring at a blank slate was by far the scariest part. First, I had to decide what I wanted my gem to be able to do, so I asked myself what I would find fun or helpful, or both. Beer is fun. Knowing which beers I absolutely must try is helpful. How about a gem that can access Beeradvocate’s top rated new beers? Yes, please!

Photo by Lance Anderson on Unsplash

Once I had my concept, I did a quick Google search for ways to create a Ruby gem. One of the top hits was Bundler, which generates all the files and dependencies I needed to get started (Bundler’s detailed read-along guide was easy to follow and really simplified what would otherwise have been a daunting process). After checking to make sure my version of bundler was up to date by running `bundle -v`, I was able to create my scaffold directory by running `bundle gem tap_rated_new_beers` (for recommendations on naming your gem, visit this guide at RubyGems.org). This step alone boosted my confidence because I now had the basic structure of my gem. Next step, set up my files!

I wanted make sure I followed the Single Responsibility Principle (SRP), so I created four files in which to write my code:

  1. lib/tap_rated_new_beers.rb (This serves as my environment.)
  2. lib/tap_rated_new_beers/scraper.rb (This file contains all the code that scrapes information from Beeradvocate’s Top Rated Beer: New page.)
  3. lib/tap_rated_new_beers/cli.rb (This file contains all the code that makes my CLI work.)
  4. lib/tap_rated_new_beers/beer.rb (this file contains all the code that instantiates each beer and it’s attributes. It also contains my open_in_browser method, which enables the user to directly access a beer’s brewery from their terminal.)

Before going much further, I wanted to make sure that my environment was set up correctly, so I added some “fake” code to my lib/tap_rated_new_beers files and ran my executable file in my terminal. My executable file, bin/tap_rated_new_beers, contains the following:

#/usr/bin/env rubyrequire 'pry'
require 'nokogiri'
require 'open-uri'
require 'colorize'

require_relative 'tap_rated_new_beers/version.rb'
require_relative 'tap_rated_new_beers/cli.rb'
require_relative 'tap_rated_new_beers/scraper.rb'
require_relative 'tap_rated_new_beers/beer.rb'

Success! My files and classes were able to communicate with each other. Now I could start brewing my TapRatedNewBeers::Scraper class.

Photo by Daniel Vogel on Unsplash

Building my scraper methods ended up being more of a challenge than I was expecting. I knew that I wanted to provide my gem user with two levels of information:

  1. A list of the top rated new beers (by rank).
  2. Each beer’s score, number of ratings, style, ABV, availability, description (if provided), and the brewery’s name, location and website url.

This meant that I needed to create two scraper methods — one that would get the rank and name of each beer, and one that would get each beer’s unique information. I was able to scrape my first level of information by isolating the table that contained the rows I wanted to access, then I iterated through each row to get the rank and name of each beer.

  def self.scrape_index_page
table_row_nodes = self.index_url.css("table").css("tr")
table_row_nodes = table_row_nodes.slice(2, 50)

table_row_nodes.each do |beer_row|
rank = beer_row.css("td")[0].text
name = beer_row.css("td")[1].css("a").first.text
beer_url = beer_row.css("td")[1].css("a").first.attributes["href"].value

temp_beer = TapRatedNewBeers::Beer.new(name)
temp_beer.rank = rank
temp_beer.beer_url = "https://www.beeradvocate.com"+ beer_url
end
end

My second scraper method needed to utilize the the beer_url for each beer to access the individual beer’s attributes. Most attributes were easy to access, but I had to apply some enumerable methods to isolate the ABV, availability and notes/description.

def self.scrape_beer_page(beer)
beer_page = Nokogiri::HTML(open(beer.beer_url))

beer.score = beer_page.css("div#score_box").css("span.BAscore_big").css("span.ba-ravg").text
beer.ratings = beer_page.css("div#score_box").css("span.ba-ratings").text
beer.style = beer_page.css("div#info_box.break").css("a")[4].text
beer.brewery = beer_page.css("div#info_box.break").css("a")[0].text
beer.location = beer_page.css("div#info_box.break").css("a")[1].text + ", " + beer_page.css("div#info_box.break").css("a")[2].text
beer.brewery_url = beer_page.css("div#info_box.break").css("a")[3].attributes["href"].value
array = beer_page.css("div#info_box.break").text.split("\n\n")
array.find do |phrase|
if phrase.include?("%")
beer.abv = phrase
elsif phrase.include?("Availability")
beer.availability = phrase
end
end
beer.notes = array.last
end
end

After scraping all the information I needed, I started building out my TapRatedNewBeers::Beer class. I created an attr_accessor for each attribute and a class variable @@all set equal to an array in which to store each beer instance that could then be shared between classes. I also added the open_in_browser method that could be called in my TapRatedNewBeers::CLI class and enable the user to access each brewery’s website (I love this feature!).

class TapRatedNewBeers::Beer
attr_accessor :name, :rank, :beer_url, :brewery, :style, :abv, :ratings, :score, :location, :brewery_url, :availability, :notes

@@all = []

def initialize(name = nil)
@name = name
@@all << self
end

def self.all
@@all
end

def open_in_browser
system("open '#{brewery_url}'")
end
end

Last but not least, I need to build the working file of my gem: TapRatedNewBeers::CLI. This final step took the longest BY FAR. I think the most challenging method to get working was select_beer. I needed to be able to account for invalid user input, and that meant converting the rank associated with each beer from a string to an integer so I could then define valid input as being 1 to 50 (or the length of the array containing all the beers, in case I decide to change the number of beers provided in the future). Thankfully, I got some help figuring this out from my amazing cohort at the Flatiron School.

def select_beer
puts "Select the rank number of the beer you'd like to sample:".red.bold
input = gets.strip
if input.to_i.between?(1, TapRatedNewBeers::Beer.all.length)

beer = TapRatedNewBeers::Beer.all.find do |beer|
beer.rank == input
end

TapRatedNewBeers::Scraper.scrape_beer_page(beer)
print_beer_info(beer)

else
puts ""
puts "Have you been drinking? Please try again.".red.bold
puts ""
display_list
end
end

After many, many trials and errors, I finally had a user-friendly, error-free gem! All I had left to do was publish it and crack open a beer (selected off my tap_rated_new_beer list, of course). I created a RubyGems account and followed the steps in the Publishing Your Gem guide

tap_rated_new_beers 🔥 gem push pkg/tap_rated_new_beers-0.1.0.gem

Pushing gem to
https://rubygems.org...
Successfully registered gem: tap_rated_new_beers (0.1.0)

…and before I knew it I had 30 downloads! That alone makes the past week’s efforts well worth it.

You can sample my Ruby CLI Gem by typing the following prompts in your terminal:

gem install tap_rated_new_beersbin/tap_rated_new_beers

Happy coding/drinking!

Photo by Yutacar on Unsplash

--

--