Code Voyeur
RSS
Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net

A Boo DSL for Transforming Objects to RSS

Many .NET (and of course Java) libraries use complex, custom configurations. The XML blocks in a NAnt build file require complex constructs to provide its great flexibility. Calling a function in NAnt requires a very non-XML like syntax.
<echo message="${hello::hello-world()}" />
Spring.NET has a powerful validation support, but requires a simple, baked-in expression language.
<v:condition test="ReturningFrom.Date >= StartingFrom.Date" when="ReturningFrom.Date != DateTime.MinValue">
<v:message id="error.returnDate.beforeDeparture" providers="returnDateErrors, validationSummary"/>
</v:condition>
In both cases, XML alone is not expressive enough to achieve the dynamic nature of the configuration requirements.

This article will examine a trivial domain-specific language used to solve the problem of how to define rules to transform an object of type T into an item block in an RSS feed.

<item>
    <title>...</title>

    <description>...</description>
    <link>...</link>
    <pubDate>...</pubDate>
</item>
Three model objects are used in the sample project to demonstrate the problem and solution. Manufacturer, Product and ProductReview are simply plain-old .NET objects.
public class Manufacturer
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public double Price { get; set; }
    public DateTime CreateDate { get; set; }
    public Manufacturer Manufacturer { get; set; }
}

public class ProductReview
{
    public int Id { get; set; }
    public Product Product { get; set; }
    public string UserName { get; set; }
    public string Review { get; set; }
    public DateTime ReviewDate { get; set; }
}
It might be tempting to define an XML configuration section that has maps property names to nodes in the RSS item block.
<item_rule for="Product">
    <title_rule property="Name" />
    ...
