Taco Town
My first project with the Flatiron school was to design a CLI app (Command Line Interface) that pulled data from any API (Application Programming Interface) available online. I decided my program would be about tacos
The API
I chose an API called Taco Fancy which hosts a collection of various taco ingredients, sorted by type, each with a recipe and a name. In order to access that data, I first needed to write a program that sends a GET request to the hosting website.
api.rb
I wrote a class Api
that used the httparty
gem to send a get request to the data endpoint. The method get_request
utilizes string interpolation to dynamically generate a URL depending on an input argument, with the default arg set to "random"
.
ENDPOINT = "http://taco-randomizer.herokuapp.com/" def get_request(query="random")
url = "#{ENDPOINT}" + "#{query}/"
uri = URI.parse(url)
response = Net::HTTP.get(uri)
end
This method, however, only returns a literal string from the database. Oh no!
Api.new.get_request
=> "{\"base_layer\": {\"url\": \"https://raw.github.com/sinker/tacofancy/master/base_layers/moroccan_lamb.md\", \"slug\": \"moroccan_lamb\", \"name\": \"Moroccan Lamb\", \"recipe\": \"Moroccan Lamb\\n=============\\n\\nA Differently Spiced Meat Than Your Usual ...(etc)
In order to manipulate this data, I need to utilize a different gem, json
. This allows the computer to parse the JavaScript Object Notation and return a usable data structure. If I call my previous get_request
method inside of the JSON
object and allow it to accept that method's input as an argument I could both get and parse the requisite data all in one line. Pretty handy!
I also defined a get_random
method that is functionally identical but doesn't accept an argument, causing get_request
to use the default query of "random"
. I felt this was more descriptive than calling search_by_type()
with no argument whenever I wanted a random taco.
def search_by_type(slug=nil)
JSON.parse(get_request(slug + "s"))
enddef get_random
JSON.parse(get_request)
end
And our new return:
=> {"base_layer"=>
{"url"=> "https://raw.github.com/sinker/tacofancy/master/base_layers/rajas_poblanas.md",
"slug"=>"taco_de_rajas_poblanas",
"name"=>"Taco de rajas poblanas",
"recipe"=>
"Taco de rajas poblanas\n======================\n\n* Bunch of poblano peppers\n* Onion\n* Tad of oil\n* Mex...(etc)
A hash! Finally, a data structure we can manipulate!
Ingredients and Tacos
I wanted my app to let you browse the various ingredients and select the ones you liked to save as a taco. This was accomplished by creating an Ingredient
class where each instance stored the info of each ingredient, and a Taco
class that had a recipe stored as an array of many ingredients.
taco.rb
The Taco class was relatively straight forward, with a few instance methods such as .recipe
(returns a Hash with keys that correspond to the taco “layer” and values that were Ingredient
instances), .save
(saved the current Taco
by shoving it into an array saved as a class method), .full?
(checks if the every layer has an assigned ingredient), .add_ingredient()
(shoves a new ingredient to the @ingredients
array), etc
def add_ingredient(ingredient)
@ingredients << ingredient unless @ingredients.include(ingredient)
enddef recipe Hash.new.tap do |recipe|
@ingredients.each {|ingredient| recipe[ingredient.type] = ingredient.name} end
end
ingredient.rb
The Ingredient
class was fun as I got to use some metaprogramming to dynamically create attributes and assign values to them using the Hash returned by .search_by_type()
.
def self.load
Api.new.tap do |api|
api.get_random.each_key do |type|
api.search_by_type(type).each do |ingredient|
Ingredient.new(type: type, ingredient: ingredient)
end
end
end
enddef initialize(type:, ingredient:)
@type = type
ingredient.each do |key, value|
self.class.attr_accessor(key)
self.send("#{key}=", value)
end
@@all << self
end
Now I can call the class method Ingredient.load
to query the api and instantiate a new Ingredient
instance for each “type” of ingredient (which layer it belongs to). Using metaprogramming, that instance will also assign itself all the corresponding values and labels it needs to function. Hurray!
The CLI
The CLI was a bit challenging, as there are a lot of conditions and inputs required for the user to navigate between different types of ingredients, view those ingredients’ recipes, add an ingredient to a taco, browse tacos, etc.
I decided to write helper methods for printing the prompt and getting the user input, using an until
to loop until the input was valid. For example:
def create_taco
input = create_taco_input
if input == Ingredient.types.count + 1
welcome
else
choose_ingredient(Ingredient.types[input-1])
end
enddef create_taco_prompt
clear
puts "Please choose an ingredient type:".bold
Ingredient.types.each_with_index {|type, index| puts "#{index + 1}. #{type.split("_").map{|word| word.capitalize}.join(" ")}"} puts "#{Ingredient.types.count + 1}. Main Menu\n\n"
enddef create_taco_input_loop
input = get_int
until input && input > 0 && input <= Ingredient.types.count + 1
invalid_input
create_taco_prompt
input = get_int
end
input
end
From there on out, I just needed to fill out the possible pathways a user could follow and make sure I didn’t leave any hanging parenthesis.
Welcome to Taco Town
It was a lot of fun writing this code and I definitely learned a lot in the process. I made many mistakes along the way and with each one came an opportunity to deepen my understanding of object oriented programming. I’m definitely looking forward to seeing what future programming challenges are on the horizon.
If you’re feeling hungry after all this taco talk, please feel free to swing by Taco Town and see what recipes lie in wait. Thanks!