Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 31 additions & 16 deletions src/NetEscapades.Configuration.Yaml/YamlConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using Microsoft.Extensions.FileProviders;
using NetEscapades.Configuration.Yaml;
using YamlDotNet.Core;
using YamlDotNet.Serialization;

namespace Microsoft.Extensions.Configuration
{
Expand All @@ -19,52 +20,65 @@ public static class YamlConfigurationExtensions
/// Adds the YAML configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="configureDeserializer">The action that configures the YAML deserializer.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, Action<DeserializerBuilder> configureDeserializer = null)
{
return AddYamlFile(builder, provider: null, path: path, optional: false, reloadOnChange: false, configureDeserializer: configureDeserializer);
}

/// <summary>
/// Adds the YAML configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path)
{
return AddYamlFile(builder, provider: null, path: path, optional: false, reloadOnChange: false);
return AddYamlFile(builder, provider: null, path: path, optional: false, reloadOnChange: false, configureDeserializer: null);
}

/// <summary>
/// Adds the YAML configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="optional">Whether the file is optional.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional)
{
return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false);
return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: false, configureDeserializer: null);
}

/// <summary>
/// Adds the YAML configuration provider at <paramref name="path"/> to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="path">Path relative to the base path stored in
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="optional">Whether the file is optional.</param>
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange)
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, string path, bool optional, bool reloadOnChange, Action<DeserializerBuilder> configureDeserializer)
{
return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange);
return AddYamlFile(builder, provider: null, path: path, optional: optional, reloadOnChange: reloadOnChange, configureDeserializer: configureDeserializer);
}

