Introducing X DSL, A More Fluent XML Builder

A while back, I posted about using DynamicObjects to facilitate building Domain Specific Languages in C#. To the extent that we have to play within the syntactic sandbox that the language itself requires, we are still able to take advantage of what is available in the way of built in operators by changing their meaning. Specifically, with the X DSL, I’ve combined some simple operators and a dynamic chaining API in order to make a code syntax that is more representative of the XML we want it to build for us. Here is an example:

Suppose we want to build this XML Document:

<Customers>
  <Customer category="Foo" type="Bar">
    <Address>
      <Street>123 Sunny Drive</Street>
      <City>Austin</City>
      <State>TX</State>
    </Address>
    <Profile CreatedOn="2013-05-13T18:44:27.39154-05:00">
      <Theme>Aristo</Theme>
      <DefaultPage>http://www.google.com</DefaultPage>
    </Profile>
  </Customer>
</Customers>

With X, we would use code that looks like this to create it:

var x = X._.Customers()
            > X._.Customer(category: "Foo", type: "Bar")
                > X._.Address()
                    > X._.Street("123 Sunny Drive")
                    + X._.City("Austin")
                    + X._.State("TX")
                < X._.Profile(CreatedOn: DateTime.Now)
                    > X._.Theme("Aristo")
                    + X._.DefaultPage("http://www.google.com");

The goal with X, is to make the code structurally and semantically similar to that which its instructions create. Take a close look. You’ll note a few interesting characteristics. First, I’m using X._ to represent the pipeline continuations. This allows me to make operations over an unknown number of chained future resources as the expression continues. There are a number of novel concepts that this approach yields up. First, we’re able to create a pretty complex structural recipe in a single expression. This is effectively a single line of code. Next, I’ve selected symbols for the operations that are indicative of directional navigation through immerging hierarchal and sibling relations. Specifically, the dialect has the following:

> means “step down and create what follows”

< means “step up and create what follows”

+ means “stay at the current level and create what follows”

These operation constructs enable navigation and creation simultaneously. This takes care of our need to create Xml nodes and their inner texts. However, what about attributes? Well, you can see that those too are enabled, by leveraging C#’s named parameters capability.

Thus, when we say:

X._.Customer(category: “Foo”)

We get:

<Customer category=Foo”>

Pretty nice.

This elegant abuse of the language can get us closer to code/output isomorphism, wherein the code resembles that which it is creating (at least more closely than the builtin APIs .NET provides for this kind of thing). It was inspired by the Excellent Builder library in Ruby. However, there are notable differences in how operators are used here to provide the Xml as the output of a composite expression, rather than a statement chain, as it works with Builder. In any case, I believe there is a compelling case to close the representational gap between data and code. If there’s anything that Lisp can teach us it’s that data and code should be thought of as the same thing. The closer we can get to that ideal the simpler, and more powerful our tools become in allowing us to express our intent and have the code just carry it out.

The implementation of X is here and below for anyone wishing to play around with it:


public class X : DynamicObject
{
    List<XElement> xsiblings = new List<XElement>();
    XElement xwrapper;
    XElement xplace;

    Dictionary<string, Func<X>> customOperators = new Dictionary<string, Func<X>>();

    private Func<X> Down
    {
        get { return () => new X(xplace) { xplace = xplace }; }
    }

    private Func<X> Up
    {
        get
        {
            var parent = xplace.Parent ?? xplace;
            parent = parent.Parent ?? parent;
            return () => new X(parent) { xplace = xplace.Parent };
        }
    }

    private Func<X> Self
    {
        get { return () => new X(this);  }
    }

    public static dynamic _{ get { return New; } }

    public static dynamic New
    {
        get
        {
            Func<dynamic> exp = () =>
            {
                var b = new X();
                return (dynamic)b;
            };
            return exp.Invoke();
        }
    }

    protected void init()
    {
        customOperators.Add(">", () => Down());
        customOperators.Add("<", () => Up());
        customOperators.Add("+", () => Self());
        customOperators.Add("_", () => _);
    }

    public X(){
        xwrapper = new XElement("Wrapper");
        xplace = xwrapper;
        init();
    }

    public X(XElement xelem)
    {
        xwrapper = xelem;
        xplace = xwrapper.Descendants().LastOrDefault() ?? xwrapper;
        init();
    }

    public X(X builder)
    {
        xwrapper = builder.xwrapper;
        xplace = xwrapper.Descendants().LastOrDefault() ?? xwrapper;
        init();
    }

    public X(X builder1, X builder2)
    {
        builder1.xplace.Add(builder2.xwrapper);
        builder2.xplace = builder1.xplace;
        init();
    }

    public override string ToString()
    {
        var sb = new StringBuilder();
        var content = xwrapper.Descendants();
        foreach (var c in content)
            sb.Append(c.ToString());
        return sb.ToString();
    }

    public static X operator +(X c1, X c2)
    {
        c1.xwrapper.Add(c2.xwrapper.Descendants());
        return c1;
    }

    public static X operator >(X c1, X c2)
    {
        var xlast = c1.xplace.Descendants().LastOrDefault();
        xlast = xlast ?? c1.xplace;
        xlast.Add(c2.xwrapper.Descendants());
        return c1;
    }

    public static X operator <(X c1, X c2)
    {
        var xlast = c1.xplace.Parent.Descendants().LastOrDefault();
        xlast = xlast ?? c1.xplace;
        var parent = xlast.Parent ?? xlast;
        parent = parent.Parent ?? parent;
        parent.Add(c2.xwrapper.Descendants());
        return c1;
    }

    public override bool TryInvoke(InvokeBinder binder, object[] args, out object result)
    {
            return base.TryInvoke(binder, args, out result);
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        result = default(object);

        if (customOperators.ContainsKey(binder.Name))
            result = customOperators[binder.Name]();
        else //build with this call
        {
            var work = new Func<object>(() =>
            {
                var diff = args.Count() - binder.CallInfo.ArgumentNames.Count;
                var innerText = string.Empty;
                var argQueue = new Queue<object>(args);
                if (diff > 0)
                    diff.Times(i => innerText += argQueue.Dequeue().ToString());

                args = argQueue.ToArray();
                var parameters = new Dictionary<string, object>();
                binder.CallInfo.ArgumentNames.Count().Times(i =>
                {
                    parameters.Add(binder.CallInfo.ArgumentNames[i], args[i]);
                });

                XElement xelem = new XElement(binder.Name,
                    parameters.ToList().Select(param => new XAttribute(param.Key, param.Value)));
                xelem.Add(innerText);
                xplace = xelem;
                return xelem;
            });
            var output = work.DynamicInvoke() as XElement;
            xwrapper.Add(output);
            result = this;
        }
        return true;
    }

    public X this[string key]
    {
        get { return customOperators[key](); }
    }
}

Namaste…

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s