Sprache — очень классная легковесная библиотека синтаксического анализатора для C #. Сегодня я экспериментировал с парсингом строк соединения EasyNetQ , поэтому я решил, что мне нужно, чтобы Sprache это сделал. Строка подключения EasyNetQ представляет собой список пар ключ-значение, например:
key1=value1;key2=value2;key3=value3
Мотивация для взгляда на что-то более сложное, чем простое разбиение строк на основе разделителей, заключается в том, что я думаю о том, чтобы иметь более сложные значения, которые сами должны были бы быть проанализированы. Но это на будущее, сегодня я просто собираюсь проанализировать простую строку подключения, где значения могут быть строками или числами (точнее, короткими).
Итак, я хочу проанализировать строку подключения, которая выглядит следующим образом:
virtualHost=Copa;username=Copa;host=192.168.1.1;password=abc_xyz;port=12345;requestedHeartbeat=3
… в строго типизированную структуру, как это:
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; } }
Я хочу, чтобы было проще добавлять новые элементы строки подключения.
Сначала давайте определим имя для функции, которая обновляет ConnectionConfiguration. Необычно используемая версия оператора using позволяет нам давать краткое имя сложному типу:
using UpdateConfiguration = Func<ConnectionConfiguration, ConnectionConfiguration>;
Теперь давайте определим небольшую функцию, которая создает анализатор Sprache для пары ключ-значение. Мы предоставляем ключ и анализатор для значения и возвращаем синтаксический анализатор, который может обновить 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; }); }
CreateSetter — это небольшая функция, которая превращает выражение свойства (например, x => x.Name) в Action <TTarget, TProperty>.
Далее давайте определим парсеры для строковых и числовых значений:
public static Parser<string> Text = Parse.CharExcept(';').Many().Text(); public static Parser<ushort> Number = Parse.Number.Select(ushort.Parse);
Теперь мы можем связать последовательность вызовов BuildKeyValueParser и Or их вместе, чтобы мы могли проанализировать любое из наших ожидаемых значений ключа:
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));
Каждый вызов BuildKeyValueParser определяет ожидаемую пару ключ-значение нашей строки подключения. Мы просто даем имя ключа, анализатор, который понимает значение, и свойство в ConnectionConfiguration, которое мы хотим обновить. По сути, мы определили небольшой DSL для строк подключения. Если я хочу добавить новое значение строки подключения, я просто добавляю новое свойство в ConnectionConfiguration и одну строку в приведенный выше код.
Теперь давайте определим синтаксический анализатор для всей строки, сказав, что мы проанализируем любое количество частей значения ключа:
public static Parser<IEnumerable<UpdateConfiguration>> ConnectionStringBuilder = from first in Part from rest in Parse.Char(';').Then(_ => Part).Many() select Cons(first, rest);
Все, что нам нужно сделать, это проанализировать строку подключения и применить цепочку функций обновления к экземпляру ConnectionConfiguration:
public IConnectionConfiguration Parse(string connectionString) { var updater = ConnectionStringGrammar.ConnectionStringBuilder.Parse(connectionString); return updater.Aggregate(new ConnectionConfiguration(), (current, updateFunction) => updateFunction(current)); }
С Sprache мы получаем много приятных вещей, одна из лучших — отличные сообщения об ошибках:
Parsing failure: unexpected 'x'; expected host or port or virtualHost or requestedHeartbeat or username or password (Line 1, Column 1).
Sprache действительно хорош для такого рода задач. Я бы порекомендовал проверить это.