182 lines
5.8 KiB
C#
182 lines
5.8 KiB
C#
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<SequentialScheduleValidator> logger)
|
|
: ISequentialScheduleValidator
|
|
{
|
|
public async Task<bool> 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<IList<string>> 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<string> 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<object>();
|
|
foreach (YamlNode node in sequence.Children)
|
|
{
|
|
node.Accept(this);
|
|
array.Add(_currentValue);
|
|
}
|
|
|
|
_currentValue = array;
|
|
}
|
|
|
|
public void Visit(YamlMappingNode mapping)
|
|
{
|
|
Dictionary<string, object> dict = new(StringComparer.OrdinalIgnoreCase);
|
|
foreach (KeyValuePair<YamlNode, YamlNode> 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);
|
|
}
|
|
}
|