DevGang
Авторизоваться

Преобразование сложных типов YAML в .NET с помощью YamlDotNet

Когда дело доходит до сериализации и десериализации YAML в .NET, YamlDotNet — это идеальная библиотека, которую на NuGet скачали более 100 миллионов раз. Она также интегрируется в различные проекты Microsoft и .NET, несмотря на отсутствие официальной библиотеки Microsoft YAML для .NET.

В этом блоге мы рассмотрим процесс создания пользовательских сериализаторов и десериализаторов YAML с использованием YamlDotNet. Чтобы проиллюстрировать эти концепции, мы рассмотрим конкретный вариант использования частичного анализа раздела переменных среды Docker Compose.

Вариант использования переменных среды Docker Compose

Docker Compose позволяет определять переменные среды в двух разных форматах. Первый, известный как формат объекта, выглядит следующим образом:

environment:
  RACK_ENV: development
  SHOW: "true"
  SESSION_SECRET:

Этот формат объекта можно напрямую десериализовать в словарь строк. Однако Docker Compose также поддерживает формат массива:

environment:
  - RACK_ENV=development
  - SHOW=true
  - SESSION_SECRET

В отличие от формата объекта, формат массива более сложен для десериализации, поскольку он состоит из массива строк. Если мы хотим последовательно десериализовать оба формата в словарь строк, нам нужно создать собственный сериализатор. Это можно сделать путем реализации интерфейса IYamlTypeConverter.

Прежде чем приступить к коду, давайте сначала разберемся с тремя типами YAML tokens, которые можно встретить при анализе документа YAML с помощью YamlDotNet:

  • Токен Scalar представляет наличие скалярного значения. Это может быть строка, число, логическое значение и т. д.
  • Токены MappingStart и MappingEnd представляют начало и конец объекта YAML — перечисление пар ключ-значение. Обратите внимание, что ключи всегда являются скалярами.
  • Токены SequenceStart и SequenceEnd представляют начало и конец массива YAML — перечисление значений.

Вы можете понять, как документы YAML можно анализировать с использованием этих токенов здесь:

# If we were to parse the "myobject" YAML value, we would encounter:
# MappingStart, Scalar (foo), Scalar (bar), MappingEnd
myobject:
  foo: bar

# If we were to parse the "myarray" YAML value, we would encounter:
# SequenceStart, Scalar (foo), SequenceEnd
myarray:
  - foo

Реализация нашего кастомного IYamlTypeConverter

Имея это в виду, давайте начнем с реализации интерфейса IYamlTypeConverter. Интерфейс имеет три метода. Первый, Accepts, используется для определения того, может ли преобразователь обрабатывать данный тип. В нашем случае мы хотим обрабатывать тип EnvironmentVariables, который я только что создал для представления словаря переменных среды:

public class EnvironmentVariables : Dictionary<string, string>
{
}

public class EnvironmentVariablesTypeConverter : IYamlTypeConverter
{
    public bool Accepts(Type type)
    {
        return type == typeof(EnvironmentVariables);
    }

    // [...]
}

Второй метод, ReadYaml, используется для десериализации документа YAML в объект .NET. В нашем сценарии мы стремимся десериализовать объект или массив YAML в EnvironmentVariables:

public object ReadYaml(IParser parser, Type type)
{
    // We'll implement the deserialization logic very soon
    return new EnvironmentVariables();
}

Третий метод, WriteYaml, используется для сериализации объекта .NET обратно в документ YAML. Для наших целей мы сериализуем объект EnvironmentVariables в формат YAML:

public void WriteYaml(IEmitter emitter, object? value, Type type)
{
    // We'll implement the serialization logic very soon
    var dict = (EnvironmentVariables)value!;
}

Теперь, когда мы обрисовали структуру нашего пользовательского сериализатора, давайте углубимся в логику десериализации. Учитывая, что схема Docker Compose YAML для переменных среды поддерживает как формат объекта, так и формат строкового массива, мы можем оценить первый токен YAML, чтобы выяснить, отмечает ли он начало объекта YAML (MappingStart) или массива YAML (SequenceStart):

public object? ReadYaml(IParser parser, Type type)
{
    if (parser.TryConsume<MappingStart>(out _))
    {
        return ParseMapping(parser); // We're parsing a YAML object
    }

    if (parser.TryConsume<SequenceStart>(out _))
    {
        return ParseSequence(parser); // We're parsing a YAML array
    }