/// <summary>
/// Adds a YAML configuration source to <paramref name="builder"/>.
/// </summary>
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="provider">The <see cref="IFileProvider"/> to use to access the file.</param>
/// <param name="path">Path relative to the base path stored in
/// <param name="path">Path relative to the base path stored in
/// <see cref="IConfigurationBuilder.Properties"/> of <paramref name="builder"/>.</param>
/// <param name="optional">Whether the file is optional.</param>
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the file changes.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange)
public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder builder, IFileProvider provider, string path, bool optional, bool reloadOnChange, Action<DeserializerBuilder> configureDeserializer)
{
if (builder == null)
{
Expand All @@ -74,17 +88,18 @@ public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder build
{
throw new ArgumentException(Resources.FormatError_InvalidFilePath(), nameof(path));
}

return builder.AddYamlFile(s =>
{
s.FileProvider = provider;
s.Path = path;
s.Optional = optional;
s.ReloadOnChange = reloadOnChange;
s.ConfigureDeserializer = configureDeserializer;
s.ResolveFileProvider();
});
}

/// <summary>
/// Adds a YAML configuration source to <paramref name="builder"/>.
/// </summary>
Expand All @@ -100,22 +115,22 @@ public static IConfigurationBuilder AddYamlFile(this IConfigurationBuilder build
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
/// <param name="stream">The <see cref="Stream"/> to read the yaml configuration data from.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddYamlStream(this IConfigurationBuilder builder, Stream stream)
public static IConfigurationBuilder AddYamlStream(this IConfigurationBuilder builder, Stream stream, Action<DeserializerBuilder> configureDeserializer = null)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}

var data = ReadStream(stream);
var data = ReadStream(stream, configureDeserializer);

return builder.Add<StaticConfigurationSource>(s => s.Data = data);

static IDictionary<string, string> ReadStream(Stream s)
static IDictionary<string, string> ReadStream(Stream s, Action<DeserializerBuilder> configureDeserializer)
{
try
{
return new YamlConfigurationStreamParser().Parse(s);
return new YamlConfigurationStreamParser(configureDeserializer).Parse(s);
}
catch (YamlException e)
{
Expand All @@ -124,4 +139,4 @@ static IDictionary<string, string> ReadStream(Stream s)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ namespace NetEscapades.Configuration.Yaml
/// </summary>
public class YamlConfigurationProvider : FileConfigurationProvider
{
public YamlConfigurationProvider(YamlConfigurationSource source) : base(source) { }
private readonly YamlConfigurationSource _source;

public YamlConfigurationProvider(YamlConfigurationSource source) : base(source)
{
_source = source;
}

public override void Load(Stream stream)
{
var parser = new YamlConfigurationStreamParser();
var parser = new YamlConfigurationStreamParser(_source.ConfigureDeserializer);
try
{
Data = parser.Parse(stream);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System;
using Microsoft.Extensions.Configuration;
using YamlDotNet.Serialization;

namespace NetEscapades.Configuration.Yaml
{
Expand All @@ -7,10 +9,12 @@ namespace NetEscapades.Configuration.Yaml
/// </summary>
public class YamlConfigurationSource : FileConfigurationSource
{
public Action<DeserializerBuilder> ConfigureDeserializer { get; set; }

public override IConfigurationProvider Build(IConfigurationBuilder builder)
{
EnsureDefaults(builder);
return new YamlConfigurationProvider(this);
}
}
}
}
141 changes: 71 additions & 70 deletions src/NetEscapades.Configuration.Yaml/YamlConfigurationStreamParser.cs
Original file line number Diff line number Diff line change
@@ -1,132 +1,133 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Microsoft.Extensions.Configuration;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Core;
using YamlDotNet.Serialization;

namespace NetEscapades.Configuration.Yaml
{
internal class YamlConfigurationStreamParser
{
private readonly Action<DeserializerBuilder> _configureDeserializer;
private readonly IDictionary<string, string> _data = new SortedDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
private readonly Stack<string> _context = new Stack<string>();
private string _currentPath;

public YamlConfigurationStreamParser(Action<DeserializerBuilder> configureDeserializer)
{
_configureDeserializer = configureDeserializer;
}

public IDictionary<string, string> Parse(Stream input)
{
_data.Clear();
_context.Clear();

// https://dotnetfiddle.net/rrR2Bb
var yaml = new YamlStream();
yaml.Load(new StreamReader(input, detectEncodingFromByteOrderMarks: true));
using var reader = new StreamReader(input, detectEncodingFromByteOrderMarks: true);
var parser = new Parser(reader);
var document = CreateDeserializer().Deserialize(parser);

if (yaml.Documents.Any())
if (document is not IDictionary<object, object> documentDict)
{
var mapping = (YamlMappingNode)yaml.Documents[0].RootNode;

// The document node is a mapping node
VisitYamlMappingNode(mapping);
throw new FormatException("Root document must be a dictionary, but got " + document?.GetType().FullName);
}

VisitMap(documentDict);

return _data;
}

private void VisitYamlNodePair(KeyValuePair<YamlNode, YamlNode> yamlNodePair)
private IDeserializer CreateDeserializer()
{
var context = ((YamlScalarNode)yamlNodePair.Key).Value;
VisitYamlNode(context, yamlNodePair.Value);
var builder = new DeserializerBuilder();
builder.WithAttemptingUnquotedStringTypeDeserialization();
_configureDeserializer?.Invoke(builder);
return builder.Build();
}

private void VisitYamlNode(string context, YamlNode node)
private void VisitMap(IDictionary<object, object> map)
{
if (node is YamlScalarNode scalarNode)
{
VisitYamlScalarNode(context, scalarNode);
}
if (node is YamlMappingNode mappingNode)
{
VisitYamlMappingNode(context, mappingNode);
}
if (node is YamlSequenceNode sequenceNode)
{
VisitYamlSequenceNode(context, sequenceNode);
}
}
var isEmpty = true;

private void VisitYamlScalarNode(string context, YamlScalarNode yamlValue)
{
//a node with a single 1-1 mapping
EnterContext(context);
var currentKey = _currentPath;

if (_data.ContainsKey(currentKey))
foreach (var entry in map)
{
throw new FormatException(Resources.FormatError_KeyIsDuplicated(currentKey));
isEmpty = false;
EnterContext(entry.Key.ToString()!);
Visit(entry.Value);
ExitContext();
}

_data[currentKey] = IsNullValue(yamlValue) ? null : yamlValue.Value;
ExitContext();
SetNullIfElementIsEmpty(isEmpty);
}

private void VisitYamlMappingNode(YamlMappingNode node)
private void Visit(object value)
{
foreach (var yamlNodePair in node.Children)
switch (value)
{
VisitYamlNodePair(yamlNodePair);
case IDictionary<object, object> map:
VisitMap(map);
break;
case IList<object> sequence:
VisitSequence(sequence);
break;
default:
VisitScalar(value);
break;
}
}

private void VisitYamlMappingNode(string context, YamlMappingNode yamlValue)
private void VisitScalar(object scalar)
{
//a node with an associated sub-document
EnterContext(context);

VisitYamlMappingNode(yamlValue);
var currentKey = _context.Peek();
if (_data.ContainsKey(currentKey))
{
throw new FormatException(Resources.FormatError_KeyIsDuplicated(currentKey));
}

ExitContext();
_data[currentKey] = scalar switch
{
null => "",
bool boolean => boolean ? "true" : "false",
string str => str,
// the only remaining type is a number, but there is not a common base class "Number". the best thing to use is IFormattable, which is also used to ensure numbers are formatted correctly
IFormattable formattable => formattable.ToString("G", CultureInfo.InvariantCulture),
_ => throw new FormatException($"Unsupported scalar type: {scalar.GetType().Name}")
};
}

private void VisitYamlSequenceNode(string context, YamlSequenceNode yamlValue)
private void VisitSequence(IList<object> sequence)
{
//a node with an associated list
EnterContext(context);
var i = 0;

VisitYamlSequenceNode(yamlValue);

ExitContext();
}

private void VisitYamlSequenceNode(YamlSequenceNode node)
{
for (int i = 0; i < node.Children.Count; i++)
for (; i < sequence.Count; i++)
{
VisitYamlNode(i.ToString(), node.Children[i]);
EnterContext(i.ToString(CultureInfo.InvariantCulture));
Visit(sequence[i]);
ExitContext();
}

SetNullIfElementIsEmpty(i == 0);
}

private void EnterContext(string context)
{
_context.Push(context);
_currentPath = ConfigurationPath.Combine(_context.Reverse());
_context.Push(_context.Count > 0 ?
_context.Peek() + ConfigurationPath.KeyDelimiter + context :
context);
}

private void ExitContext()
{
_context.Pop();
_currentPath = ConfigurationPath.Combine(_context.Reverse());
}

private bool IsNullValue(YamlScalarNode yamlValue)
private void SetNullIfElementIsEmpty(bool isEmpty)
{
return yamlValue.Style == YamlDotNet.Core.ScalarStyle.Plain
&& (
yamlValue.Value == "~"
|| yamlValue.Value == "null"
|| yamlValue.Value == "Null"
|| yamlValue.Value == "NULL"
);
if (isEmpty && _context.Count > 0)
{
_data[_context.Peek()] = null;
}
}
}
}
Loading