Building a CLI Gem in Ruby was the hardest thing I’ve ever had to code. I tried following the instructions from several videos (watching them all twice just to make sure I could understand them), took notes and studied the github repositories of other’s cli gems.
But I wasn’t getting anywhere after several weeks. I tried to set up another repository after my first attempt failed. Usually with projects I find it helps to have more than one version. The first is my practice run, the second is usually my finished product. But this failed miserably.
I tried going back to basics and just kept it simple. Just remember you build methods, connect them and call them. If you can remember when to connect them and when to call them it will save you time, effort and a headache. It might even be fun!
If I had to do this over again, here are the steps I would use to be more systematic in my approach:
1. Set Up the Gem
The CLI Gem I created is called ‘Makeup-Sales-CLI-Gem.’ It lets you see the sales going on at Ulta Beauty’s website. First, set up a new Github repository. Watch this video to help you set up your project in the IDE. Now that the Github repository is set up you need to make sure your folders and files are set up. I named the gem ‘Makeup-Sales-CLI-Gem’. Within that gem I had three folders: bin, config, and makeup-sales. Setting up the gem should be straightforward but unless you put the right gems (e.g. nokogiri) in the right files, you’ll continue to run into errors.
The makeup.gemspec file should have these dependecies with the updated versions:
spec.add_development_dependency “bundler”, “~> 2.0” spec.add_development_dependency “rake”, “~> 10.0” spec.add_development_dependency “rspec” spec.add_development_dependency “pry”
spec.add_dependency “nokogiri”
Now you should be ready to modify your README file which tells the user what the gem is all about and how to use it.
2. Set up the README file
Give a short description on what your gem does when it’s run and installation instructions for the user to follow to run the gem. Like this:
Makeup Sales
Welcome to the Makeup Sales CLI Gem!
Follow the installation instructions below to see the sales going on at Ulta Beauty’s website.
Installation
Add this line to your application’s Gemfile:
gem 'makeup-sales-cli-gem'
Install nokogiri
gem install nokogiri
And then execute:
$ bundle
Or install it yourself as:
$ gem install makeup-sales-cli-gem
Usage
Type the below and follow the screen prompts
$ ./bin/makeup-sales
Requiring nokogiri and installing it are two different things. I found this out the hard way. I had required nokogiri but didn’t know I was also supposed to install it in the IDE until I read about it in another blog post. It’s the little things that matter most when it comes to coding so be sure to pay attention and don’t be afraid to go looking for answers on your own and ask for help if the CLI isn’t running correctly after several attempts and you don’t understand the errors.
To install nokogiri type this in the command line:
gem install nokogiri
Now I’ll move on to set up the Bin and Config Folders
4. Set up the Bin Folder and the Config Folder
The great thing about Ruby programming is everything is connected to everything else. It’s like playing dot to dot. I need to create a makeup-sales file in the bin folder to run the gem.
#!/usr/bin/env ruby # the shebang line tells the program loader what command to use to execute the file
require_relative '../lib/makeup-sales' # calling the makeup-sales file
MakeupSales::CLI.new.call # callling the cli, new method creates a new call class method allowing the gem to run it's # methods and allowing the user to see the gem running on the command line
The config folder contains the environment.rb file which takes the links of all the files and puts them in one place to keep them all connected.
require 'nokogiri'
require 'open-uri'
require 'pry'
require_relative '../lib/makeup-sales/scraper'
require_relative '../lib/makeup-sales/product'
require_relative '../lib/makeup-sales/cli'
require_relative '../lib/makeup-sales/version'
5. Set up the Lib folder
The Lib folder is where I’ll build the gems methods to get it running. I’ll need a makeup-sales.rb file to hold the module ‘Makeup-sales’.
module MakeupSales # connects the files in the MakeupSales folder
end
require_relative '../config/environment' # compiles the files together with the dependencies allowing the gem to # function
6. Makeup-Sales file - CLI, Product, Scraper, Version
Time to create the gem itself. All the code for this gem to run will go in a folder called ‘Makeup-Sales’ which consists of four files: CLI.rb, Product.rb, Scraper.rb, Version.rb. The .rb is added to tell the IDE you’re writing in Ruby. CLI.rb file:
class MakeupSales::CLI #MakeupSales is the module, CLI is the class
def call
MakeupSales::Scraper.scrape_product # Calling the Scraper method
puts "\nWelcome to the Makeup Sales of Ulta Beauty:\n" # This is what the User sees
main_menu # calling the main_menu method
end
def main_menu # this gives the user the option to view the products
puts "Type 'list' to see a list of the products."
puts "Or type 'quit' to leave."
input = gets.strip.downcase # this method allows the user to choose what input they want
case(input) # this case statement has the argument of input; the outcome is determined by the user's input
when 'list'
product_list # product_list is called from below when the user types 'list'
when 'quit'
goodbye # calls the goodbye method
else
puts "Invalid Entry" # anything the user types other than 'list' or 'quit'
main_menu # loops back to the beginning
end
end
def product_list
puts "Here are the beauty products on sale today:\n"
MakeupSales::Product.all.each.with_index(1) do |product, idx| # shows the products in list form each with index
puts "-------------------------------------------------" # MakeupSales::Product.all calls the products you've
puts "#{idx}. #{product.brand}" # pushed into the @@all array from the Product class
puts "#{product.description}" # this method uses interpolation to call the scraped data
puts "Sale Price:#{product.sale_price}" # from the MakeupSales::Scraper class
puts "Original Price:#{product.previous_price}"
puts "-------------------------------------------------"
end
puts "\nSelect a number for the product you want more info about."
input = gets.strip.to_i - 1 #index value 0-98
max_input = MakeupSales::Product.all.size - 1 # max_input puts a stop on how many numbers the user can correctly until input.between?(0,max_input) # input, in this case 1 - 98 is allowed
puts "Sorry, please enter a number between 1 and #{max_input + 1}"
input = gets.strip.to_i - 1
end
product_object = MakeupSales::Product.all[input] # creating a variable from Product class and calling @@all array
MakeupSales::Scraper.scrape_product_details(product_object) # calling Scraper class
puts product_object.more_info # calling this method from Scraper class
next_product # giving the user the option to return to the main menu
end
def next_product
puts "\nWould you like to see another product? Y or N?"
answer = gets.strip.downcase
if answer == 'y'
puts product_list
elsif answer == 'n'
puts goodbye
else
puts "invalid entry"
next_product
end
end
def goodbye
puts "Thank you for shopping with us! See you soon!"
end
end
Product.rb:
class MakeupSales::Product
attr_accessor :brand, :sale_price, :previous_price, :description, :url, :more_info
@@all = [] # all products are in this array
def initialize(brand)
@brand = brand
@@all << self # pushing the products into the @@all array
end
def self.all
@@all
end
end
Scraper.rb file:
class MakeupSales::Scraper
def self.scrape_product
website = Nokogiri::HTML(open("https://www.ulta.com/promotion/sale")) #Scaping from this url
section = website.css("ul#foo16") # the unordered list which contains the products
products = section.css("li") # products are contained in the list item or 'li' HTML tags
products.css("div.productQvContainer").each do |product| # iterate over the products in this div container with each
brand = product.css("h4.prod-title").text.strip # created the brand variable to initialize in the Product class
product_object = MakeupSales::Product.new(brand) # create the product_object instance variable
product_object.description = product.css("p.prod-desc").text.strip # scrape the product_object's attributes
product_object.sale_price = product.css("span.pro-new-price").text.strip
product_object.previous_price = product.css("span.pro-old-price").text.strip
product_object.url = "https://www.ulta.com" + product.css("a").attr("href").value # url is a combination of the original # url plus the scraped data of each individual url tags contained in the anchor tags
end
end
def self.scrape_product_details(product_object) # this method gives each individual product a seperate description
website = Nokogiri::HTML(open(product_object.url) # because of the product_object.url variable
product_object.more_info = website.css(".collapse.in").text.strip
end
end
Version.rb file:
module MakeupSales
VERSION = "0.1.0"
end
7. Ask for Help If You Get Stuck
This project took me over a month and I had to go over each basic concept no less than three times and watched the videos twice. It looks simple but keeping it simple is the real challenge. I couldn’t have done it without the help of several teachers from Flatiron School.
Sometimes the answer is simple if you know what to look for but if you don’t it can take you longer than it should. I regret that I didn’t ask for help sooner or more often but I don’t regret the amount of study and work I put into it. Don’t be afraid to review. If you feel like you’re over reviewing, you’re probably doing it right. Don’t get discouraged. Coding is complicated and isn’t meant to be picked up overnight. If you’re frustrated take a break. Go over everything slowly and be sure to take care of yourself.
I hope this helps you in building your CLI Gem. I learn best from documented code explaining what every method’s purpose is and what it’s doing. Here’s the Github link if you want a better idea of what the structure is for a Ruby gem.
Good luck and happy coding!