Joshua Blewitt

Partying with HTTParty in Ruby 💎

##Introduction

Last month, I talked about creating the Video Game API which is hosted on Heroku. This time, I decided to create a CLI application written in Ruby to communicate to it!

You can find the code hosted on GitHub here.

But first let's take a look at how making a HTTP request would look like using Ruby. This is for logging in:

url = URI("https://mighty-cliffs-81365.herokuapp.com//login")

https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true

request = Net::HTTP::Post.new(url)
request["Content-Type"] = "application/json"
request.body = "{\r\n    \"username\": \"Josh\",\r\n    \"password\": \"mypass\"\r\n}"

response = https.request(request)
puts response.read_body

And then for displaying all the games in the database:

url = URI("https://mighty-cliffs-81365.herokuapp.com//displayGames")

https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true

request = Net::HTTP::Get.new(url)
request["Authorization"] = "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJmcmVzaCI6ZmFsc2UsImlhdCI6MTYxNzU1OTE5OSwianRpIjoiYWQ3MDQ3OWEtNWQxOC00MWYxLWI3M2MtZDQ1NGRjMjZkMzFiIiwibmJmIjoxNjE3NTU5MTk5LCJ0eXBlIjoiYWNjZXNzIiwic3ViIjoiSm9zaCIsImV4cCI6MTYxNzU2MDA5OX0.zkQFBFSRnh2eU04D26AUhlWZXjggztZI9_Nri89eOTk"

response = https.request(request)
puts response.read_body

This is already a lot of work and this is just two endpoints! Trying to manage this for five different endpoints is going to be a pain.

What if there was another way?

Thankfully, there is!

There is the Ruby Gem, HTTParty.

HTTParty is described as:

Makes http fun again! Ain't no party like a httparty, because a httparty don't stop.

Let's take a look at the requirements for using this Gem!

  • Ruby 2.0.0 or higher
  • multi_xml
  • You like to party!

###Time to partayyyyyy

There are two ways to approach the task of creating a CLI application with HTTParty. I could either create a variable and use class methods from HTTParty to get the job done or create my own class.

As the Video Game API has multiple endpoints and authorisation via a Bearer token, I decided to create my own class.