    throw new InvalidOperationException("Expected a YAML object or array");
}

Метод TryConsume, как следует из названия, пытается использовать токен YAML из документа. Если токен имеет ожидаемый тип, он используется, и метод возвращает значение true. Если нет, метод возвращает false, и анализатор не движется вперед. Давайте реализуем два метода синтаксического анализа:

private static EnvironmentVariables ParseMapping(IParser parser)
{
    var envvars = new EnvironmentVariables();

    // Read all the key-value pairs until we reached the end of the YAML object
    while (!parser.Accept<MappingEnd>(out _))
    {
        var key = parser.Consume<Scalar>();
        var value = parser.Consume<Scalar>();
        envvars[key.Value] = value.Value;
    }

    // Consume the mapping end token
    parser.MoveNext();
    return envvars;
}

// Regex that parses a key-value pair in the array format (e.g. "FOO=BAR" or "FOO")
private static readonly Regex EnvironmentVariableLineRegex = new Regex("^(?<key>[^=]*)(=(?<value>.*))?$", RegexOptions.Compiled);

private static EnvironmentVariables ParseSequence(IParser parser)
{
    var envvars = new EnvironmentVariables();

    // Read all the array values until we reach the end of the YAML array
    while (!parser.Accept<SequenceEnd>(out _))
    {
        var scalar = parser.Consume<Scalar>();

        if (EnvironmentVariableLineRegex.Match(scalar.Value) is { Success: true } match)
        {
            var key = match.Groups["key"].Value;
            var value = match.Groups["value"].Success ? match.Groups["value"].Value : string.Empty;
            envvars[key] = value;
        }
        else
        {
            throw new InvalidOperationException("Invalid key value mapping: " + scalar.Value);
        }
    }

    // Consume the mapping end token
    parser.MoveNext();
    return envvars;
}

Имея эти методы, мы можем поддерживать как форматы объектов, так и форматы массивов. Логика десериализации теперь завершена. Далее давайте займемся логикой сериализации. Наша цель — сериализовать объект EnvironmentVariables в формат YAML. Метод Emit можно использовать для создания токенов YAML:

private static readonly char[] KeyCharactersThatRequireQuotes = { ' ', '/', '\\', '~', ':', '$', '{', '}' };

public void WriteYaml(IEmitter emitter, object? value, Type type)
{
    var envvars = (EnvironmentVariables)value!;

    // We start a new YAML object
    emitter.Emit(new MappingStart(AnchorName.Empty, TagName.Empty, isImplicit: true, MappingStyle.Block));

    foreach (var entry in envvars)
    {
        // We try to determine if the value needs to be quoted if it contains special characters
        var keyScalar = entry.Key.IndexOfAny(KeyCharactersThatRequireQuotes) >= 0
            ? new Scalar(AnchorName.Empty, TagName.Empty, entry.Key, ScalarStyle.DoubleQuoted, isPlainImplicit: false, isQuotedImplicit: true)
            : new Scalar(AnchorName.Empty, TagName.Empty, entry.Key, ScalarStyle.Plain, isPlainImplicit: true, isQuotedImplicit: false);

        // Write the key, then the value
        emitter.Emit(keyScalar);
        emitter.Emit(new Scalar(AnchorName.Empty, TagName.Empty, entry.Value, ScalarStyle.DoubleQuoted, isPlainImplicit: false, isQuotedImplicit: true));
    }

    // We end the YAML object
    emitter.Emit(new MappingEnd());
}

С помощью этих методов мы создали собственный сериализатор и десериализатор YAML для типа EnvironmentVariables. Впоследствии мы можем использовать его для десериализации документа YAML в объект EnvironmentVariables:

var envvarTypeConverter = new EnvironmentVariablesTypeConverter();

var deserializer = new DeserializerBuilder()
    .WithTypeConverter(envvarTypeConverter)
    .IgnoreUnmatchedProperties() // don't throw an exception if there are unknown properties
    .Build();

var serializer = new SerializerBuilder()
    .WithTypeConverter(envvarTypeConverter)
    .DisableAliases() // don't use anchors and aliases (references to identical objects)
    .Build();

// Returns:
// environment:
//   foo: "bar"
var yamlText = serializer.Serialize(new DockerComposeService
{
    Environment = new EnvironmentVariables
    {
        ["foo"] = "bar"
    }
});

// 
var yamlObj = deserializer.Deserialize<DockerComposeService>(yamlText);

Источник:

Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу