How I created my first Ruby Gem

Hi everyone! My name is Gilbert Torchon and I am a code lover. I am currently a student at Flatiron School. If you don’t know what you can look it up here.

As part of their Full-Stack Web Developer curriculum I had to learn Ruby (a server-side script language) and the last part of the Ruby Object Oriented section project involved creating a gem that scraped data from a website. Now, I want to walk you through how I did that.

What is a gem?

Well, first you may ask yourself what is a gem. A gem is nothing more than a piece of code that can be installed as an external library or shared with the world as a package trough the RubyGem platform. If you are more of a javascript person here’s what you want to know:

A Ruby gem is to Ruby what an npm package is for javascript.

Let’s create our own!

There is a couple of ways to do that. But one of the simplest ways is to use Bundler. Bundler is a gem and can help creating gems (it’s weird I know). To do that, assuming you got Ruby installed and the bundler gem installed already with the command

gem install bundler 

you can run the command:

bundle gem hotels_of_haiti

The gem I want to create is called “hotels_of_haiti” but you can put whatever you want after bundle gem . This command will create a tree of files just like this:

Now that we have of all our files created, we can initialize it as a git repository by typing this command (assuming that we are at the root project file in the terminal) : git init . Also, we should create a repository on GitHub with the same name and add copy the link that appear after we click the “Clone or Download” button and paste it as a remote reference to our local repository. We can do that by typing git remote add origin <paste-the-link-here> . Then off course, we should add all the files with git add . plus make a commit with git commit -m "your message" then push it with git push origin master . Now your remote repository is in sync with your local repository and you are good to go.

Why are we doing all of that you may ask? We are doing this so we can fill information in the .gemspec file which looks like this :

If you just generated the file like we just did, you will have to fill the information such as homepage (paste the github repository there), authors, email and so on. The “metadata” section can be commented and additional dependencies can be added at the end of the file. After that you’re good to go! Time for coding!

Coding the actual gem

First, let me talk about what I want the gem to do: The gem will not be some kind of utility libraries to help others in their code but rather a CLI program. This CLI programs scrapes data from a website then displays a list of hotels all over the Republic of Haiti (where I am from :) ). The user can then navigate through this data and get additional details about a specific hotel. Wow! it sounds tough isn’t it? Well, it’s not so much. Those who want to know what is web scraping can look it up here.

First, create a config folder at the root of your project then add a file named environment.rb . This file will store all the projects external dependencies and the paths to our actual code. Paste this code inside of it :

require "pry"
require "nokogiri"
require "open-uri"
require "terminal-table"
require "tty-spinner"
require "hotels_of_haiti/version"
require "hotels_of_haiti/cli"
require "hotels_of_haiti/hotel"
require "hotels_of_haiti/scraper"

The first 5 lines are the external gems that our program use so we need to require them. The last 3 are the actual files will be containing our code. The “version” file has been already created for us by bundle.

Then, we should require our environment inside the main file our program hotels_of_haiti.rb which is located at the root of the lib folder. All code that make our program work should be there.

the first line will install all of your specified gem and the second will require our main program file. Remember the 3 files that we required in our environment? Le’ts create them : They should be named cli.rb , scraper.rb and hotel.rb

Inside the cli.rb let’s put this code inside of it:

class HotelsOfHaiti::CLIdef call
HotelsOfHaiti::Scraper.scrape_all
puts "Welcome to 'Hotels of Haiti'"
launch
end
def list_hotel
fields=["ID",'NAME']
table = Terminal::Table.new :title =>"LIST OF HOTELS",:headings =>fields, :rows => Hotel.hotels_to_array(@range,[:id,:name]), :style => {:width => 80, :padding_left => 3, :border_x => "=", :border_i => "x"}
puts table
end
def launch
loop do
puts "How many hotels do you want to see?(Choose between 1 and # {Hotel.all.count})."
@range=gets.strip.to_i
break if @range!=0 && @range <= Hotel.all.count
end
list_hotels
show_hotel
puts "Do you which to go back to the hotel list?(type yes or no)"
input=gets.strip

if input.downcase=="yes"
launch
else
puts "Good bye!"
end
enddef show_hotel
input=nil
loop do
puts "Which hotel you want to see more detail about(Choose by ID)??"
input=gets.strip.to_i
break if input<=@range
end
found_hotel=Hotel.find_by_id(input-1)
puts "X-------------------------------------------------X"
puts " #{found_hotel.name} "
puts "X-------------------------------------------------X"
# puts "Name: #{found_hotel.name}"
puts "Address: #{found_hotel.address}"
puts "Phone: #{found_hotel.phone}"
puts "E-mail: #{found_hotel.email}"
puts "Website: #{found_hotel.website}"
puts "Pricing: #{found_hotel.pricing}"
end
end

