Search code examples
rubyclassapi-designgets

How to use user input across classes in Ruby?


I’m writing an app that scrapes genius.com to show a user the top ten songs. The user can then pick a song to see the lyrics.

I’d like to know how to employ the user input collected in my cli class inside of a method in my scraper class.

Right now I have part of the scrape happening outside the scraper class, but I'd like a clean division of responsibility.

Here’s part of my code:

Class CLI

def get_user_song 
  chosen_song = gets.strip.to_i
  if chosen_song > 10 || chosen_song < 1
  puts "Only the hits! Choose a number from 1-10."
end

I’d like to be able to do something like the below.

Class Scraper

def self.scrape_lyrics
  page = Nokogiri::HTML(open("https://genius.com/#top-songs")) 
  @url = page.css('div#top-songs a').map {|link| link['href']}
  user_selection = #input_from_cli #<---this is where I'd like to use the output 
  # of the 'gets' method above.

  @print_lyrics = @url[user_selection - 1]
  scrape_2 = Nokogiri::HTML(open(@print_lyrics))
  puts scrape_2.css(".lyrics").text
end 

I'm basically wondering how I can pass the chosen song variable into the Scraper class. I've tried a writing class method, but was having trouble writing it in a way that didn't break the rest of my program.

Thanks for any help!


Solution

  • I see two possible solutions to your problem. Which one is appropriate for this depends on your design goals. I'll try to explain with each option:

    • From a plain reading of your code, the user inputs the number without seeing the content of the page (through your program). In this case the simple way would be to pass in the selected number as a parameter to the scrape_lyrics method:

      def self.scrape_lyrics(user_selection)
        page = Nokogiri::HTML(open("https://genius.com/#top-songs")) 
        @url = page.css('div#top-songs a').map {|link| link['href']}
        @print_lyrics = @url[user_selection -1]
        scrape_2 = Nokogiri::HTML(open(@print_lyrics))
        puts scrape_2.css(".lyrics").text
      end 
      

      All sequencing happens in the CLI class and the scraper is called with all necessary data at the get go.

    • When imagining your tool more interactively, I was thinking it could be useful to have the scraper download the current top 10 and present the list to the user to choose from. In this case the interaction is a little bit more back-and-forth. If you still want a strict separation, you can split scrape_lyrics into scrape_top_ten and scrape_lyrics_by_number(song_number) and sequence that in the CLI class. If you expect the interaction flow to be very dynamic it might be better to inject the interaction methods into the scraper and invert the dependency:

      def self.scrape_lyrics(cli)
        page = Nokogiri::HTML(open("https://genius.com/#top-songs")) 
        titles = page.css('div#top-songs h3:first-child').map {|t| t.text}
        user_selection = cli.choose(titles) # presents a choice to the user, returning the selected number
      
        @url = page.css('div#top-songs a').map {|link| link['href']}
        @print_lyrics = @url[user_selection - 1]
        scrape_2 = Nokogiri::HTML(open(@print_lyrics))
        puts scrape_2.css(".lyrics").text
      end 
      

      See the tty-prompt gem for an example implementation of the latter approach.