using System.Globalization; using System.IO.Abstractions; using System.Text.Json; using ErsatzTV.Core; using ErsatzTV.Core.Interfaces.Scheduling; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Schema; using YamlDotNet.RepresentationModel; using JsonSerializer = System.Text.Json.JsonSerializer; using JsonSchemaNet = Json.Schema; namespace ErsatzTV.Infrastructure.Scheduling; public class SequentialScheduleValidator(IFileSystem fileSystem, ILogger logger) : ISequentialScheduleValidator { public async Task ValidateSchedule(string yaml, bool isImport) { try { string schemaFileName = GetSchemaPath(isImport); string schemaText = await fileSystem.File.ReadAllTextAsync(schemaFileName); var buildOptions = new JsonSchemaNet.BuildOptions { SchemaRegistry = new JsonSchemaNet.SchemaRegistry() }; JsonSchemaNet.JsonSchema schema = JsonSchemaNet.JsonSchema.FromText(schemaText, buildOptions); string jsonString = ConvertYamlToJsonString(yaml); JsonElement jsonElement = JsonElement.Parse(jsonString); JsonSchemaNet.EvaluationResults result = schema.Evaluate(jsonElement); if (!result.IsValid) { logger.LogWarning("Sequential schedule definition failed validation"); return false; } return true; } catch (Exception ex) { logger.LogWarning(ex, "Unexpected error while validating sequential schedule definition"); } return false; } public string ToJson(string yaml) { string jsonString = ConvertYamlToJsonString(yaml); var schedule = JObject.Parse(jsonString); string formatted = JsonConvert.SerializeObject(schedule, Formatting.Indented); string[] lines = formatted.Split('\n'); return string.Join('\n', lines.Select((line, index) => $"{index + 1,4}: {line}")); } // limited to 1000/hr, but only called manually from UI public async Task> GetValidationMessages(string yaml, bool isImport) { try { string schemaFileName = GetSchemaPath(isImport); using StreamReader sr = fileSystem.File.OpenText(schemaFileName); await using var reader = new JsonTextReader(sr); var schema = JSchema.Load(reader); string jsonString = ConvertYamlToJsonString(yaml); var schedule = JObject.Parse(jsonString); return schedule.IsValid(schema, out IList errorMessages) ? [] : errorMessages; } catch (Exception ex) { return [ex.Message]; } } private static string ConvertYamlToJsonString(string yaml) { using var textReader = new StringReader(yaml); var yamlStream = new YamlStream(); yamlStream.Load(textReader); var visitor = new YamlToJsonVisitor(); yamlStream.Accept(visitor); return JsonConvert.SerializeObject(JsonConvert.DeserializeObject(visitor.JsonString), Formatting.Indented); } private static string GetSchemaPath(bool isImport) { return Path.Combine( FileSystemLayout.ResourcesCacheFolder, isImport ? "sequential-schedule-import.schema.json" : "sequential-schedule.schema.json"); } private sealed class YamlToJsonVisitor : IYamlVisitor { private readonly JsonSerializerOptions _options = new() { WriteIndented = false }; private object _currentValue; public string JsonString => JsonSerializer.Serialize(_currentValue, _options); public void Visit(YamlScalarNode scalar) { string value = scalar.Value; if (string.IsNullOrEmpty(value)) { _currentValue = null; return; } // Try to parse in order of most specific to most general if (value.Equals("true", StringComparison.OrdinalIgnoreCase)) { _currentValue = true; return; } if (value.Equals("false", StringComparison.OrdinalIgnoreCase)) { _currentValue = false; return; } if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int intResult)) { _currentValue = intResult; return; } if (double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out double doubleResult)) { _currentValue = doubleResult; return; } _currentValue = value; } public void Visit(YamlSequenceNode sequence) { var array = new List(); foreach (YamlNode node in sequence.Children) { node.Accept(this); array.Add(_currentValue); } _currentValue = array; } public void Visit(YamlMappingNode mapping) { Dictionary dict = new(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair entry in mapping.Children) { string key = entry.Key switch { YamlScalarNode scalar => scalar.Value, _ => entry.Key.ToString() }; entry.Value.Accept(this); dict[key!] = _currentValue; } _currentValue = dict; } public void Visit(YamlDocument document) => document.RootNode.Accept(this); public void Visit(YamlStream stream) => stream.Documents[0].RootNode.Accept(this); } }