So what’s up with Brew Finder?
TL;DR? Check out the video of Brew Finder in action:
I just hit a big milestone, the first final project for a language module in my Flatiron School journey! We were tasked with creating our own Ruby gem that would use a command line interface (CLI) to allow a user to interact with the program and access data (either from an API or scraped from a website).
This was the first time I was asked to create something of my own from scratch—until this point, we had been coding in test-driven labs. It was daunting to think about building something with no further guidance than the basic project requirements, so I decided to K.I.S.S. (keep it simple, stupid). Instead of coming up with a program and then finding the website or API that would allow the program to do what I wanted, I decided to start with finding an API that I knew I could work with and would be relatively simple. I was looking for free, easy access without a key if possible, and simple output. I found the perfect API in the Open Brewery DB: it has a number of easy calls to get different arrays of results, and the results are always an array of hashes and no further nested data structure beyond the tag list array. Sweet.
Now I had an API that would be easy to work with, I wanted to build the simplest possible CLI that would meet the requirements of the project. I wanted a program that would:
- Welcome user and ask for zip
- Return a list of breweries from that zip
- Ask user to pick a number from the list
- Display details about the selected list item
- Give the user the option to pick another from list, search again, or exit
The Coding Process
So now I had an API and knew the basic functionality of my program… and I had no idea how to start! The blank page is scary, yo.
I reviewed the project requirements once again, and found a super helpful resource on the information page: a video of Flatiron School founder, Avi Flombaum, talking through the process of building a CLI gem (called Daily Deal) from the ground up. I was able to follow Avi’s process and get my own project up and running relatively easily:
- Imagine the gem: what is the user experience?
- Start with project structure (i.e., files): Google the structure and use tools like Bundler to make set up a breeze.
- Start with entry point: how does the user execute the program?
- Force the run file to create the CLI (and subsequently every other object).
- Stub out interface: make fake objects to get things working together.
- Make things real: start using real data to make objects.
- Discover objects: Each object does one thing, each method has one function.
- Red, Green, Refactor: Get something working, then break it and make it better.
I had already done step 1 by finding my API and sketching out the basic funtion of my CLI, so next was setting up the file structure. Bundler is fantastic for setting up the basic gem structure and providing the environment file, version, etc. I did a little renaming and restructuring to make it look the way I wanted, but Bundler did most of the heavy lifting. On to step 3, how the program is executed in the bin folder:
#!/usr/bin/env ruby
require "bundler/setup"
require "env"
BrewFinder::CLI.new.call
I hadn’t made the CLI class yet, or the call method, but I knew I would need to set Ruby as the environment, require the Bundler set up and my environment folder, and I wanted the program to run as soon as a new instance of the CLI class was instantiated. By instantiating the CLI and the .call
method, I was “forcing the run file to create the CLI.”
Next I was just creating basic objects and trying to get them to play nicely together. I made a CLI that put out simple messages and made a Brewery class and populated it with objects that were hard coded. Once I got the CLI working with the Brewery class, I was ready to use the API to create real Brewery objects. My Brewery and API objects were pretty simple and easy to get functioning properly. For the Brewery object, I gave it all the attributes from the Open Brewery DB results, used a hash and the .send
method to assign the key-value pairs to the attributes, made a display details method, and methods to access all Brewery objects and clear all Brewery objects.
class BrewFinder::Brewery
attr_accessor :id, :name, :street, :brewery_type, :city, :state, :postal_code, :country, :longitude, :latitude, :phone, :website_url, :updated_at, :tag_list
@@all = []
def initialize(hash)
hash.each {|k, v| self.send("#{k}=", v)}
@@all << self
end
def self.display_details(index)
b = self.all[index]
puts "---------------"
puts "#{b.name} is located at #{b.street} in #{b.city}, #{b.state}."
puts "Here are some details about #{b.name}:"
puts "Type: #{b.brewery_type}"
puts "Phone: #{b.phone}"
puts "Website: #{b.website_url}"
puts "---------------"
end
def self.all
@@all
end
def self.destroy_all
@@all.clear
end
end
For the API, I just needed to call for breweries by zip code and use that returned array of hashes to instantiate new Brewery objects. The method takes a zip code as an argument and interpolates the zip code into the query to Open Brewery DB.
class BrewFinder::API
def self.breweries_zip(zip)
breweries = HTTParty.get("https://api.openbrewerydb.org/breweries?by_postal=#{zip}")
breweries.each {|brewery_hash| BrewFinder::Brewery.new(brewery_hash)}
end
end
The CLI was where the magic happens. I got it working with just the zip code really quickly, but then I need to spend some time “discovering objects” and methods. I got it to the point where I felt each object and method did one job, but I still wanted more so I decided to expand the scope of my project.
Shining it up
Once I had the zip code search working (meaning I was able to start the program, run the search, look at results, and search another zip code without exiting the program), I was getting some momentum in my coding and wanted to add some extra bells and whistles. I added a search by state method to the API class:
def self.breweries_state(state)
breweries = HTTParty.get("https://api.openbrewerydb.org/breweries?per_page=50&by_state=#{state}")
breweries.each {|brewery_hash| BrewFinder::Brewery.new(brewery_hash)}
end
The Brewery object didn’t need any changes, it was still doing its job of instantiating objects, keeping track of the collection and clearing the collection between searches thanks to the CLI calling on the .destroy_all
method.
I edited the CLI to take either zip or state searches, display results differently depending on the type of search (display the street for zip searches and the city for state searches), and be able to handle invalid input and loop users back to the search function. I changed the placement of while
loops and phrasing of prompts to make sense in the various scenarios where they might appear and ran my program dozens and dozens of times trying every combination of valid and invalid entries I could think of. I was in the Red, Green, Refactor step and it was actually really fun! Finally I added some color using the colorize gem and ASCII art that I got from this cool site and then edited the art to make it my own by changing the handle, letters on the mug, and added color.
class BrewFinder::CLI
def call
welcome
search_breweries
brewery_details
end
def welcome
puts "Welcome to Brew Finder! Let's find you some brews near you.".colorize(:yellow)
puts
puts " ~ ~"
puts " ( o )o)"
puts " ( o )o )o)"
puts " (o( ~~~~~~~~o "
puts " ( )'" + " ~~~~~~~' ".colorize(:yellow)
puts " ( )|) " + " |-.__.. ".colorize(:yellow)
puts " o" + "| _ |-__ | ".colorize(:yellow)
puts " o" + "| |_) | | | ".colorize(:yellow)
puts " | |_) | | | ".colorize(:yellow)
puts " o" + "| | / / ".colorize(:yellow)
puts " | | / / ".colorize(:yellow)
puts " | |- ' ".colorize(:yellow)
puts " .========. ".colorize(:yellow)
puts ""
end
def search_breweries
puts "Would you like to see a selection of 50 breweries in your state,".colorize(:yellow)
puts "or all the breweries in your zip code? Please type 'zip' or 'state'.".colorize(:yellow)
choice = gets.strip.downcase
if choice == "zip"
puts "Please enter your 5-digit zipcode.".colorize(:yellow)
input = gets.strip.to_i
BrewFinder::API.breweries_zip(input)
display_zip
elsif choice == "state"
puts "Please enter the full, unabbreviated name of your state.".colorize(:yellow)
input = gets.strip.downcase.gsub(" ","_")
BrewFinder::API.breweries_state(input)
display_state
else
puts "I'm sorry, that is not a valid choice.".colorize(:yellow)
end
end
def display_zip
puts ""
BrewFinder::Brewery.all.each.with_index(1) {|b, i| puts "#{i})".colorize(:yellow) + " #{b.name} - #{b.street} - #{b.brewery_type}"}
puts ""
puts "Which brewery would you like to learn about? Please enter a number.".colorize(:yellow)
end
def display_state
puts ""
BrewFinder::Brewery.all.each.with_index(1) {|b, i| puts "#{i})".colorize(:yellow) + " #{b.name} - #{b.city} - #{b.brewery_type}"}
puts ""
puts "Which brewery would you like to learn about? Please enter a number.".colorize(:yellow)
end
def brewery_details
input = nil
while input != "exit"
puts "You can type 'start over' to search again or 'exit'.".colorize(:yellow)
input = gets.strip.downcase
if input == "exit"
goodbye
elsif input.to_i > 0 && input.to_i <= BrewFinder::Brewery.all.length
BrewFinder::Brewery.display_details(input.to_i-1)
puts "Pick a new number from the list to learn about another brewery.".colorize(:yellow)
elsif input == "start over"
BrewFinder::Brewery.destroy_all
search_breweries
else
puts "Not sure what you meant... Please pick a number from the list.".colorize(:yellow)
end
end
end
def goodbye
BrewFinder::Brewery.destroy_all
puts "Goodbye, and thanks for checking out Brew Finder!".colorize(:yellow)
end
end
My beer mug in all its colorized glory:
Lessons Learned
What surprised me the most about this project was how much FUN I had! Up until this project the coding labs had been rewarding, but I wouldn’t call them “fun.” With this project, I couldn’t get away from my computer—I’d tell myself I was done for the night, then 30 minutes later I’d be back trying to smooth out some wrinkles, hunting for bugs, looking for the DRY-est code I could get. As a relative newb in coding I’m sure there’s room for improvement, but I’m really proud of my work on this project and think it does a pretty good job of following best practices (I was tempted to use GOTO
when a loop was about to break my mind, but refrained and refactored…).
I also learned a lot more about how Ruby functions within the larger landscape of technology, rather than just learning the syntax and methods. By creating the file structure and environment, requiring relative files, installing gems, etc I feel like I have a much more holistic understanding of programming than I had been getting from labs up to this point.
I feel renewed energy in my learning journey, and even better I feel prepared and interested in starting side projects of my own to work on outside of Flatiron curriculum—I’m thinking I’ll try another CLI gem but using scraped data rather than an API.
And lastly, check out my gem on GitHub!