</item>
A problem quickly arises with this simple mapping when the title node should be composed of the manufacturer's name and the product's name. The XML could be extended to support some basic operations, such as concatenation.
<title_rule property="Manufacturer.Name + Name" />
However, this setup gets further complicated when formatting rules are considered.
<title_rule property="Manufacturer.Name.ToUpper() + Name" />
The rule definitions either become code embedded in XML (IronPython for example) or an expression language is baked in (as with Spring.NET's validation).

An arguably cleaner solution would be to create a simple DSL that is both easier to read and more powerful. The DSL for this article's project is simple, defining only five "keywords."

rule_for:
    title
    description
    link
    pubDate
The DSL will be interpreted and executed by the class RssDslEngine. This class will define the DSL, essentially by passing function references into a Boo script. The Boo script will then provide function references in return. The RssDslEngine will first interpret the DSL in its static constructor, allowing support for rule caching. Rules may be re-interpreted using the Reset method.
public class RssDslEngine
{        
        private static Dictionary<string, Dictionary<string, Func<object, string>>> _rules = 
            new Dictionary<string, Dictionary<string, Func<object, string>>>();
        private string _ruleName = null;
        InteractiveInterpreter _interpreter = null;
 
    static RssDslEngine()
    {
        new RssDslEngine().init();             
    }

    public void Reset()
    {
        init();
    }

    ...        

    private void init()
    {
        ...
    }

    private void getRuleBody(string ruleName)
    {
        ...
    }
}
Most of the work for building and interpreting the DSL takes place in the private init method.
private void init()
{
    string filePath = HttpContext.Current.Server.MapPath("~/App_Data/rules.boo");
    _interpreter = new InteractiveInterpreter();
    _interpreter.Ducky = true;
    ...
}
The first line of init() simply maps the path of the boo rule file. The next two lines initialize the privately scoped InteractiveInterpreter instance, with duck typing enabled. The instance of Boo's InteractiveInterpreter class will execute the Boo script.
Action<string, Action> ruleFor = delegate(string name, Action action)
{
    if (!_rules.ContainsKey(name))
        _rules[name] = new Dictionary<string, Func<object, string>>();
    _ruleName = name;
    action();
};
The next chunk in init() creates an instance of an Action delegate named ruleFor, which takes as input a string and another Action instance. When ruleFor is invoked, the private static _rules Dictionary is updated to include a reference to a new Dictionary that will itself contain delegates. The delegate passed to ruleFor is then executed.
List<string> fields = RssFieldFactory.Create();
fields.ForEach(x => getRuleBody(x));
The init method continues by creating a list of field names (one for each of an RSS item's child nodes). For each of these field names, the getRuleBody method is executed.
private void getRuleBody(string ruleName)
{
    Action<Func<object, string>> action = (x) => _rules[_ruleName][ruleName] = x;
    _interpreter.SetValue(ruleName, action);
}
The getRuleBody method creates a new Action instance for each of the defined fields. The body of the action simply adds the delegate reference to the _rules Dictionary keyed with the field name. In other words for the field title, a delegate is created that requires as input another delegate that is passed an object and returns a string. When the title delegate is executed, the delegate it was passed is placed in the _rules dictionary. The next line passes a reference to the Action instance to the Boo interpreter. Essentially, this line will give the Boo script the ability to invoke the Action.
_interpreter.SetValue("rule_for", ruleFor);
_interpreter.EvalCompilerInput(new FileInput(filePath));
The next line of init() passes a reference to ruleFor to the Boo interpreter. The final line provides the actual script execution.
rule_for "Product":
    title def(x):
        return x.Name
    description def(x):
        return x.Description
    link def(x):
        return "http://codevoyeur.com/products/"
    pubDate def(x):
        return x.CreateDate
The rules.boo file contains the configured transformation rules. As this script is executed, the engine will invoke the referenced delegates. The rule_for function is called, invoking the ruleFor Action instance. The input to rule_for is the string Product and the entire block of code that follows. Boo's flexible syntax allows functions to be called without parentheses. It also allows parameterless closures (delegates) to be passed as arguments using the colon and block syntax above.

Next, the call to title invokes the anonymous delegate that was created for the title field in getRuleBody. Its input is another anonymous closure. A reference to this closure is then stored in the engine's _rules dictionary - basically this looks like _rules["Product"]['title"] = x => return x.Name; without the DSL. The calls to description, link and pubDate produce the same results.

To support the notion that this simple DSL is better than the above XML based solutions, only slightly more complicated rules need be considered.

rule_for "Product":
    title def(x):
        return "${x.Manufacturer.Name.ToUpper()} - ${x.Name}"
    description def(x):
        return x.Description
    link def(x):
        return "http://codevoyeur.com/products/${x.Id}"
    pubDate def(x):
        return x.CreateDate.ToString("s")
In the revised rules, it becomes more evident that this DSL introduces the full power of Boo and the .NET Framework to the configuration language. There is no need for complex XML syntax or a built in expression language (which is sort of like having a DSL within a DSL).
rule_for "ProductReview":
    title {x | return "${x.Product.Manufacturer.Name.ToUpper()} - ${x.Product.Name} Review by ${x.UserName}" }
    description { x | x.Review }
    link {x | "http://codevoyeur.com/productreviews/${x.Id}" }
    pubDate { x | x.ReviewDate.ToString("s") }
Because Boo supports a whitespace insensitive closure syntax, a rule_for block may also be written more tersely.
public string Execute(object o, string field)
{
    string ruleName = o.GetType().Name;
    if (_rules.ContainsKey(ruleName))
        return _rules[ruleName][field].Invoke(o);
    else
        throw new ApplicationException("Invalid rule name");            
}
The final bit of RssDslEngine code is the Execute method, which allows consuming classes to invoke the various rules. Consumers pass in the object to be transformed and the field (rule name) to be applied.

The sample project includes an RssWriter class that is responsible for constructing the XML for an RSS feed for an array of type T.

public RssWriter(string title, string link, string description, T[] items)
{
    RssDslEngine engine = new RssDslEngine();

    XmlTextWriter writer = new XmlTextWriter(_stringWriter);            
    writer.WriteStartElement("rss");
        ...
            
    foreach (T item in items)
    {
        writer.WriteStartElement("item");                
        List<string> fields = RssFieldFactory.Create();
        fields.ForEach(x => writer.WriteElementString(x, engine.Execute(item, x)));
        writer.WriteEndElement();
    }

    ...
}
Each element in the array is serialized to an item block using the rules defined in the rules.boo file.
public void ProcessRequest(HttpContext context)
{
    context.Response.ContentType = "text/xml";

    Manufacturer roland = new Manufacturer() { Id = 1, Name = "Roland" };
    Manufacturer fender = new Manufacturer() { Id = 2, Name = "Fender" };
    Manufacturer zoom = new Manufacturer() { Id = 3, Name = "Zoom" }; 
      
    Product juno = new Product() {    ...    };
    Product tele = new Product() {    ...    };
    Product h2 = new Product() {    ...    };

    RssWriter<Product> writer = new RssWriter<Product>
        (
            ...
            new Product[] {juno, tele, h2}
        );            
    context.Response.Write(writer.GetFeed());
}
The sample web project also includes two files, Products.ashx and ProductReviews.ashx that create Product and ProductReview arrays and then use the RssWriter class to output an RSS feed.
<rss version="2.0">
  <channel>
    <title>Code Voyeur Music Product Listing</title>
    <link>http://www.codevoyeurmusic.com</link>
    <description>New Products</description>
    <language>en-us</language>
    <item>
      <title>ROLAND - Juno D 61 Key Pro Keyboard</title>
      <description>...</description>
      <link>http://codevoyeur.com/products/1000</link>
      <pubDate>2008-06-25T22:33:40</pubDate>
    </item>
    <item>
      <title>FENDER - American Series Telecaster Electric Guitar</title>
      <description>...</description>
      <link>http://codevoyeur.com/products/1001</link>
      <pubDate>2008-06-22T22:33:40</pubDate>
    </item>
    <item>
      <title>ZOOM - H2 Handy Portable Digital Recorder</title>
      <description>...</description>
      <link>http://codevoyeur.com/products/1003</link>
      <pubDate>2008-06-24T22:33:40</pubDate>
    </item>
  </channel>
</rss>
The need for a a DSL to transform object properties to RSS item nodes might not be pressing, but hopefully it demonstrates how bloated XML configurations might be made both simpler and more extensible. Looking beyond the RSS example, it is not difficult to imagine a similar but more complex DSL that allows objects to be serialized in different formats (JSON or HTML for example).

Finally, there are more elegant and more complex ways to implement a Boo DSL (see references). This article admittedly and intentionally takes a brute-force approach by passing delegates references into global interpreter values. The engine could certainly be more polished (better caching and thread safety).

Download Sample Project

References

Ayende on implementing a DSL
Source for sample project
Article Posted: Thursday, June 26, 2008

Leave a Comment


Contact Code Voyeur about this article.