Friday, September 28, 2012

Parsing a Connection String With Sprache

Sprache is a very cool lightweight parser library for C#. Today I was experimenting with parsing EasyNetQ connection strings, so I thought I’d have a go at getting Sprache to do it. An EasyNetQ connection string is a list of key-value pairs like this:

key1=value1;key2=value2;key3=value3

The motivation for looking at something more sophisticated than simply chopping strings based on delimiters, is that I’m thinking of having more complex values that would themselves need parsing. But that’s for the future, today I’m just going to parse a simple connection string where the values can be strings or numbers (ushort to be exact).

So, I want to parse a connection string that looks like this:

virtualHost=Copa;username=Copa;host=192.168.1.1;password=abc_xyz;port=12345;requestedHeartbeat=3

… into a strongly typed structure like this:

public class ConnectionConfiguration : IConnectionConfiguration
{
public string Host { get; set; }
public ushort Port { get; set; }
public string VirtualHost { get; set; }
public string UserName { get; set; }
public string Password { get; set; }
public ushort RequestedHeartbeat { get; set; }
}

I want it to be as easy as possible to add new connection string items.

First let’s define a name for a function that updates a ConnectionConfiguration. A uncommonly used version of the ‘using’ statement allows us to give a short name to a complex type:

using UpdateConfiguration = Func<ConnectionConfiguration, ConnectionConfiguration>;

Now lets define a little function that creates a Sprache parser for a key value pair. We supply the key and a parser for the value and get back a parser that can update the ConnectionConfiguration.

public static Parser<UpdateConfiguration> BuildKeyValueParser<T>(
string keyName,
Parser<T> valueParser,
Expression<Func<ConnectionConfiguration, T>> getter)
{
return
from key in Parse.String(keyName).Token()
from separator in Parse.Char('=')
from value in valueParser
select (Func<ConnectionConfiguration, ConnectionConfiguration>)(c =>
{
CreateSetter(getter)(c, value);
return c;
});
}

The CreateSetter is a little function that turns a property expression (like x => x.Name) into an Action<TTarget, TProperty>.

Next let’s define parsers for string and number values:

public static Parser<string> Text = Parse.CharExcept(';').Many().Text();
public static Parser<ushort> Number = Parse.Number.Select(ushort.Parse);

Now we can chain a series of BuildKeyValueParser invocations and Or them together so that we can parse any of our expected key-values:

public static Parser<UpdateConfiguration> Part = new List<Parser<UpdateConfiguration>>
{
BuildKeyValueParser("host", Text, c => c.Host),
BuildKeyValueParser("port", Number, c => c.Port),
BuildKeyValueParser("virtualHost", Text, c => c.VirtualHost),
BuildKeyValueParser("requestedHeartbeat", Number, c => c.RequestedHeartbeat),
BuildKeyValueParser("username", Text, c => c.UserName),
BuildKeyValueParser("password", Text, c => c.Password),
}.Aggregate((a, b) => a.Or(b));

Each invocation of BuildKeyValueParser defines an expected key-value pair of our connection string. We just give the key name, the parser that understands the value, and the property on ConnectionConfiguration that we want to update. In effect we’ve defined a little DSL for connection strings. If I want to add a new connection string value, I simply add a new property to ConnectionConfiguration and a single line to the above code.

Now lets define a parser for the entire string, by saying that we’ll parse any number of key-value parts:

public static Parser<IEnumerable<UpdateConfiguration>> ConnectionStringBuilder =
from first in Part
from rest in Parse.Char(';').Then(_ => Part).Many()
select Cons(first, rest);

All we have to do now is parse the connection string and apply the chain of update functions to a ConnectionConfiguration instance:

public IConnectionConfiguration Parse(string connectionString)
{
var updater = ConnectionStringGrammar.ConnectionStringBuilder.Parse(connectionString);
return updater.Aggregate(new ConnectionConfiguration(), (current, updateFunction) => updateFunction(current));
}

We get lots of nice things out of the box with Sprache, one of the best is the excellent error messages:

Parsing failure: unexpected 'x'; expected host or port or virtualHost or requestedHeartbeat or username or password (Line 1, Column 1).

Sprache is really nice for this kind of task. I’d recommend checking it out.

5 comments:

Simone said...

What about a simple 5-liner? http://pastie.org/4856625


Mike Hadlow said...

Very nice Simone.

Don't really meet my requirements to also be able to parse the value types though.

Simone said...

Not sure why you would want structured types in a connection string (I'd go for json, for instance), but that's definitely possible, that's what TypeConverters, which is what I'm using in the sample, do: http://msdn.microsoft.com/en-us/library/yy580hbd.aspx

EvilSpudBoy said...

You mentioned a CreateSetter function but left out the implementation. You really can't get this to work without it though, so I had to come up with my own implementation.

Here is what I did in case anyone is stuck on that.


static Action CreateSetter(Expression> getExpr)
{
var member = getExpr.Body;
var param = Expression.Parameter(typeof(TProperty), "value");
var setExpr = Expression.Lambda>(
Expression.Assign(member, param), getExpr.Parameters[0], param);

return setExpr.Compile();
}

EvilSpudBoy said...

The code above got mangled.

Here it is:

http://pastie.org/4915547