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); } }