Search code examples
rubynokogiri

Ruby // Nokogiri trying to store selectors for various objects in a hash


I am trying to store the selectors in hashes assigned to the appropriate topics which I will then scrape from a webpage. However, when I do so, I am met with an 'undefined method' error for the "css" method.

Example:

@@letters_hash = {
      "a" => {
         uppercase: "A",
         history: css('div.class_1').css('div.class_2').text,
         url: "www.alphabet.com"
      }
}

Is there a way to encapsulate this? Or, if I store it as a string, is there a way to remove the string and get it back to the methods?

Thank you for your time.


Solution

  • css('div.class_1').css('div.class_2').text is self.css('div.class_1').css('div.class_2').text and self is your Database class. It doesn't have a css method. You need to call the method on something which has a css method like a Nokogiri node.

    Callbacks

    If you want to store a set of methods to call on some Nokogiri node you'll get later, you make a callback using a little anonymous function called a lambda.

    @letters = {
          "a" => {
             uppercase: "A",
             history: ->(node) { node.css('div.class_1').css('div.class_2').text },
             url: "www.alphabet.com"
          }
    }
    

    That takes a node as an argument and calls the methods on the node.

    Then later when you have a node you can call this function.

    @letters_hash[letter][:history].call(node)
    

    Objects

    At this point it's getting compliated and should be encapsulated in an object.

    class LetterTopic
      def initialize(letter)
        @letter = letter
      end
    
      def node_history(node)
        node.css('div.class_1').css('div.class_2').text
      end
    
      def uppercase
        @letter.upcase
      end
    
      def url
        "www.alphabet.com"
      end
    end
    
    letters = {
      "a" => LetterTopic.new("a")
    }
    
    node = ...get a Nokogiri node...
    
    letters[letter].node_history(node)
    

    A Note About Class Variables

    @@letters_hash does not do what you think. Class variables in Ruby are shared by subclasses. If you subclass Database they will all share a single @@letters_hash variable.

    class Database
      @@letters = {}
    
      def self.letters
        @@letters
      end
    end
    
    class Databasement < Database
    end
    
    Database.letters[:a] = 'database'
    Databasement.letters[:a] = 'databasement'
    
    p Database.letters     # {:a=>"databasement"}
    p Databasement.letters # {:a=>"databasement"}
    

    Instead, use Class Instance Variables. Like everything else in Ruby, the Database class is an object and can have its own instance variables.

    class Database
      # Everything inside `class << self` works on the class object.
      class << self
        def letters
          @letters ||= {}
        end
      end
    end
    
    class Databasement < Database
    end
    
    Database.letters[:a] = 'database'
    Databasement.letters[:a] = 'databasement'
    
    p Database.letters       # {:a=>"database"}
    p Databasement.letters   # {:a=>"databasement"}