Статьи

Разбор строки подключения с помощью Sprache C # Parser

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