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!

engineersmnky
  • 25,495
  • 2
  • 36
  • 52
Yorgos Lamprakis
  • 761
  • 7
  • 16
  • 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 – engineersmnky Sep 21 '17 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](https://github.com/JoshCheek/ruby-kickstart) – Yorgos Lamprakis Sep 21 '17 at 18:17

2 Answers2

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.

Sergio Tulentsev
  • 226,338
  • 43
  • 373
  • 367
  • 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. – Aleksei Matiushkin Sep 21 '17 at 07:19
  • @mudasobwa: feel free to offer an improvement in your answer :) – Sergio Tulentsev Sep 21 '17 at 07:20
  • I am all in monkeypatching! I even refuse to refine! – Aleksei Matiushkin Sep 21 '17 at 07:21
  • Thanks @SergioTulentsev !! – Yorgos Lamprakis Sep 21 '17 at 07:36
  • 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? ;) – engineersmnky Sep 21 '17 at 13:57
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
Aleksei Matiushkin
  • 119,336
  • 10
  • 100
  • 160