DEV Community

alliecaton
alliecaton

Posted on

New York Times Bestsellers List CLI Gem

Over the last week, I've jumped head first into creating my very first ruby project from scratch. I created a gem that will give you information about the New York Times Fiction Bestsellers for a given date,________

Step 1: API Class
The first thing I did when I got started (right after I ran bundler to create my gem's structure) was build out the API class. My class initializes with a date that the user inputs, and creates the call. I created two methods in this class: one that gets the API call and returns the hash that I am interested in using, and one that creates objects out of each book that the API call retrieves.

    attr_accessor :date

    def initialize(date)
        @date = date
        create_book
    end

    ## Calls on the API with the date initialized
    def get
        url = "https://api.nytimes.com/svc/books/v3/lists/#{@date}/combined-print-and-e-book-fiction.json?api-key=#{ENV["API_KEY"]}"
        uri = URI.parse(url)
        response = Net::HTTP.get_response(uri)
        response_hash = JSON.parse(response.body)
        response_hash["results"]["books"]
    end


    ## Creates a book using a hash of relevant attributes
    def create_book
        get.each do |book|
            new_book = NytCli::Book.new(book)
        end
    end
Enter fullscreen mode Exit fullscreen mode

As of now, when I call create_book, nothing will be created! Which leads us to...

Step 2: Book Class
For my book class, I knew I wanted every book to require a title and an author, but I also wanted other attributes to be initialized if they existed in the API response hash. So I set up my book's initialize method to account for optional attributes:

    ## Initializes book with only specified keys of a hash-- possibly add where to buy later
    def initialize(hash)
        @title = title
        @author = author
        # @description = description
        hash.each do |key,value| 
            if key == "title" 
                self.send(("#{key}="), value)
            elsif key == "rank"
                self.send(("#{key}="), value)
            elsif key == "author"
                self.send(("#{key}="), value)
            elsif key == "description"
                self.send(("#{key}="), value)
            elsif key == "buy_links"
                self.send(("#{key}="), value)
            end
        end
        save
    end 
Enter fullscreen mode Exit fullscreen mode

It's not the DRY-est method, and I am interested in doing some research about how to condense this down (looking at you StackOverflow). The next thing I did was set up some class variables. I knew I wanted a class variable that collected all book objects in an array, one that collected the book objects that the user gets more information about (more on that later), and one that collects the book objects that a user saves to their session collection (also more on that later)

    @@all = [] ## All book instances
    @@all_selected = [] ## All book instances that the user has viewed
    @@saved = [] ## Books user has saved to their session collection
Enter fullscreen mode Exit fullscreen mode

Once that was out of the way, I went ahead and built out a bunch of standard methods that would come in handy later on when building my CLI class. There is a method for resetting the @@all array, one that returns a book object using an argument of a book title, a save method to shovel books into the @@all array, and a few others to assist some functions I wanted to build out in the CLI class. Now, onto the monster...

Step 3: CLI Class
Here is where the real meat of the application lies. The application has 5 main functions:

  1. Print a list of books that are associated with a user input date
  2. Choose a book from that list to get more information about it
  3. Open a link to buy a selected book from the application
  4. Add a selected book to a user's saved books for a session
  5. Show the user's saved collection, and offer the same functions as a selected book from a NYT list

I'll skip over showing some of the more basic methods in this class for brevity, and talk about the methods that gave me the most trouble, and the methods that I learned something new while creating.

The biggest hurdle I had was creating methods to validate a user-input date. I needed to validate whether the date was actually a real date, whether the date was input in the correct format to interact with the API, and whether the date fell between the user's current date, and the date that the NYT API started at (when they started publishing the Bestsellers list online). Initially, I tried to wrap up these 3 validity checks into the same method, and things quickly got out of hand, so I split them into two. The first method valid_date? checks to make sure the date is a real date and in the correct format:

    ## Checks if date is real and in the correct format
    def valid_date?(date)
        Date.valid_date? *"#{Date.strptime(date,"%Y-%m-%d")}".split('-').map(&:to_i) 
        rescue 
            puts "\n#{@@emojis[0]} Oops! That input is invalid. Please input a valid date using YYYY-MM-DD #{@@emojis[0]}\n".red
            sleep(1)
            get_date
    end
Enter fullscreen mode Exit fullscreen mode

I had no idea about the build in Date and Time classes in ruby until I this project. It made life soooo much easier to be able to use those classes. Basically, this method defines what an acceptable format is, parses out the input date into a format that the Date class can read, and then calls the built in Date class method valid_date? to check whether or not the date is a real date. The second validity method I created was my valid_timeframe? method:

    def valid_timeframe?(date)
        new_date = Time.new(date.split("-")[0].to_i, date.split("-")[1].to_i, date.split("-")[2].to_i)
        nyt_start_date = Time.new(2011, 2, 13)
        if new_date <= Time.now && new_date >= nyt_start_date
            true
        else
            puts "\n#{@@emojis[0]} Oops! That input is invalid. Make sure your date falls between now and 2011-02-13 #{@@emojis[0]}\n".red
            sleep(1)
            get_date
        end
    end
Enter fullscreen mode Exit fullscreen mode

This method simply checks to make sure that the date falls within a valid timeframe for the API call. If both of those validity methods, check out, a new API object is called and initialized, which also means we have a bunch of new book objects created and ready to access! Woohoo!

The next methods I built out were fairly simply display methods for listing out the books that were created along with the API call in a numbered list, along with a method to retrieve a user input number associated with one of the listed books, and display the title, author, rank, and description of that selected book, along with some new menu options.

Screen Shot 2021-01-08 at 1.01.33 PM
Screen Shot 2021-01-08 at 1.02.08 PM

I had a lot of fun building out the method to buy a book because it was the first method I built in this application that used an external gem. I used Launchy for this method which basically allows you to launch links from inside a CLI. Very cool!

    ## Links out to a B&N link for selected book 
    def buy_book(title)
        current_book = NytCli::Book.find_by_title (title)
        current_book.buy_links.each do |shop|
            if shop["url"].include? "barnes"
                Launchy.open(shop["url"])
                self.ask
            end 
        end 
    end
Enter fullscreen mode Exit fullscreen mode

The API gives links to several different sites to buy books from including Amazon, Indiebound, B&N, Google Books, etc.
Initially I wanted to only launch Bookshop (support your local book stores!!!!) links, but for some reason many of the links from NYT for Bookshop and Indiebound were broken! Not cool!! So I decided to use Barnes & Noble as a last resort and seemed more reliable within this API. I utilized my Book class self.find_by_title method to retrieve the book instance, and then iterated over the hash of links associated with that object to retrieve just the Barnes & Noble one. Then we pass that link into Launchy, and we're golden!

The next bit of functionality I had a good time with was the save books functionality. I really liked the idea of a place where you could collect books from different dated lists you view within the application, in case you see something you like and want to remember for later.

The basic functionality of this is very similar to the option to get a list of Bestseller books for a date, just pulling from a different place. From an individual book screen, you are able to save a book to your collection. Under the hood this means shoveling the book into a Book class variable array @@saved. To display a user's saved list, I iterate over this array and output a list. From here, things work and look pretty much the same as the functionality to list books from a Bestseller list.
Screen Shot 2021-01-08 at 1.14.43 PM

And that's pretty much it! I learned a lot of new things while creating this CLI, namely how to set up an environment and where and when to require what. Up until now, I've been writing methods to pass tests provided by Flatiron School, so there isn't much setup involved. It was very satisfying (and frustrating at times) to take the ruby skills I've learned over the last 3 weeks, and actually put them into something that works. I'm still working out how to make this a real, install-able gem, but I'm getting close! I had a lot of fun doing this, and I will definitely be making more ruby gems in the future!

Discussion (0)