I Built an OO CLI App that utilizes the Rest Countries API to provide basic information on any Country in the World. Here's How.

I Built an OO CLI App that utilizes the Rest Countries API to provide basic information on any Country in the World. Here's How.

countries nog pic.jpeg

For my first project at FlatIron, I was given the task to build a Ruby CLI application which is meant to be my first portfolio project. The app is supposed to utilize basic programming concepts, object-oriented programming, and work with third-party data by scraping a web page or accessing an API. My application uses the REST COUNTRIES to fetch a list of all the countries and individual country data for the user to interact with. The user can enter a country to get basic information such as capital, population, language, currency, region, and sub-region. The user also has an option to look at a list of all the countries in the world, and from that list, they can select a country that they want to get more information on. To close the program, the user can simply enter 'exit'.

Pre-requisites

My application utilizes these basic concepts:

  • Basic Ruby concepts(blocks, loops, variable, etc_

  • Object-oriented programming in Ruby(classes, attributes, object-relationships, etc)

  • Working with external data(APIs)

Using the REST COUNTRIES API

I choose to work with the REST COUNTRIES API because it seemed like the most interesting to me. Actually, I take that back, the REST COUNTRIES API was actually my second choice. My first choice was the Marvel API. I wanted to use that API to get access to Marvels glorifying library of characters, but I failed at getting access. I got to the point where I was given an API key but there came many restrictions after that. Maybe I'll try it again on another project.

Screen Shot 2020-04-18 at 12.46.13 PM.png

The REST COUNTRIES API was my second because I thought It would be cool to provide users with some information on any country in the world. Unlike the Marvel API, the REST COUNTRIES API requires no need for an API KEY.

Planning

My first initial step to planning out this project was creating a kanban board on my Notion app.

Screen Shot 2020-04-18 at 12.49.14 PM.png

The board was because I could visually see tasks that had were not started, In progress, and completed. Having that visual representation helped me stay organized, but what I later realized was that I also should have started by building a wireframe or a flowchart detailing the users' experience and how each class would come into play. By not starting with a wireframe or flowchart I came ran into some problems when building my objects because I did not have a visual representation of the program would work. I eventually had to ask for some assistance to help me straighten out my processes. Towards the end of the project, I decided to make a flowchart displaying the processes is a user was to enter a country:

CountriesAPP Process Flowchart.png

Allocating Responsibilities

In object-oriented programming, it is important to be aware of what tasks need to be done and then giving those tasks to different classes to execute. I decided to make the following classes based on my observation:

  • A Country class

    • Responsible for initializing instances(objects) of a country with variables and storing that object in a class variable called all.

    • Hold a Class constant variable that stores the names of every country inside an array.

  • An API class

    • Fetch all the information from the API, and only format the names of every country.
    • Fetch individual country data and format it.
  • A CLI class

    • Access the data from the other classes
    • Interact with the user

Build, Test, Learn, Repeat

My project did not require any gems, or I should say that I choose not to use gems for this project. The only files that had to be set before starting my project were the Executable and environment files:

#!/usr/bin/env ruby 


require_relative '../lib/environment'

Cli.new.run
require 'pry'
require 'HTTParty' #allows me to request data from API
require 'json'

require_relative './mycountries/cli'
require_relative './mycountries/api'
require_relative './mycountries/country'

Building the API class was fairly simple since I didn't require an API key. Inside the method class, I built two class methods, API.get_country_list and API.get_country_info. The get country list method calls the COUNTRIES API, sets a variable and gives it a value of the data that was called. It then takes that variable and parses it turning it into JSON syntax. Lastly, the method invokes the collect method on that variable to return an array with only the names of every country in the work. The get country list method takes in an argument, which should be the name of a country. It then calls the API with the country name at the end using string interpolation. It then sets variables and gives it a value of the parsed data. Lastly, it instantiates a Country instance and matches its values from the parsed data.

class Api
    def self.get_country_list
        url = "https://restcountries.eu/rest/v2/all"
        response = Net::HTTP.get_response(URI(url)).body
        countries_list = JSON.parse(response)
        clean_list = countries_list.collect do |country|
            country["name"]
        end
    end 

    def self.get_country_info(country)
        x = country.gsub(" ", "%20")
        url = "https://restcountries.eu/rest/v2/name/#{x}?fullText=true"
        response = Net::HTTP.get(URI(url))
        parsed_response = JSON.parse(response)[0]
        currency_symbol = parsed_response ["currencies"].map {|s| s["symbol"]}.join
        currencies = parsed_response["currencies"].map {|c | c["name"]}.join
        languages = parsed_response["languages"].map {|l| l["name"]}.join(", ")
        Country.new(name: parsed_response["name"], capital: parsed_response["capital"], region: parsed_response["region"], sub_region: parsed_response["subregion"], population: parsed_response["population"], language: languages, currency: currencies, symbol: currency_symbol)
    end 

end

The main function of the Country class is to initialize all necessary variables for a country instance and to store every instance in a class variable.

class Country
    attr_accessor :name, :capital, :region, :sub_region, :population, :language, :currency, :symbol

    @@all = []
    COUNTRIES_LIST = []

    def initialize(name:, capital:, region:, sub_region:, population:, language:, currency:, symbol:)
        @name = name 
        @capital = capital 
        @region = region
        @sub_region = sub_region
        @population = population
        @language = language
        @currency = currency
        @symbol = symbol
        save 
    end 

    def save 
        @@all << self 
    end 

    def self.all
        @@all
    end

    def self.list
        COUNTRIES_LIST
    end 

    def self.find_by_name(name)
        @@all.select {|c| c.name == name.capitalize}
    end 

    def self.print_by_name(name)
        if @@all.select {|c| c.name == name.capitalize}

        end 
    end 
end

The CLI class is built to interact with the user. Even though this is a back-end application, a design mindset is necessary to provide users with a good experience.

class Cli
    def run 
        puts " "
        puts "Hello and welcome to my COUNTRIES APP!" if Country::COUNTRIES_LIST.length == 0
        Country::COUNTRIES_LIST << Api.get_country_list if Country::COUNTRIES_LIST.length == 0
        puts " "
        puts "Enter a name of a country to get more information about it."
        puts "..Enter 'list' to SELECT from a list of all the countries IN THE WORLD."
        puts "...Or Enter 'exit' to exit! "
        puts " "
        @country = gets.strip.downcase
        if @country == "list"
            print_list
        elsif Country.list[0].include?(@country.capitalize)
            print_country_info
        elsif @country == 'exit'
            puts "---------------------------------------"
            puts " "
            puts "Thank you for using my APP:)"
            puts " "
            puts "Bye!"
            puts " "
            puts "---------------------------------------"
            exit
        else
            error
        end 
    end 


    def print_list 
        puts " "
        puts "Here is a list of all 250 countries. Type in a number to get more info on that country."
        puts "---------------------------------------"
        Api.get_country_list.each.with_index(1) do |name, index|
            puts "#{index}. #{name}" 
            puts "---------------------------------------"
        end
        puts " "
        puts "Enter a number from the list ^ to get more info on the country:"
        input = gets.strip.downcase.to_i
        @country = Country.list[0][input - 1]
        print_country_info
    end 

    def print_country_info 
        if Country.all.any? {|c| c.name.downcase == @country.downcase}
            @country_object = Country.all.detect {|c| c.name.downcase == @country.downcase}
        else 
            @country_object = Api.get_country_info(@country)
        end 
        puts " "
        puts "Here is more information on #{@country_object.name.upcase}:"
        puts " "
        puts "---------------------------------------"
        puts " "
        puts "Capital: #{@country_object.capital}"
        puts " "
        puts "Population: #{add_commas(@country_object.population.to_s)}" 
        puts " "
        puts "Language(s): #{@country_object.language}"
        puts " "
        puts "Currency: #{@country_object.currency} (#{@country_object.symbol})"
        puts " "
        puts "Region: #{@country_object.region}"
        puts " "
        puts "Sub-region: #{@country_object.sub_region}"
        puts " "
        puts "---------------------------------------"
        run 
    end 

    def error  
        puts "---------------------------------------"
        puts "I do not understand- please try again:"
        puts " "
        run  
    end 



    def add_commas(num_string)
        #Gives commas to a string of intgers, then converts that string into an integer. 
        #So if you want to pass in a number you first have to convert it into a string using to_s.
        num_string.reverse.scan(/\d{3}|.+/).join(",").reverse
    end 

end

Conclusion

I've built static web pages with HTML and CSS in the past and also played around with Javascript but this is the first project that I have started and finished with an MVP. My biggest take away from this project is to always start with a wireframe or some kind of visual representation of what the application should do.

While building the application I also learned a lot about different countries around the world. I hope any be who uses it will get the same learning experience.

Fork/clone the project from my repo on Github and give it a go!

Happy coding!