Dynamic class in ruby trick

Today I will share an interesting way to create classes with ruby and its metaprogramming features. I will introduce what my problem was and how I developed my approach. Hope you enjoy!

The problem

I was solving an exercise of the Piscine Ruby Day 03 of 42school(I am not afiliated nor student there, just solving by self interests) in ruby. The involved building an Html generator inheriting tags from the Elem class, defined as:

class Text
  def initialize(param)
    @content = param
  end

  def to_s
    @content
  end
end

class Elem
  attr_accessor :content, :opt, :tag, :tag_type

  def initialize(tag, content = [], tag_type = "double", opt = {})
    @tag = tag
    @content = content.is_a?(Array) || content.is_a?(Text) ? content : [content]
    @opt = opt
    @tag_type = tag_type
  end

  def add_content(*args)
    args.each { |el| @content << el}
  end

  def to_s
    attributes = @opt.map { |key, value| " #{key}='#{value}'" }.join("")
    check_for_content_tags = %w[Html Head Body].include?(@tag) ?  "<#{@tag}#{attributes}>\n" : "<#{@tag}#{attributes}>"
    open_tag = @tag_type == "simple" ? "<#{@tag}#{attributes} />" : check_for_content_tags

    inner_content = @content.is_a?(Text) ? content.to_s : @content.map { |el| 
                                                                      if el.is_a? String
                                                                        el
                                                                      else
                                                                        el.to_s
                                                                      end
                                                                    }.join("")

    close_tag = @tag_type == "double" ? "</#{@tag}>\n" : "\n"
    
    "#{open_tag}#{inner_content}#{close_tag}"
  end
end

The obvious approach was to inherit all the elements required by the exercise as a new classes, calling the super and so on. However, as the exercise mentioned in a disclaimer this approach is tedious, and there is another way to do it.


class Html < Elem
  def initialize(content = [], opt = {})
    super('Html', content, 'double', opt)
  end
end

class Head < Elem
  def initialize(content = [], opt = {})
    super('Head', content, 'double', opt)
  end
end

Imagine doing this to all the class(Html, Head, Body, Title, Meta, Img, Table, Th, Tr, Td, Ul, Ol, Lo, H1, H2, P, Div, Span, Hr, Br) required by the exercise-a pure exercise of patience and not programming. I thought about solving it with metaprogramming, but I was not very familiar with it on ruby, so I decided to research.

The solution

While searching the web, I found this answer by the user "sepp2k", an interesting way to create classes. You use the "Object.const_set", where the first argument is the class name, and the Class.name can be used to define the new methods inside a block using define_method and can also be used to inherit from a class and use its modules.

dynamic_name = "TestEval2"

Object.const_set(dynamic_name, Class.new) # If inheriting, use Class.new( superclass )
dummy2 = eval("#{dynamic_name}")
puts "dummy2: #{dummy2}"

You can learn more from the Module#const_set documentation.

So, the solution was easy to implement: Iterate over an array of future class names, create a new classes for them with the Object.const_set method, define a block that will inherits from the previously defined Elem class, create a logic to differentiate between "simple" and "double" tag type for some classes et voi là.

ags = %w[Html Head Body Title Meta Img Table Th Tr Td Ul Ol Lo H1 H2 P Div Span Hr Br]

tags.each { |tag|
  Object.const_set(tag, Class.new(Elem) { 
    define_method(:initialize) { |content = [], opt = {}|
      tag_type = %w[Br Img Meta Hr].include?(tag) ? "simple" : "double"
      super(tag, content, tag_type, opt)
    }
  })
}

Works completly flawless and can be easily tested with the one liner provided by the 42 school:

puts Html.new([Head.new([Title.new("Hello ground!")]), 
Body.new([H1.new("Oh no, not again!"),  Img.new([], 
{'src':'http://i.imgur.com/pfp3T.jpg'}) ]) ]).to_s

Conclusion

I hope you find this useful on your journey. It is visible meta-programming can be used instead of relying on classic inheritance approach commonly used on object oriented programming(I think it is a misconception of OOP, but that is another story), resembling something like the factory design pattern.

Hope this technical blog post helps you on your journey. Special thanks to the anonymous on Stack Overflow, to the ruby documentation and to the 42schools for creating this problem.