0

I am taking ruby-kickstart (Josh Cheek) challenges and even though I managed to pass all the test there is one thing I cannot understand.

In the exercise you are being asked to override the << method for an instance variable. Specifically here is what you have to do:

In Ruby on Rails, when a person goes to a URL on your site, your application looks at the url, and maps it to a controller method to handle the request

My boss wanted to be able to specify what CSS class the body of the HTML output should have, based on which controller method was being accessed. It fell to me to provide a method, which, when invoked, would return a String that could handle the request There are a few nuances, though. The String you return must be retained during the object's entire life The method must be able to be called multiple times The String you return should know how to add new classes: each class is separated by a space The only method you need to worry about being invoked is the << method. (plus a few other irrelevant things) EXAMPLE:

controller = ApplicationController.new   
controller.body_class                  
#=> ""    
controller.body_class << 'admin'    
controller.body_class     
#=> "admin"   
controller.body_class << 'category'    
controller.body_class        
#=> "admin category"    
controller.body_class << 'page' << 'order'    
controller.body_class                  
#=> "admin category page order"

My working solution:

class ApplicationController 

  def initialize
    @body_class = ""
  end

  def body_class
    def @body_class.<<(str)
      puts "self is:"+self
      return self if self=~/\b#{Regexp.escape(str)}\b/
      if self==""
        self.concat(str)
      else
        self.concat(" ")
        self.concat(str)
      end
    end

    return @body_class
  end
end

Everything works perfectly fine. But an earlier solution I gave (and it didn't work) was the following

class ApplicationController 
  attr_accessor :body_class
  def initialize
    @body_class = ""
  end

  def @body_class.<<(str)
    puts "self is:"+self
    return self if self=~/\b#{Regexp.escape(str)}\b/


    if self==""
      self.concat(str)
    else
      self.concat(" ")
      self.concat(str)
    end

  end

  def body_class                #Even this for the way I work it out on my mind is unnecessary 
    return @body_class
  end



end

When someone runs on the second not-working sollution the following

obj = ApplicationController.new
obj.body_class << "Hi"

The << method is not being overridden by the object's singleton. I do not understand why I have to wrap the singleton methods inside the body_class method. (Mind that in the second solution there is an attr_accessor.

Could anyone enlighten me please! Thanks!

2
  • On another note you might want to consider what that regex does. Many frameworks use css styles like message, message-important, message-warning and a hyphen will be treated as a word boundary so if @body_class is "admin-log" and you then want to append log or admin that will not happen Commented Sep 21, 2017 at 14:12
  • Hi @engineersmnky, Thanks for your comments. This example is from a beginner’s tutorial to learn the ruby's basics. There are challenges that are being checked by rspec. I don't thing that the code is intended to work on a real app :) The tutorial can be found here Commented Sep 21, 2017 at 18:17

2 Answers 2

4

I do not understand why I have to wrap the singleton methods inside the body_class method.

To access the correct instance variable. When you attempt to override it outside of method, you're in the class context. This code runs at class load time. No instances have been created yet. (And even if instances did exist at this point, @body_class instance variable belongs to class ApplicationController, which is itself an instance of class Class).

You need instance context.


Also I am pretty sure that this problem can be solved without any method patching voodoo. Just provide a different object (conforming to the same API. This is called "duck typing").

class ApplicationController

  def body_class
    @body_class ||= CompositeClass.new
  end

  class CompositeClass
    def initialize
      @classes = []
    end

    def <<(new_class)
      @classes << new_class
    end

    # I imagine, this is what is ultimately called on ApplicationController#body_class,
    # when it's time to render the class in the html.
    def to_s
      @classes.join(' ')
    end
  end
end

Didn't test this code, naturally.

Sign up to request clarification or add additional context in comments.

5 Comments

Delegating to an array is nice. I actually doubt there is a need in CompositeClass, just delegate to a native Array and provide kinda build_class_string method.
@mudasobwa: feel free to offer an improvement in your answer :)
I am all in monkeypatching! I even refuse to refine!
Thanks @SergioTulentsev !!
I prefer this composition over a singleton method especially when that singleton method overrides an existing documented method String#<<. Notable difference though is that String#<< and String#concat when given an integer will convert it to a character where as your solution will just convert it to a String on output (#to_s). What I am really interested in is how does one unnaturally test code? ;)
1

BTW, the proper way to do it is to explicitly extend the instance variable:

class A
  attr_reader :body_class
  def initialize
    @body_class = "".extend(Module.new do
      def <<(other)
        return self if self[/\b#{Regexp.escape(other)}\b/]
        concat(' ') unless empty?
        concat(other)
      end
    end)    
  end  
end

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.