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 действительно хорош для такого рода задач. Я бы порекомендовал проверить это.