|
@@ -1,327 +1,101 @@
|
|
|
-using System.Collections.Concurrent;
|
|
|
|
|
using System.Collections.Specialized;
|
|
using System.Collections.Specialized;
|
|
|
using RackPeek.Domain.Resources;
|
|
using RackPeek.Domain.Resources;
|
|
|
using RackPeek.Domain.Resources.Hardware.Models;
|
|
using RackPeek.Domain.Resources.Hardware.Models;
|
|
|
using RackPeek.Domain.Resources.Services;
|
|
using RackPeek.Domain.Resources.Services;
|
|
|
using RackPeek.Domain.Resources.SystemResources;
|
|
using RackPeek.Domain.Resources.SystemResources;
|
|
|
|
|
+using YamlDotNet.Core;
|
|
|
using YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization;
|
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
|
|
|
|
namespace RackPeek.Yaml;
|
|
namespace RackPeek.Yaml;
|
|
|
|
|
|
|
|
-public sealed class YamlResourceCollection(bool watch) : IDisposable
|
|
|
|
|
|
|
+public sealed class YamlResourceCollection(string filePath)
|
|
|
{
|
|
{
|
|
|
- private static readonly TimeSpan ReloadDebounce = TimeSpan.FromMilliseconds(300);
|
|
|
|
|
|
|
+ private readonly Lock _fileLock = new();
|
|
|
|
|
+ private readonly List<Resource> _resources = LoadFromFile(filePath);
|
|
|
|
|
|
|
|
- private readonly object _sync = new();
|
|
|
|
|
|
|
+ public IReadOnlyList<Hardware> HardwareResources =>
|
|
|
|
|
+ _resources.OfType<Hardware>().ToList();
|
|
|
|
|
|
|
|
- private readonly List<ResourceEntry> _entries = [];
|
|
|
|
|
- private readonly List<string> _knownFiles = [];
|
|
|
|
|
- private readonly ConcurrentDictionary<string, DateTime> _reloadQueue = [];
|
|
|
|
|
- private readonly bool _watch = watch;
|
|
|
|
|
- private readonly Dictionary<string, FileSystemWatcher> _watchers = [];
|
|
|
|
|
|
|
+ public IReadOnlyList<SystemResource> SystemResources =>
|
|
|
|
|
+ _resources.OfType<SystemResource>().ToList();
|
|
|
|
|
|
|
|
- public IReadOnlyList<string> SourceFiles
|
|
|
|
|
- {
|
|
|
|
|
- get
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- return _knownFiles.ToList();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public IReadOnlyList<Hardware> HardwareResources
|
|
|
|
|
- {
|
|
|
|
|
- get
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- return _entries
|
|
|
|
|
- .Select(e => e.Resource)
|
|
|
|
|
- .OfType<Hardware>()
|
|
|
|
|
- .ToList();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public IReadOnlyList<SystemResource> SystemResources
|
|
|
|
|
- {
|
|
|
|
|
- get
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- return _entries
|
|
|
|
|
- .Select(e => e.Resource)
|
|
|
|
|
- .OfType<SystemResource>()
|
|
|
|
|
- .ToList();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public IReadOnlyList<Service> ServiceResources
|
|
|
|
|
- {
|
|
|
|
|
- get
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- return _entries
|
|
|
|
|
- .Select(e => e.Resource)
|
|
|
|
|
- .OfType<Service>()
|
|
|
|
|
- .ToList();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- public void Dispose()
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- foreach (var watcher in _watchers.Values)
|
|
|
|
|
- watcher.Dispose();
|
|
|
|
|
-
|
|
|
|
|
- _watchers.Clear();
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
- // Loading
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
-
|
|
|
|
|
- public void LoadFiles(IEnumerable<string> filePaths)
|
|
|
|
|
- {
|
|
|
|
|
- foreach (var file in filePaths)
|
|
|
|
|
- {
|
|
|
|
|
- TrackFile(file);
|
|
|
|
|
- LoadFile(file);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ public IReadOnlyList<Service> ServiceResources =>
|
|
|
|
|
+ _resources.OfType<Service>().ToList();
|
|
|
|
|
|
|
|
- public void Load(string yaml, string file)
|
|
|
|
|
- {
|
|
|
|
|
- TrackFile(file);
|
|
|
|
|
-
|
|
|
|
|
- var newEntries = Deserialize(yaml)
|
|
|
|
|
- .Where(r => r != null)
|
|
|
|
|
- .Select(r => new ResourceEntry(r!, file))
|
|
|
|
|
- .ToList();
|
|
|
|
|
-
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- RemoveEntriesFromFile(file);
|
|
|
|
|
- _entries.AddRange(newEntries);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void LoadFile(string file)
|
|
|
|
|
- {
|
|
|
|
|
- var yaml = File.Exists(file)
|
|
|
|
|
- ? SafeReadAllText(file)
|
|
|
|
|
- : string.Empty;
|
|
|
|
|
-
|
|
|
|
|
- var newEntries = Deserialize(yaml)
|
|
|
|
|
- .Where(r => r != null)
|
|
|
|
|
- .Select(r => new ResourceEntry(r!, file))
|
|
|
|
|
- .ToList();
|
|
|
|
|
-
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- RemoveEntriesFromFile(file);
|
|
|
|
|
- _entries.AddRange(newEntries);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
- // Watching
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
-
|
|
|
|
|
- private void TrackFile(string file)
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- if (!_knownFiles.Contains(file))
|
|
|
|
|
- _knownFiles.Add(file);
|
|
|
|
|
|
|
+ // --- CRUD ---
|
|
|
|
|
|
|
|
- var directory = Path.GetDirectoryName(file);
|
|
|
|
|
- if (directory == null || !_watch || _watchers.ContainsKey(directory))
|
|
|
|
|
- return;
|
|
|
|
|
-
|
|
|
|
|
- var watcher = new FileSystemWatcher(directory)
|
|
|
|
|
- {
|
|
|
|
|
- EnableRaisingEvents = true,
|
|
|
|
|
- NotifyFilter = NotifyFilters.LastWrite
|
|
|
|
|
- | NotifyFilters.FileName
|
|
|
|
|
- | NotifyFilters.Size
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- watcher.Changed += OnFileChanged;
|
|
|
|
|
- watcher.Created += OnFileChanged;
|
|
|
|
|
- watcher.Deleted += OnFileChanged;
|
|
|
|
|
- watcher.Renamed += OnFileRenamed;
|
|
|
|
|
-
|
|
|
|
|
- _watchers[directory] = watcher;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void OnFileChanged(object sender, FileSystemEventArgs e)
|
|
|
|
|
|
|
+ public void Add(Resource resource)
|
|
|
{
|
|
{
|
|
|
- lock (_sync)
|
|
|
|
|
|
|
+ UpdateWithLock(list =>
|
|
|
{
|
|
{
|
|
|
- if (!_knownFiles.Contains(e.FullPath))
|
|
|
|
|
- return;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
|
|
|
|
|
+ throw new InvalidOperationException($"'{resource.Name}' already exists.");
|
|
|
|
|
|
|
|
- QueueReload(e.FullPath);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void OnFileRenamed(object sender, RenamedEventArgs e)
|
|
|
|
|
- {
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- if (_knownFiles.Contains(e.OldFullPath))
|
|
|
|
|
- {
|
|
|
|
|
- RemoveEntriesFromFile(e.OldFullPath);
|
|
|
|
|
- _knownFiles.Remove(e.OldFullPath);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- if (_knownFiles.Contains(e.FullPath))
|
|
|
|
|
- QueueReload(e.FullPath);
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void QueueReload(string file)
|
|
|
|
|
- {
|
|
|
|
|
- _reloadQueue[file] = DateTime.UtcNow;
|
|
|
|
|
-
|
|
|
|
|
- Task.Delay(ReloadDebounce).ContinueWith(_ =>
|
|
|
|
|
- {
|
|
|
|
|
- if (_reloadQueue.TryGetValue(file, out var timestamp) &&
|
|
|
|
|
- DateTime.UtcNow - timestamp >= ReloadDebounce)
|
|
|
|
|
- {
|
|
|
|
|
- _reloadQueue.TryRemove(file, out var _);
|
|
|
|
|
- LoadFile(file);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ resource.Kind = GetKind(resource);
|
|
|
|
|
+ list.Add(resource);
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
- // CRUD
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
-
|
|
|
|
|
- public void Add(Resource resource, string sourceFile)
|
|
|
|
|
- {
|
|
|
|
|
- TrackFile(sourceFile);
|
|
|
|
|
-
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- _entries.Add(new ResourceEntry(resource, sourceFile));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
public void Update(Resource resource)
|
|
public void Update(Resource resource)
|
|
|
{
|
|
{
|
|
|
- lock (_sync)
|
|
|
|
|
|
|
+ UpdateWithLock(list =>
|
|
|
{
|
|
{
|
|
|
- var existing = _entries.FirstOrDefault(e =>
|
|
|
|
|
- e.Resource.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
-
|
|
|
|
|
- if (existing == null)
|
|
|
|
|
- throw new InvalidOperationException($"Resource '{resource.Name}' not found.");
|
|
|
|
|
-
|
|
|
|
|
- _entries.Remove(existing);
|
|
|
|
|
- _entries.Add(new ResourceEntry(resource, existing.SourceFile));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
+ if (index == -1) throw new InvalidOperationException("Not found.");
|
|
|
|
|
+ list[index] = resource;
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public void Delete(string name)
|
|
public void Delete(string name)
|
|
|
{
|
|
{
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- var existing = _entries.FirstOrDefault(e =>
|
|
|
|
|
- e.Resource.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
-
|
|
|
|
|
- if (existing == null)
|
|
|
|
|
- throw new InvalidOperationException($"Resource '{name}' not found.");
|
|
|
|
|
-
|
|
|
|
|
- _entries.Remove(existing);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ UpdateWithLock(list => { list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); });
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- public Resource? GetByName(string name)
|
|
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private void UpdateWithLock(Action<List<Resource>> action)
|
|
|
{
|
|
{
|
|
|
- lock (_sync)
|
|
|
|
|
- {
|
|
|
|
|
- return _entries
|
|
|
|
|
- .Select(e => e.Resource)
|
|
|
|
|
- .FirstOrDefault(r =>
|
|
|
|
|
- r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private void RemoveEntriesFromFile(string file)
|
|
|
|
|
- {
|
|
|
|
|
- _entries.RemoveAll(e => e.SourceFile == file);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
- // Serialization
|
|
|
|
|
- // ----------------------------
|
|
|
|
|
-
|
|
|
|
|
- public void SaveAll()
|
|
|
|
|
- {
|
|
|
|
|
- List<string> files;
|
|
|
|
|
- List<ResourceEntry> snapshot;
|
|
|
|
|
-
|
|
|
|
|
- lock (_sync)
|
|
|
|
|
|
|
+ lock (_fileLock)
|
|
|
{
|
|
{
|
|
|
- files = _knownFiles.ToList();
|
|
|
|
|
- snapshot = _entries.ToList();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ action(_resources);
|
|
|
|
|
+
|
|
|
|
|
+ var serializer = new SerializerBuilder()
|
|
|
|
|
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
|
|
+ .Build();
|
|
|
|
|
|
|
|
- foreach (var file in files)
|
|
|
|
|
- {
|
|
|
|
|
- var resources = snapshot
|
|
|
|
|
- .Where(e => e.SourceFile == file)
|
|
|
|
|
- .Select(e => e.Resource);
|
|
|
|
|
|
|
+ var payload = new OrderedDictionary
|
|
|
|
|
+ {
|
|
|
|
|
+ ["resources"] = _resources.Select(SerializeResource).ToList()
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- SaveToFile(file, resources);
|
|
|
|
|
|
|
+ File.WriteAllText(filePath, serializer.Serialize(payload));
|
|
|
|
|
+
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static void SaveToFile(string filePath, IEnumerable<Resource> resources)
|
|
|
|
|
|
|
+ private string GetKind(Resource resource)
|
|
|
{
|
|
{
|
|
|
- var serializer = new SerializerBuilder()
|
|
|
|
|
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
|
|
- .Build();
|
|
|
|
|
-
|
|
|
|
|
- var payload = new OrderedDictionary
|
|
|
|
|
|
|
+ return resource switch
|
|
|
{
|
|
{
|
|
|
- ["resources"] = resources.Select(SerializeResource).ToList()
|
|
|
|
|
|
|
+ Server => "Server",
|
|
|
|
|
+ Switch => "Switch",
|
|
|
|
|
+ Firewall => "Firewall",
|
|
|
|
|
+ Router => "Router",
|
|
|
|
|
+ Desktop => "Desktop",
|
|
|
|
|
+ Laptop => "Laptop",
|
|
|
|
|
+ AccessPoint => "AccessPoint",
|
|
|
|
|
+ Ups => "Ups",
|
|
|
|
|
+ SystemResource => "System",
|
|
|
|
|
+ Service => "Service",
|
|
|
|
|
+ _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
|
|
|
};
|
|
};
|
|
|
-
|
|
|
|
|
- File.WriteAllText(filePath, serializer.Serialize(payload));
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private static OrderedDictionary SerializeResource(Resource resource)
|
|
|
|
|
|
|
+ private OrderedDictionary SerializeResource(Resource resource)
|
|
|
{
|
|
{
|
|
|
var map = new OrderedDictionary
|
|
var map = new OrderedDictionary
|
|
|
{
|
|
{
|
|
|
- ["kind"] = resource switch
|
|
|
|
|
- {
|
|
|
|
|
- Server => "Server",
|
|
|
|
|
- Switch => "Switch",
|
|
|
|
|
- Firewall => "Firewall",
|
|
|
|
|
- Router => "Router",
|
|
|
|
|
- Desktop => "Desktop",
|
|
|
|
|
- Laptop => "Laptop",
|
|
|
|
|
- AccessPoint => "AccessPoint",
|
|
|
|
|
- Ups => "Ups",
|
|
|
|
|
- SystemResource => "System",
|
|
|
|
|
- Service => "Service",
|
|
|
|
|
- _ => throw new InvalidOperationException($"Unknown resource type: {resource.GetType().Name}")
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ ["kind"] = GetKind(resource)
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
var serializer = new SerializerBuilder()
|
|
var serializer = new SerializerBuilder()
|
|
@@ -332,7 +106,7 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
|
|
|
|
|
|
|
|
var props = new DeserializerBuilder()
|
|
var props = new DeserializerBuilder()
|
|
|
.Build()
|
|
.Build()
|
|
|
- .Deserialize<Dictionary<string, object?>>(yaml) ?? new();
|
|
|
|
|
|
|
+ .Deserialize<Dictionary<string, object?>>(yaml);
|
|
|
|
|
|
|
|
foreach (var (key, value) in props)
|
|
foreach (var (key, value) in props)
|
|
|
if (key != "kind")
|
|
if (key != "kind")
|
|
@@ -340,75 +114,58 @@ public sealed class YamlResourceCollection(bool watch) : IDisposable
|
|
|
|
|
|
|
|
return map;
|
|
return map;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- private static List<Resource> Deserialize(string yaml)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ private static List<Resource> LoadFromFile(string filePath)
|
|
|
{
|
|
{
|
|
|
- if (string.IsNullOrWhiteSpace(yaml))
|
|
|
|
|
- return [];
|
|
|
|
|
|
|
+ // 1. Robustness: Handle missing or empty files immediately
|
|
|
|
|
+ if (!File.Exists(filePath)) return new List<Resource>();
|
|
|
|
|
+ var yaml = File.ReadAllText(filePath);
|
|
|
|
|
+ if (string.IsNullOrWhiteSpace(yaml)) return new List<Resource>();
|
|
|
|
|
|
|
|
var deserializer = new DeserializerBuilder()
|
|
var deserializer = new DeserializerBuilder()
|
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
.WithCaseInsensitivePropertyMatching()
|
|
.WithCaseInsensitivePropertyMatching()
|
|
|
.WithTypeConverter(new StorageSizeYamlConverter())
|
|
.WithTypeConverter(new StorageSizeYamlConverter())
|
|
|
|
|
+ // 2. The "Pragmatic" Fix: Automatically choose the class based on the "kind" key
|
|
|
|
|
+ .WithTypeDiscriminatingNodeDeserializer(options =>
|
|
|
|
|
+ {
|
|
|
|
|
+ options.AddKeyValueTypeDiscriminator<Resource>("kind", new Dictionary<string, Type>
|
|
|
|
|
+ {
|
|
|
|
|
+ { Server.KindLabel, typeof(Server) },
|
|
|
|
|
+ { Switch.KindLabel, typeof(Switch) },
|
|
|
|
|
+ { Firewall.KindLabel, typeof(Firewall) },
|
|
|
|
|
+ { Router.KindLabel, typeof(Router) },
|
|
|
|
|
+ { Desktop.KindLabel, typeof(Desktop) },
|
|
|
|
|
+ { Laptop.KindLabel, typeof(Laptop) },
|
|
|
|
|
+ { AccessPoint.KindLabel, typeof(AccessPoint) },
|
|
|
|
|
+ { Ups.KindLabel, typeof(Ups) },
|
|
|
|
|
+ { SystemResource.KindLabel, typeof(SystemResource) },
|
|
|
|
|
+ { Service.KindLabel, typeof(Service) }
|
|
|
|
|
+ });
|
|
|
|
|
+ })
|
|
|
.Build();
|
|
.Build();
|
|
|
|
|
|
|
|
- var raw = deserializer.Deserialize<
|
|
|
|
|
- Dictionary<string, List<Dictionary<string, object>>>>(yaml);
|
|
|
|
|
-
|
|
|
|
|
- if (raw == null || !raw.TryGetValue("resources", out var items))
|
|
|
|
|
- return [];
|
|
|
|
|
-
|
|
|
|
|
- var resources = new List<Resource>();
|
|
|
|
|
-
|
|
|
|
|
- foreach (var item in items)
|
|
|
|
|
|
|
+ try
|
|
|
{
|
|
{
|
|
|
- if (!item.TryGetValue("kind", out var kindObj) || kindObj == null)
|
|
|
|
|
- continue;
|
|
|
|
|
-
|
|
|
|
|
- var kind = kindObj.ToString();
|
|
|
|
|
- var typedYaml = new SerializerBuilder()
|
|
|
|
|
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
|
|
- .Build()
|
|
|
|
|
- .Serialize(item);
|
|
|
|
|
-
|
|
|
|
|
- Resource resource = kind switch
|
|
|
|
|
- {
|
|
|
|
|
- "Server" => deserializer.Deserialize<Server>(typedYaml),
|
|
|
|
|
- "Switch" => deserializer.Deserialize<Switch>(typedYaml),
|
|
|
|
|
- "Firewall" => deserializer.Deserialize<Firewall>(typedYaml),
|
|
|
|
|
- "Router" => deserializer.Deserialize<Router>(typedYaml),
|
|
|
|
|
- "Desktop" => deserializer.Deserialize<Desktop>(typedYaml),
|
|
|
|
|
- "Laptop" => deserializer.Deserialize<Laptop>(typedYaml),
|
|
|
|
|
- "AccessPoint" => deserializer.Deserialize<AccessPoint>(typedYaml),
|
|
|
|
|
- "Ups" => deserializer.Deserialize<Ups>(typedYaml),
|
|
|
|
|
- "System" => deserializer.Deserialize<SystemResource>(typedYaml),
|
|
|
|
|
- "Service" => deserializer.Deserialize<Service>(typedYaml),
|
|
|
|
|
- _ => null
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- if (resource != null)
|
|
|
|
|
- resources.Add(resource);
|
|
|
|
|
|
|
+ // 3. Deserialize into a wrapper class to handle the "resources:" root key
|
|
|
|
|
+ var root = deserializer.Deserialize<YamlRoot>(yaml);
|
|
|
|
|
+ return root?.Resources ?? new List<Resource>();
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return resources;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private static string SafeReadAllText(string file)
|
|
|
|
|
- {
|
|
|
|
|
- for (var i = 0; i < 5; i++)
|
|
|
|
|
|
|
+ catch (YamlException)
|
|
|
{
|
|
{
|
|
|
- try
|
|
|
|
|
- {
|
|
|
|
|
- return File.ReadAllText(file);
|
|
|
|
|
- }
|
|
|
|
|
- catch (IOException)
|
|
|
|
|
- {
|
|
|
|
|
- Thread.Sleep(50);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Handle malformed YAML here or return empty list
|
|
|
|
|
+ return new List<Resource>();
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- return string.Empty;
|
|
|
|
|
|
|
+ // Simple wrapper class to match the YAML structure
|
|
|
|
|
+ private class YamlRoot
|
|
|
|
|
+ {
|
|
|
|
|
+ public List<Resource>? Resources { get; set; }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private sealed record ResourceEntry(Resource Resource, string SourceFile);
|
|
|
|
|
-}
|
|
|
|
|
|
|
+ public Resource? GetByName(string name)
|
|
|
|
|
+ {
|
|
|
|
|
+ return _resources.FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|