Word Lookup is a CLI application coded in Ruby that I created for my first project at Flatiron School.
It uses the Words API to look up a word provided by the user and list its definitions. The user can then choose to view the synonyms, antonyms, rhymes, or similar words for the word they entered.
This post goes through the app from start to finish and explains the classes and methods used, as well as some of the design decisions I made. It's mostly for my own future reference, but hopefully it will be of some use to other newbies looking to build their first CLI app!
Source code
Check out the source code for this project at https://github.com/caitlinhinshaw/word-lookup.
Starting the program
To start the app, navigate to the root folder in the terminal, then run ./bin/word_lookup
. This file contains the following code:
#!/usr/bin/env ruby
require_relative '../lib/environment'
WordLookup::CLI.new.call
The top line tells the terminal that this is a Ruby file - by including it, we don't have to put 'ruby' in front of the filename to run it on the command line (i.e. ruby ./bin/word_lookup
).
The middle line loads environment.rb
, which in turn loads all the other needed files and gems:
require_relative "./word_lookup/version"
require_relative "./word_lookup/cli"
require_relative "./word_lookup/word"
require_relative "./word_lookup/api"
require 'pry'
require 'json'
require 'uri'
require 'net/http'
require 'openssl'
require 'dotenv/load'
require 'colorize'
The last line starts the CLI sequence by creating a new instance of the CLI class and calling the CLI instance method call
.
#CLI Class
def call
puts "\nWelcome to Word Lookup!".colorize(:light_cyan)
choose_word
end
We can see that the call
method prints out a greeting to the user, then calls the choose_word
method.
#CLI class
def choose_word
puts "\nEnter a word to look up:".colorize(:light_green)
word = gets.strip
@current_word = WordLookup::Word.new(word)
validate_word
end
The choose_word
method asks the user what word they want to look up and saves their response into a local variable word
.
Then, it creates a new instance of the Word class by passing in word
as an argument, and saves this instance to @current_word
, an instance variable of the CLI class.
Finally, it calls the next method in the CLI flow, validate_word
.
Creating a new instance of the Word class
Since we want to fetch information from the Words API when a Word instance is created, we need to create a custom initialize
method.
#Word class
def initialize(word_text)
@word_text = word_text
@detail_hash = WordLookup::API.new.fetch_word_details_from_API(@word_text)
@@all << self
end
The custom method accepts the word_text
that the user entered and saves it to the instance attribute @word_text
.
It creates a new instance of the API class, calls the method fetch_word_details_from_API
on the new API instance with @word_text
as the argument, then saves the returned hash into the attribute @detail_hash
.
Finally, it saves the new Word instance (accessed with self
) to
the class attribute @@all
.
Fetching word details from the API
The API class uses the uri
, openssl
, and dotenv
gems, as well as the net/http
and json
packages included with Ruby.
When fetch_word_details
is called, it calls the method API_call
, which returns JSON data. The method convert_json_to_ruby
converts the JSON into Ruby, then fetch_word_details
returns the converted hash back to the Word method that called it.
#API class
def fetch_word_details_from_API(word)
url = URI("https://wordsapiv1.p.rapidapi.com/words/#{word}/")
json = API_call(url)
convert_json_to_ruby(json)
end
def API_call(url)
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
request = Net::HTTP::Get.new(url)
request["x-rapidapi-host"] = 'wordsapiv1.p.rapidapi.com'
request["x-rapidapi-key"] = ENV['WORDS_API_KEY']
http.request(request)
end
def convert_json_to_ruby(json)
JSON.parse(json.body)
end
Validating the word
Before trying to return any details on the word the user entered, we should make sure it's a real word that exists in the API data, not something like 'asfadsfa' or '431&!6'.
The Words API makes this easy: it includes a "success"
key in the
returned hash. If the value is false
, then the word was not found in the database. We can use this to build our validate_word
method.
#CLI class
def validate_word
if @current_word.detail_hash["success"] == false
puts "\nThe word '#{@current_word.word_text}' was not found. Please try again.".colorize(:light_red)
choose_word
else
@current_word.add_details
list_definitions
end
end
If the word is not valid, it calls the choose_word
method again, and the user can enter another word.
If the word is valid, it calls the add_details
method from the Word class on the @current_word
instance, which assigns values to @current_word
's attributes based on the data in the @detail_hash
.
Then, it calls the next method in the CLI flow - list_definitions
.
Populating the Word instance with attributes
#Word class
def add_details
fetch_definitions
fetch_synonyms
fetch_antonyms
fetch_similar_words
fetch_rhymes
end
def fetch_definitions
@definitions = []
@detail_hash["results"].each do |meaning|
@definitions << meaning["definition"]
end
end
def fetch_synonyms
@synonyms = []
@detail_hash["results"].each do |meaning| #collects the synonyms for each meaning to display all at once
@synonyms << meaning["synonyms"]
@synonyms << meaning["also"]
end
@synonyms = flatten_and_remove_nils(@synonyms)
end
#Other fetch methods have a similar structure to the above
Each attribute for an instance of Word is an array that lists all the details found for a particular detail type: i.e., all the synonyms, all the rhymes, etc.
Every fetch method iterates through the @detail_hash
and adds
values from the relevant key-value pairs to the attribute array.
Sometimes, the values added are an array themselves, which turns the attribute array into a nested array. Since we just want a simple list of all the details, we can use a method to flatten the attribute array and make sure it doesn't contain any nulls or duplicates.
#Word class
def flatten_and_remove_nils(array)
flat_array = array.flatten.uniq
flat_array.compact unless array.none? {|a| a.nil?}
#returns a flattened array with no nils or duplicate values
#unless statement is necessary because array.compact returns nil if
#there are no nil values, and we want it to return the array instead
end
After all the details are added to the Word instance, we can easily access them from inside the API class.
Listing the definitions
#CLI class
def list_definitions
puts "\nHere are the definition(s) for '#{current_word.word_text}':".colorize(:light_blue)
@current_word.definitions.each_with_index do |definition, index|
puts "#{index+1}. #{definition}"
end
list_detail_categories
end
The method list_definitions
gets the current word's @definitions
array and iterates through the array to print each definition to a numbered list.
After the definitions are printed, it calls list_detail_categories
, the next method in the CLI flow.
Listing the detail categories
#CLI class
def list_detail_categories
puts "\nWhat information would you like about the word '#{current_word.word_text}'? Enter a number.".colorize(:light_green)
puts "\n"
WordLookup::Word.detail_categories.each_with_index do |category, index|
puts "#{index+1}. #{category}"
end
choose_details
end
The list_detail_categories
method prompts the user to choose what kind of information they would like about the chosen word.
It prints the available options using the Word class variable
@@detail_categories
, which contains the available detail types as strings in an array. Storing the names of available detail types this way makes it easy to print them as a numbered list using each_with_index
, and also increases maintainability since those string values are not repeated throughout the code.
Finally, this method calls the next method in the CLI flow, choose_details
.
Choosing the details
#CLI class
def choose_details
chosen_details = gets.strip.to_i
if valid_choice?(chosen_details, WordLookup::Word.detail_categories)
list_details(chosen_details)
else
puts "This is not a valid choice. Please try again.".colorize(:light_red)
choose_details
end
end
This method takes in the user's input and saves it to the local variable chosen_details
.
Since there are only 4 valid choices - 1, 2, 3, and 4 - we need to use another method, valid_choice?
to validate the user's input.
If the choice is valid, the method calls list_details
, the next method in the CLI flow.
If the choice is not valid, it prints a message to the user to try again, and calls choose_details
again so the user can enter another choice.
Validating the user's choice
#CLI class
def valid_choice?(input, array)
input <= array.length && input > 0
end
The method valid_choice?
checks if the input is a non-negative number less than or equal to the length of the @@detail_categories
array. This is better than hard-coding the allowed inputs - we might want to add more detail categories in the future, and referencing the array directly will reduce the number of updates needed if we do.
It returns true
if the choice is valid, and false
if it is not.
Listing the details
#CLI class
def list_details(chosen_details)
case chosen_details
when 1
output_detail_results(@current_word.synonyms, "synonyms")
when 2
output_detail_results(@current_word.antonyms, "antonyms")
when 3
output_detail_results(@current_word.similar_words, "similar words")
when 4
output_detail_results(@current_word.rhymes, "rhymes")
end
choose_next_action
end
This method accepts the user's input as an argument, and uses it to decide which of the current word's detail attributes should be printed out. It passes the chosen attribute, (along with the string that represents it for printing purposes), to the method output_detail_results
.
After output_detail_results
runs, it calls the next method in the CLI flow - choose_next_action
.
Printing the detail results
This method prints the chosen details, if any exist.
If the detail array is empty, it displays a message to the user that no details were found.
If the array is not empty, it uses the helper method print_detail_array
to iterate through the details and print them as an unordered list.
#CLI class
def output_detail_results(word_details_array, word_details)
if word_details_array.empty?
puts "\nThe word '#{@current_word.word_text}' has no listed #{word_details}.".colorize(:light_blue)
else
puts "\nHere are the #{word_details} for '#{@current_word.word_text}':".colorize(:light_blue)
print_detail_array(word_details_array)
end
end
def print_detail_array(array)
array.each do |item|
puts "- #{item}"
end
end
Choosing the next action
The choose_next_action
method allows the user to get more details on the current word (by calling list_detail_categories
), or look up another word (by calling choose_word
) without exiting and restarting the program. It also prints a goodbye message if the user chooses to exit.
#CLI class
def choose_next_action
puts "\nWhat would you like to do?".colorize(:light_green)
puts "\nEnter 1 for more details on '#{@current_word.word_text}'."
puts "Enter 2 to look up a new word."
puts "Enter 3 to exit the program."
next_action = gets.strip.to_i
case next_action
when 1
list_detail_categories
when 2
choose_word
when 3
puts "\nGoodbye!".colorize(:light_cyan)
puts "\n"
exit
else
puts "This is not a valid selection. Please try again.".colorize(:light_red)
choose_next_action
end
end
The code in action
Thanks for reading :D
If you have any questions or feedback, feel free to leave a comment or find me on Twitter @DevCait!
Top comments (0)