203 lines
5.7 KiB
C#
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);
|
|
}
|
|
}
|