As part of the initialise method for the class, I decided it would be good to go ahead and call the login endpoint and store the token in a class variable (as we'll be needing this to actually make requests to other endpoints)

def initialize(username, password)
    @login = {username: username, password: password}.to_json
    #parse response into a hash
    @token = self.class.post('https://mighty-cliffs-81365.herokuapp.com/login', body: @login, :headers => {'Content-Type' => 'application/json'}).parsed_response
    puts "Logged in! Token is: #{@token['access_token']}"
end

I've also included a small message displaying the key once the authentication is done.

The token itself is stored as a hash, as I'm using the built in method from HTTParty called 'parsed_response'.

With the token stored, we can now begin acessing the rest of the API. We'll start with displaying games.

###Displaying games

Alright, let's define another method in the class for displaying games!

def displayGames
    #parse response into a hash
    gamesList = self.class.get("https://mighty-cliffs-81365.herokuapp.com/displayGames", :headers => {'Authorization' => "Bearer #{@token['access_token']}"}).parsed_response
    #check if the hash is empty
    if gamesList.empty?
        puts "There are no games in the database!"
    else
        #print each game
        for game in gamesList['Games'] do
            puts "Name: #{game['Game']['Name: ']}\nGenre: #{game['Game']['Genre: ']}\nPlatform: #{game['Game']['Platform: ']}\nPublisher: #{game['Game']['Publisher: ']}\nYear: #{game['Game']['Year']}\n\n"
        end
    end
end

This is where HTTParty really begins to shine. Remember how I demonstrated above by making a request using the built in Ruby methods? With HTTParty, the response is stored and parsed into a hash on a single line which calls the API. With the response stored as a variable, we can iterate through it and display the games to the user!

I put a small check to ensure that the hash isn't empty, makes the experience better for the user. If the hash is populated, a for loop is used to iterate through each game.

Okay, onto the next endpoint!

###Finding a game

Next up, we have finding a game. This relies on a user entering a serch term and see if there are any matches!

Let's take a look at how this method looks

def findAGame
    puts "Please enter a search term"
    search = gets.chomp
    puts "You have entered #{search}"

    request = {name: search}.to_json

    response = self.class.get("https://mighty-cliffs-81365.herokuapp.com/findGame", :headers => {'Authorization' => "Bearer #{@token['access_token']}", 'Content-Type' => 'application/json'}, body: request)
    #parse response into a hash
    responseHash = response.parsed_response

    #check if the response is empty
    if response.code == 200
        puts "Search results:"
        for game in responseHash['Games'] do
            puts "Name: #{game['Game']['Name: ']}\nGenre: #{game['Game']['Genre: ']}\nPlatform: #{game['Game']['Platform: ']}\nPublisher: #{game['Game']['Publisher: ']}\nYear: #{game['Game']['Year']}\n\n"
        end
    else
        puts "Couldn't find a game with the term of #{search}!"
    end
end

This is where things get slightly more complex with the request (and slightly tripped me up!)

At first, I was having some trouble with getting this request to work. Everytime I sent a request, it bounced back with an error message from the API. But why? Everything seemed okay, the request was in the correct format. So running the API locally and firing the request I soon discovered what was wrong; the content type wasn't in the correct format.

So after reading through the documentation and examples, I discovered that I needed to add the content-type to the request.

At this stage, I knew that if the search term couldn't find anything, it would return a 404. So I decided to store the response (as a hash) in a separate variable and leave the response variable alone so I could access the status code. This status code is used in an if statement to make sure that if the code is 200, then it will iterate through the results in the hash and display them. If not, then a message telling the user that no games could be be found is displayed.

Looking good!

###Updating a game

Next up, we have being able to update something in the database:

def updateGane
    puts "Enter the ID of the game you want to update"
    id = gets.chomp.to_i
    puts "Enter the field you wish to update (i.e. name, genre)"
    field = gets.chomp.downcase
    puts "Enter the value"
    value = gets.chomp
    puts "You have entered...\nID: #{id}\nField #{field}\nValue: #{value}"

    request = {id: id, field: field, value: value}.to_json

    response = self.class.put("https://mighty-cliffs-81365.herokuapp.com/updateGame", :headers => {'Authorization' => "Bearer #{@token['access_token']}", 'Content-Type' => 'application/json'}, body: request)

    if response.code == 200
        puts "Updated the following:\nName: #{response['Game updated:']['Game']['Name: ']}\nGenre: #{response['Game updated:']['Game']['Genre: ']}\nPlatform: #{response['Game updated:']['Game']['Platform: ']}\nPublisher: #{response['Game updated:']['Game']['Publisher: ']}\nYear: #{response['Game updated:']['Game']['Year']}"
    else
        puts "Failed to update game"
    end
end

Similar to finding a game, the response code is used to determine the response from the if statement. If it was 200 then the details of the updated game are returned, else a message saying that the game could not be updated is displayed.

One thing that did trip me up slightly is referring to the field from the response. In my first attempt, I wasn't adding a space after the name of the field - turns out that it was needed as I defined it to have a space, do'h! 🤦‍♂️

Moving on...

###Deleting a game

Next we have the functionality of deleting a game.

Let's take a look!

def deleteGame
    puts "Enter the ID of the game you want to delete"
    id = gets.chomp.to_i

    request = {id:id}.to_json

    #parse response into a hash
    response = self.class.delete("https://mighty-cliffs-81365.herokuapp.com/deleteGame", :headers => {'Authorization' => "Bearer #{@token['access_token']}", 'Content-Type' => 'application/json'}, body: request)
    parsedResponse = response.parsed_response
        
    #check if the response is empty
    if response.code == 200
        puts "The following game has been deleted:\nName: #{parsedResponse['The following has been deleted']['Game']['Name: ']}\nGenre: #{parsedResponse['The following has been deleted']['Game']['Genre: ']}\nPlatform: #{parsedResponse['The following has been deleted']['Game']['Platform: ']}\nPublisher: #{parsedResponse['The following has been deleted']['Game']['Publisher: ']}\nYear: #{parsedResponse['The following has been deleted']['Game']['Year']}"
    else
        puts "No game was found with the ID of #{id}!"
    end
end

This is very similar to updating a game; if there is a game that can be deleted - return the details of the deleted game. If not, return a message that no game of the specified ID could be found.

Next up, we have the last endpoint!

###Add a game

Finally, we have the functionality of adding a game to the database.

Let's take a look!

def addGame
    puts "Enter the name of the game you want to add"
    name = gets.chomp
    puts "Enter the platform"
    platform = gets.chomp
    puts "Enter the publisher"
    publisher = gets.chomp
    puts "Enter the genre"
    genre = gets.chomp
    puts "Enter the year of release"
    year = gets.chomp.to_i

    request = {name: name, platform: platform, publisher: publisher, genre:genre, year:year}.to_json

    response = self.class.post("https://mighty-cliffs-81365.herokuapp.com/addGame", :headers => {'Authorization' => "Bearer #{@token['access_token']}", 'Content-Type' => 'application/json'}, body: request)

    if response.code == 201
        puts "Game added!"
    else
        puts "Failed to add a game to the database!"
    end
end

One of the longer methods for this class, mostly because it asks for a lot of input from the user.

Once we've built the request, we send a post request!

If the response is a 201, then a message is displayed that the game was added to the database. Else, a message is displayed that the game could not be added to the database.

###The main method

This is a CLI so there needs to be a way for the user to interact with it! This is where the main method comes into play.

Let's take a look!

def main(videoGameApi)
    while true
        puts "Welcome to the Video Game API CLI tool in Ruby! Please choose an option!"
        puts "1) Display Games\n2) Find a game\n3) Update a game\n4) Delete a game\n5) Add a game\n6) Quit"
        choice = gets.chomp.to_i

        case choice
        when 1
            videoGameApi.displayGames
        when 2
            videoGameApi.findAGame
        when 3
            videoGameApi.updateGane
        when 4
            videoGameApi.deleteGame
        when 5
            videoGameApi.addGame
        when 6
            puts "Goodbye!"
            break
        else
            puts "I don't understand that! Please try again!"
        end
    end
end

Probably not the most exciting method ever but it gets the job done by using a case statement in a while loop.

And now to run the program!

puts "Now logging in, please wait...."
puts "NOTE: This application is hosted on Heroku. If this is the first call, it might take some time to retrieve a response."
#create object
videoGameApi = VideoGameAPI.new("Josh", "mypass")
#run main
main(videoGameApi)

And that's it! We have our CLI tool to communicate with the Video Game API in Ruby using the HTTParty Gem!

As always, be sure to check out the code on GitHub! I've included a Gemfile for the HTTParty Gem so you can use Bundler against it.

Thanks for reading! 👏

Tags:


A photo of me!

I'm Joshua Blewitt, I'm passionate about product, a technology advocate, customer champion, curious mind and writer. I've worked for companies such as Rightmove, Domino's Pizza and IQVIA.

Let me know your thoughts!
More about me