…but my love for pretty DSLs shook its head firmly against it :P
I also had an ulterior motive – to figure out how the desc method works. Since the definition of a task is a separate method call from the description, how do the descriptions get associated? I’ve always wanted to look into it, but it was pretty far down my list. “Oh well” I thought, “no time like the present!”
Hence I began the dive into Rake’s source code, starting with the desc method:
# Describes the next rake task. Duplicate descriptions are discarded.## Example:# desc "Run the Unit Tests"# task test: [:build]# # ... run tests# end#defdesc(description)# :doc:Rake.application.last_description=descriptionend
Of course, it had to store the last description. It seems so obvious now!
moduleRake# The TaskManager module is a mixin for managing tasks.moduleTaskManager# Track the last comment made in the Rakefile.attr_accessor:last_descriptiondefinitialize# :nodoc:super@last_description=nilenddefdefine_task(task_class,*args,&block)# :nodoc:task=#...task.add_description(get_description(task))taskendprivate# Return the current description, clearing it in the process.defget_description(task)desc=@last_description@last_description=nildescendendend
That looks pretty straightforward:
1. Initialize a last_description variable, reset it for every task.
2. Add the last description to every task.
This module is included within the Application object like this:
moduleRakemoduleTaskdefinitialize(task_name,app)@comments=end# Clear the existing prerequisites and actions of a rake task.defclearclear_commentsend# Clear the existing comments on a rake task.defclear_comments@comments=selfend# Add a description to the task. The description can consist of an option# argument list (enclosed brackets) and an optional comment.defadd_description(description)returnunlessdescriptioncomment=description.stripadd_comment(comment)ifcomment&&!comment.empty?enddefcomment=(comment)# :nodoc:add_comment(comment)enddefadd_comment(comment)# :nodoc:returnifcomment.nil?@comments<<email@example.com?(comment)endprivate:add_comment# Full collection of comments. Multiple comments are separated by# newlines.deffull_commenttransform_comments("\n")endendend
Again, very straightforward:
1. Define an array of comments
2. Add any new comments to the array
3. Collate them for display.
Armed with this knowledge, let’s see how we can achieve the pretty syntax we wanted. We’ll start by patching the DSL module to add a new guard method:
moduleRake# The TaskGuards module is a mixin for adding guards for tasks.moduleTaskGuards# Track the last set of guards defined in the Rakefile.attr_accessor:last_guardsdefinitialize# :nodoc:super@last_guards=endprivate# Return the current guards, clearing it in the process.defget_guards(task)guards=@last_guards@last_guards=guardsendendend
We’re following pretty much the same code as the desc feature’s. Now to write some code to actually add the guards as prerequisites:
We first start by calling super with no arguments, which calls the ancestor’s define_task method with all arguments preserved. This would return a Rake::Task object, on which we call the #enhance method. This accepts an array of prerequisites, which are provided by our #get_guards method.
Let’s hook these up by including the TaskGuards module:
That should do it! Here’s the entire code, in all its glory:
moduleRake# The TaskGuards module is a mixin for adding guards for tasks.moduleTaskGuards# Track the last set of guards defined in the Rakefile.attr_accessor:last_guardsdefinitialize# :nodoc:super@last_guards=enddefdefine_task(task_class,*args,&block)# :nodoc:super.enhanceget_guards(task)endprivate# Return the current guards, clearing it in the process.defget_guards(task)guards=@last_guards@last_guards=guardsendendendmoduleRakemoduleDSLprivatedefguard(args)# :doc:Rake.application.last_guards<<argsendendendmoduleRakeclassApplicationincludeTaskGuardsendend
I’d stick this into a file, and include that in my Rakefile if I actually wanted to use this feature :D
This is how I’d write the two guards I used as an example. Note how we fail fast via the exit method:
namespace:guardsdotask:feature_x_enableddo# ...endtask:no_productiondoifis_environment(:production)puts'This task cannot be run in the production environment!'exitendendenddefis_environment(environment)ifdefined?(Rails)Rails.env==environmentelsifrack_env=ENV['RACK_ENV']rack_env==environmentelse['1','TRUE'].include?ENV[environment.upcase]endend
This was a mildly pointless experiment I tried to get my DSL fix, and also to understand how Rake works under the hood. I discovered how modular the library is, and how readable it makes the code. All in all, a well-spent Sunday evening!