Get to Know a Gem: Main Chapter 2

Recap

In the previous post I covered the outer shell of Main, namely the Main{} block that all Main apps call. In this post I'll go over what happens when you invoke the #argument method.

Part 1: argument('foo')

For reference, I'll base my investigation on the file a.rb that can be found in the Gem's 'samples' directory:

Main {
  argument('foo'){
    required                    
    cast :int                   
    validate{|foo| foo == 42}   
    description 'the foo param' 
  }

  def run
    puts "A#run"
    p params['foo'].given?
    p params['foo'].value
  end
}

As looked at in my previous post, when initially creating a Program instance, Main calls:

def build(*args, &block)
  …
  program.module_eval(&factory)
  …
end

Here, the module_eval processes everything in the initial block defined within Main{}. So what happens when module_eval hits:

argument('foo'){
  required                    
  cast :int                   
  validate{|foo| foo == 42}   
  description 'the foo param' 
}

Firstly #argument is actually a call to a method of the same name in ClassMethods:

def argument(*a, &b)
  (parameters << Parameter.create(:argument, main=self, *a, &b)).last
end

Following the Parameter#create method leads us to lib/main/parameter.rb where the Parameter class is defined. Parameter is the base class for all the 'types' that can be defined within the Main {} block, such as Option, Keyword, Argument, and Environment. So, #create looks like this:

def create(type, main, *a, &b)
  c = class_for(type)
  obj = c.allocate
  obj.type = c.sym
  obj.main = main
  obj.instance_eval {initialize(*a, &b)}
  obj
end

Parameter#create is essentially a factory method that based on the inputs will return an instance of a particular subclass. Thus, that #class_for finds the specific subclass:

def class_for(type)
  sym = type.to_s.downcase.to_sym
  c = Types.detect {|t| t.sym == sym}
  raise ArgumentError, type.inspect unless c
  c
end

There's nothing too out of the ordinary going on here, but looking in a little more to the Types array Main uses a clever bit of Ruby to generate an array of subclasses.

Types = [ Parameter ]
def inherited other
  Types << other
end

It took looking into Ruby's documentation to realize that #inherited is actually a standard Ruby callback that is called whenever a subclass is defined. In this way, Main succinctly generates a simple and maintanable way to hold a list of available subtypes.

So, Main finds the Argument class, instantiates an instance and then gives us another opportunity to look into some more metaprogramming with obj.instance_eval {initialize(*a, &b)}.

PART 2: Argument's arguments

If you recall, argument takes an argument and a block, which we saw passed to instance_eval on our instance of an Argument. It may get a little tricky to follo what happens, but hopefully my explanation is clear enough.

As we already have an instance of Argument, you may be wondering how or why initialize is being called. This is a clever trick that is definitely uncommon in Ruby code. obj is an allocated instance of Argument, but because we never called Argument#new #initialize hasn't been called yet. I'm not quite sure why Ara Howard chose to take this option as opposed to simply calling new and passing main and c.sym as additional arguments. Regardless, this is a good bit of Ruby knowledge that perhaps can come in handy one day. In fact, there is a related discussion from over 8 years ago that's worth reading.

So, #initialize is called and a lot happens. The method looks like this:

def initialize(name, *names, &block)
  @names = Cast.list_of_string(name, *names)

  @names.map! do |name|
    if name =~ %r/^-+/
      name.gsub! %r/^-+/, ''
    end

    if name =~ %r/=.*$/
      argument( name =~ %r/=\s*\[.*$/ ? :optional : :required )
      name.gsub! %r/=.*$/, ''
    end

    name
  end
  @names = @names.sort_by{|name| name.size}.reverse
  @names[1..-1].each do |name|
    raise ArgumentError, "only one long name allowed (#{ @names.inspect })" if
      name.size > 1
  end

  DSL.evaluate(self, &block) if block
  sanity_check!
end

The first chunk contained in the map! statement is there to parse through the arguments given to argumenet(), in our case foo. It may seem a little daunting, but all those regexes are doing are remove -- characters, and parsing the option main syntax for optional and required arguments. Once that's done, main checks that only one of the string arguments is actually a full word and the others are just single characters.

Processing the argument's block is left to DSL.evaluate(self, &block). The methods for this class are comprehensive but fairly straightforward. #evaluate appears as:

def self. evaluate param, &block
  new(param).instance_eval(&block)
end

Nothing complex here, DSL creates a new instance, param which is our instance of Argument is set as an instance variable,@param and the remaining block is instance evaled.

By now, Main's use of a variant of eval follow a similar pattern, it's metaprogramming, but it's not trying to be too clever or difficult to read. All those methods in our block,

required                    
cast :int                   
validate{|foo| foo == 42}   
description 'the foo param' 

Are just methods defined in DSL. For example, #required,

def required bool = true
  param.required = bool 
end

Simply sets an attribute on our Argument instance. The other methods in the block do exactly the same.

What's notable about the way Argument interacts with DSL is the fact that it passes itself to DSL, methods are defined on the Argument instance by DSL and nothing is returned. Hence DSL.evaluate(self, &block) if block. I've never really come across this pattern of an object handing itself over to be modified, but it seems an extensible way as a concept to place extraneous responsibilities to other classes that then augment an initial object.

CONCLUSION

From a rather high level, that's how Main's DSL is parsed. When I started reading the code it did seem daunting as there are many classes, and a lot of code. But the basic functionality begins out of basic Ruby patterns that leverage #eval methods and blocks. In the coming sections I will breakdown more detailed views of what exactly happens when our #run method is called and how Main parses through parameters. Understanding the DSL is really only half the journey, Main does quite of a lot of work once your application is run.

About Me

I'm Ruby Developer working in the Bay Area with previous experience in C#, and Java. I have a particular interest in software design principles, and metaprogramming.

  • kavinderd@gmail.com
  • RSS Feed
comments powered by Disqus