Creating the Watcher Builder
When I build this kind of library, I like to separate the configuration of the component from the actual creation of the component internally. I use a common module defining the configuration syntax, which I share between a builder and the actual implementation. When the user starts using the DSL, he's actually talking to the builder and the builder object creates the implementation instance a little bit later. That way, chance of errors is lower and the syntax is explicitly defined separately so it's easier for somebody else to learn. We'll start with the parts of the builder component that can exist on their own without any other classes. Listing 2 shows the output of the specs for that part.
loading the library - should raise an exception when IRONRUBY_VERSION is undefined WatcherSyntax when initializing - should raise an error when no path is given - should have a path - should have an empty filters collection when no filter is provided - should register the filters - should by default not recurse in subdirs - should register and execute the block when initialized - should allow setting the path - should allow setting extra filters - should disable recursing into subdirs - should enable recursing into subdirs when registering handlers - should register a handler without a filter - should register a handler with a filter - should register a handler by method name
The specs above are actually about the behavior that will be shared between the builder and the watcher implementation object. Implementing this class would bring us pretty close to where we need to be in terms of syntax. Afterwards, it's a matter of providing an entry point and actually building the implementation.
Playing Nice With Other Libraries
The library we are creating will only work on IronRuby. It might be good form to inform other Ruby implementations that this library needs to be loaded from IronRuby to work.
There are several ways to solve that and most of them involve checking for a constant or the value of a constant. In this case, I opted to check for the constant IRONRUBY_VERSION, because I know that constant is unique for the IronRuby implementation.
Shri Borde of the IronRuby/DLR developer team recommends the following statement to check if you're running in IronRuby:
do_some_ironruby_stuff if defined?(RUBY_PLATFORM) and RUBY_PLATFORM == "ironruby".
The latter way is probably the proper way of checking for the Ruby platform you're running on.
Because I know where this is heading, I'm going to include this straight into a module. I started out with this as a part of the WatcherBuilder class and later extracted it into the WatcherSyntax module shown in Listing 3.
module WatcherSyntax
attr_accessor :path, :filters, :subdirs, :handlers
def path(val = nil) #1
@path = val if val
@path
end
def filter(*val) #2
@filters = register_filters @filters, *val
end
def top_level_only #3
@subdirs = false
end
def recurse
@subdirs = true
end
alias_method :include_subdirs, :recurse
def on(action, *filters, &handler) #4
@handlers ||= {}
@handlers[action.to_sym] ||= {}
hand = @handlers[action.to_sym]
filters = [:default] if filters.empty?
filters.each do |filt|
filt = :default if filt.to_s.empty?
hand[filt] ||= []
hand[filt] << handler
end
end
private
def register_filters(coll, *val)
val.inject(coll||[]) { |memo, filt| memo << filt unless memo.include?(filt); memo }
end
end
#1 Overloading the path attribute getter
#2 Registering filters
#3 Switching for subdirectories
#4 Registering event handlers
The code in Listing 3 first overloads the path getter (#1) that is created by the attr_accessor method. When the provided value is not nil, it will set the path value. Next, we define the filter method (#2). This can be used to register extra filters. The actual registration of a filter is handled by a private method register_filters that ensures the new filters are added only if they don't already exist. The next bit is two methods where one is the inverse of the other to function as a switch to flick on recursing into subdirectories or not (#3). The last method is the on method (#4), which is probably the most complex method in this chunk of code.
The reason that this code is more complex than it could be is because I didn't create a proper class to encapsulate a handler registration but instead I am using a Hash as data structure. As a consequence, it also needs to be initialized with default values. After the data structure initialization, it will add the handler to the registration's handler collection and it will register the filters in the filter collection for this handler. When the filters parameter of the on method is empty, it gets initialized with a :default symbol for the key in the Hash.
If we were to include the module presented here at this point into a class we'd pass about 90% of the specs from Listing 2. We'll now write some specs for the methods that are specific to a builder:
WatcherBuilder when building watches - should create a watcher - should register a new watcher in the watcher bucket - should register a new watcher and start it
This output of the specs mentions a watcher bucket. This is a class I did create for encapsulating watcher registrations. I chose to create a class in this case because we need extra behavior over the behavior of a standard Array, we want it to be able to stop and start the registered watchers and the map method should return a new WatcherBucket instead of an Array. For brevity, this class and its specs aren't included in the listings in this chapter, but they are provided with the code samples for this book. Listing 4 shows the complete implementation of the builder class.
class WatcherBuilder
include WatcherSyntax #1
def initialize(path, *filters, &configure) #2
@path = path
@filters = register_filters [], *filters
@subdirs = false
@handlers = {}
instance_eval &configure if configure #3
end
def build
Watcher.new @path, @filters, @subdirs, @handlers #4
end
def method_missing(name, *args, &b) #5
if name.to_s =~ /^on_(.*)/
self.on $1, *args, &b
else
super
end
end
def self.watch(path, *filters, &b)
@watchers ||= WatcherBucket.new #6
@watchers << WatcherBuilder.new(path, *filters, &b).build
end
def self.build(&b)
@watchers = WatcherBucket.new
instance_eval(&b)
@watchers.start_watching
@watchers
end
end
#1 Include the Syntax module
#2 Initialize defaults
#3 Call instance_eval with extra config
#4 Build an implementation object
#5 Method missing for named handlers
#6 Entry point for the DSL
The first thing we do is include the WatcherSyntax module in the WatcherBuilder class (#1). This gives us access to all the instance methods defined in that module. Next, we set up a constructor method (#2) that will set up some defaults. The last thing it does is call instance_eval and pass it the &configure block (#3).
Using instance_eval in this case is what gives us clean syntax without defining a receiver for the methods. When you call instance_eval in an object instance, the contents of that block are evaluated with the receiver of self; that is, self becomes the context of the block.
After the initialization of the builder, we defined a build instance method that creates a new instance of a Watcher implementation (#4). The last instance method that is defined is method_missing (#5). You may have noticed that we've only defined the on method in the WatcherSyntax module, which takes an action as first argument. We're going to leverage method_missing as a method dispatcher for all methods that start with on_. The remainder of the method name is assumed to be an action name and it gets dispatched to the on method. If it doesn't start with on_, the call gets passed on to the old behavior.
The last two methods are class methods, and both could serve as entry points into our DSL. The first one defines a single watch on a path with its handlers and adds those to the WatcherBucket (#6) contained by the @watchers singleton variable. The last method build is the actual entry point for our DSL and can handle many calls to the watch, thereby allowing the creation of multiple watches on multiple paths. All that is left to do now for us to get to the syntax shown in Listing 1 is define a global method that forwards its call to the build method of the WatcherBuilder class.
def filesystem(&b) FsWatcher::WatcherBuilder.build(&b) end
If we were to run the specs at this point, they should all pass, allowing us to move on to the actual implementation of the Watcher class.
(master)» ibacon -q spec/**/*_spec.rb ..................... 21 tests, 29 assertions, 0 failures, 0 errors
All specs pass, the natural order of the universe is preserved -- in short, all is well with the world. We now have a configuration DSL that can build watchers and start them if only they would have been implemented. The next and last part of this article talks about implementing the actual functionality.