This file contains a class that is reponsible of creating our whole program. the call methods launch the program, the list_hotels method list the hotels and the “show_hotel” method display a specific hotel with more details.

This class makes use of two other class : Hotel and Scraper. Let’s paste this code to our hotel class :

class Hotel  @@all=[]
attr_accessor :name,:address,:phone,:email,:number_of_rooms,:website,:pricing
def initialize(name:,address:nil,phone:nil,email:nil,number_of_rooms:nil,website:nil,pricing:nil)@name=name
@address=address
@phone=phone
@email=email
@number_of_rooms=number_of_rooms
@website=website
@pricing=pricing
@@all << self
enddef self.all
@@all
end
def self.hotels_to_hash
hotel_hashes=[]
@@all.each do |hotel|
hotel_hash={
name: hotel.name,
address:hotel.address,
phone:hotel.phone,
email:hotel.email,
number_of_rooms:hotel.number_of_rooms,
pricing:hotel.pricing
}
hotel_hashes << hotel_hash
end
hotel_hashes
end
def self.hotels_to_array(range=@@all.count,fields=[:name,:address,:phone,:email,:number_of_rooms,:website,:pricing])
hotels_to_array=[]
hotel_to_hashes=self.hotels_to_hash
hotel_to_hashes.each_with_index do |hotel_hash,index|
if index > range - 1
break
end
if(fields.include? :id)
hotel_array=[index+1]
end
fields.each do |field|
if hotel_hash.keys.include? field
hotel_array << hotel_hash[field]
end
end
hotels_to_array << hotel_array
end
hotels_to_arrayenddef self.find_by_id(id)
@@all[id]
end
end

This code will basically instantiate a Hotel instance with all the attributes necessary. It contains a class variable named “all” that keep of track of all instances of the class that are created inside our program. The other methods converts the instances inside the “all” class to different data formats.

Now let’s handle our scraper file. This file will be responsible for taking the data from our website, cleaning it and instantiates our hotel. Here’s the code that does that :

class HotelsOfHaiti::ScraperURL="https://www.haiti-reference.com/pages/plan/geographie-et-tourisme/tourisme-en-haiti/hotels-provinces/"
def self.scrape_all
spinner = TTY::Spinner.new(":spinner Scraping hotels ...")
spinner.auto_spin
docs = Nokogiri::HTML(open(URL))
main_node=docs.css(".entry-content")
# each 'h4' is associated with a 'table' to form a the hotel
hotel_names=main_node.css("h4")
hotel_infos_table=main_node.css("table")
hotel_index=0
max_to_scrape=hotel_names.count
loop do
hotel = {}
name = hotel_names[hotel_index].text
hotel[:name]=name
table=hotel_infos_table[hotel_index].css('tbody tr')
table.each do |row|
#binding.pry
row_cells= row.css('td')

case row_cells[0].css('b').text.downcase.delete(" ")
when "adresse:"
address=row_cells[1].text.gsub("\n","")
hotel[:address]=address
when "e-mail:"
if(row_cells[1].text!="")
hotel[:email]=row_cells[1].text
end
when "téléphone:"
if(row_cells[1].text!="")
hotel[:phone]=row_cells[1].text
end
when "téléphones:"
if(row_cells[1].text!="")
hotel[:phone]=row_cells[1].text
end
when "siteweb:"
if(row_cells[1].text!="")
hotel[:website]=row_cells[1].text
end
when "no.dechambres:"
if(row_cells[1].text!="")
hotel[:number_of_rooms]=row_cells[1].text
end
when "tarifs:"
if(row_cells[1].text!="" || row_cells[1].text != "Non disponible")
hotel[:pricing]=row_cells[1].text
end
end
end

hotel_instance=Hotel.new(hotel)
break if hotel_index == max_to_scrape-1
hotel_index+=1
end
spinner.stop('Done!')
end
end

All that we described are done inside a single method “scrape_all”. This method also makes use of an external libray “tty-spinner” to create a CLI loader before scraping ends.

After that, let’s go ahead and create a file called “hotels_of_haiti” in our bin folder but without the .rb extension. This file will be the real start of our program. Let’s paste this code inside of it:

require “bundler/setup”require “hotels_of_haiti”HotelsOfHaiti::CLI.new.call

Voila! That is all required to make our code work. Now if we want to make use of it like a gem we should firs build it with this command gem build hotels_of_haiti . It will create a file named like hotels_of_haiti-0.1.0.gem . You can install it with the command gem install if you want or push it on RubyGems.

Thank You for reading! The whole project can be found on GitHub at this link. Now if you want to see the gem running you can look at this video :

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store