This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
mfgames-nitride-cil/src/MfGames.Nitride.Yaml/ParseYamlHeader.cs

203 lines
5.7 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using MfGames.Gallium;
using MfGames.Nitride.Contents;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace MfGames.Nitride.Yaml;
/// <summary>
/// An operation that parses the header of a text file and pulls out the
/// YAML header with a specific type.
/// </summary>
/// <typeparam name="TModel">The model that represents the header.</typeparam>
public class ParseYamlHeader<TModel> : OperationBase
where TModel : class, new()
{
private bool ignoreUnmatchedProperties;
private INamingConvention namingConvention;
public ParseYamlHeader()
{
this.namingConvention = CamelCaseNamingConvention.Instance;
this.ignoreUnmatchedProperties = true;
this.RemoveHeader = true;
}
public Func<Entity, string, Exception, ParseYamlHeaderErrorHandling>?
EntityError
{
get;
set;
}
/// <summary>
/// Gets or sets a value indicating whether the header should be removed
/// and the text content be removed.
/// </summary>
private bool RemoveHeader { get; set; }
/// <inheritdoc />
public override IEnumerable<Entity> Run(
IEnumerable<Entity> input,
CancellationToken cancellationToken = default)
{
// Set up the YAML parsing.
DeserializerBuilder builder =
new DeserializerBuilder().WithNamingConvention(
this.namingConvention);
if (this.ignoreUnmatchedProperties)
{
builder = builder.IgnoreUnmatchedProperties();
}
IDeserializer deserializer = builder.Build();
// Process through the files. We only care about the text ones
// and we'll put a default TModel in for those that don't have a
// header.
return input
.SelectEntity<ITextContent>(
(
entity,
content) => this.Parse(entity, content, deserializer));
}
public ParseYamlHeader<TModel> WithEntityError(
Func<Entity, string, Exception, ParseYamlHeaderErrorHandling>? value)
{
this.EntityError = value;
return this;
}
/// <summary>
/// Sets the flag if the module should ignore unmatched properties
/// which defaults to true (ignore).
/// </summary>
/// <param name="value">The new value.</param>
/// <returns>The module for chaining.</returns>
public ParseYamlHeader<TModel> WithIgnoreUnmatchedProperties(bool value)
{
this.ignoreUnmatchedProperties = value;
return this;
}
/// <summary>
/// Sets the naming convention to something other than the default
/// camel case.
/// </summary>
/// <param name="value">The new naming convention.</param>
/// <returns>The module for chaining.</returns>
public ParseYamlHeader<TModel> WithNamingConvention(INamingConvention value)
{
this.namingConvention = value;
return this;
}
public ParseYamlHeader<TModel> WithRemoveHeader(bool value)
{
this.RemoveHeader = value;
return this;
}
private Entity Parse(
Entity entity,
ITextContent content,
IDeserializer deserializer)
{
// Get the textual input from the stream.
using TextReader reader = content.GetReader();
// See if the first line is one that indicates a YAML header.
string? line = reader.ReadLine();
if (line != "---")
{
// This file doesn't have a YAML header, so add the default
// version of our model and move on.
return entity.Set(new TModel());
}
// Read the rest of the header until we get to the end of the
// header. If we get to the end of the file first, then we don't
// have a valid file and just return the default.
StringBuilder buffer = new();
buffer.AppendLine(line);
while ((line = reader.ReadLine()) != null)
{
// If have a separator, then we're done processing.
if (line == "---")
{
break;
}
// Read the next line into memory.
buffer.AppendLine(line);
}
// If the line is null, then we got to the end of the file without
// finding a proper header, so use a default and have no other
// changes.
if (line == null)
{
return entity.Set(new TModel());
}
// Pull out the model so we can append it later.
string yaml = buffer.ToString();
TModel? model;
try
{
model = deserializer.Deserialize<TModel>(yaml);
}
catch (Exception exception)
{
ParseYamlHeaderErrorHandling disposition =
this.EntityError?.Invoke(entity, yaml, exception)
?? ParseYamlHeaderErrorHandling.Throw;
if (disposition == ParseYamlHeaderErrorHandling.Ignore)
{
return entity;
}
throw;
}
// If we are not removing the header, then we're done.
if (!this.RemoveHeader)
{
return entity.Set(model);
}
// Read the rest of the reader into a new (reused) buffer so we can
// set the new text content to the text without the header.
buffer.Length = 0;
while ((line = reader.ReadLine()) != null)
{
buffer.AppendLine(line);
}
// Set the model and return it.
return entity.Set(model)
.SetTextContent(new StringTextContent(buffer.ToString()))
.Set(HasYamlModel.Instance);
}
}