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;
///
/// An operation that parses the header of a text file and pulls out the
/// YAML header with a specific type.
///
/// The model that represents the header.
public class ParseYamlHeader : 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?
EntityError
{
get;
set;
}
///
/// Gets or sets a value indicating whether the header should be removed
/// and the text content be removed.
///
private bool RemoveHeader { get; set; }
///
public override IEnumerable Run(
IEnumerable 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(
(
entity,
content) => this.Parse(entity, content, deserializer));
}
public ParseYamlHeader WithEntityError(
Func? value)
{
this.EntityError = value;
return this;
}
///
/// Sets the flag if the module should ignore unmatched properties
/// which defaults to true (ignore).
///
/// The new value.
/// The module for chaining.
public ParseYamlHeader WithIgnoreUnmatchedProperties(bool value)
{
this.ignoreUnmatchedProperties = value;
return this;
}
///
/// Sets the naming convention to something other than the default
/// camel case.
///
/// The new naming convention.
/// The module for chaining.
public ParseYamlHeader WithNamingConvention(INamingConvention value)
{
this.namingConvention = value;
return this;
}
public ParseYamlHeader 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(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);
}
}