Ruby excels at “embedded” DSLs – domain specific languages that are simultaneously plain Ruby and yet distinctly their own. RSpec springs to mind as an excellent example. At any rate, I have a DSL that recently underwent a fairly invasive change, and I wanted to automate moving model descriptions from the old format to the new.
The DSL describes domain objects in a language-agnostic fashion. For example:
defines a pair of objects, one designed to wrap around the other. Using this language, one can generate data objects in some language of your choice: Smalltalk, perhaps, or Haskell.
The DSL has one extra bit: because the language describes a wire contract between two services, it’s sensible to version the entities, so that one can quickly see that two services have gotten out of sync: one or the other needs upgrading, for instance. So our domain model might look like this:
This works pretty well: the wrapping object has version “2.0″ while the wrapped object inherits its version – “1.0″ – from its namespace. Imagine that you have some namespace with a large number of entities. It’s very easy to accidentally upgrade all objects in a namespace when you only meant to upgrade some. So the per-namespace versioning needed to disappear, forcing the user to explicitly express all versions on a per-definition basis. Clearly, in a large model, that would be an onerous task. However, this is still plain Ruby, and there just happens to be a Ruby->AST translator lying around, so it makes sense to manipulate the AST directly, and not mess around with crazy nonsense like trying to use regular expressions or similar. And given that I just happen to have a Ruby zipper library capable of traversing arbitrary hierarchical structures, a plan forms…
Getting the AST is simplicity itself:
which gives us a lovely S-expression:
All that remains is (a) transforming the AST and (b) writing the AST out again as Ruby source. The latter’s pretty boring, so let’s just stick with the former. We need to do two things: select certain nodes, and transform them. So given some helper methods added to
Enumerable, we need just say
which yields the desired, altered, model definition:
In the process of writing the above, I stumbled across some bugs in the zipr library. First, I realised that a traversal of a complicated tree would have multiple downs and ups. That caused a problem because originally “rooting” the zipper – committing all the edits to build your new structure – would detect that an edit had occured and invoke a special “root a changed structure” path. If one went up after an edit, this “something has changed” got lost. But the second issue turned out to be another instance of the same basic problem.
So we know a zipper has two parts, namely a one-hole context, and a value to plug that hole. We also know that a one-hole context is made up of a few parts: nodes to the left of the current focus point, nodes to the right, and so on. One of these pieces of information is the path from the current focus back to the root of the structure – the trail of breadcrumbs, as Learn You a Haskell calls it. Suppose we’ve just edited a node, and our path consists of a sequence of nodes a0, a1, …, an. If we move up, to node an, the context around that node has not changed. If we root the structure, the rooting process won’t know that something’s changed, and we’ve lost our edit. Suppose instead we move to the left or right. Our parent node is still an, but note that that path does not include the changed node. Again, rooting the structure does not pass through any context that says “hey, wait a minute, something’s changed”, and again our edit is lost!