Parsing a Connection String With 'Sprache' C# Parser
Join the DZone community and get the full member experience.
Join For FreeSprache 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.
Published at DZone with permission of Mike Hadlow, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments