|
|
@@ -1,18 +1,34 @@
|
|
|
using System.Collections.Specialized;
|
|
|
+using RackPeek.Domain.Persistence;
|
|
|
+using RackPeek.Domain.Persistence.Yaml;
|
|
|
using RackPeek.Domain.Resources;
|
|
|
using RackPeek.Domain.Resources.Models;
|
|
|
using RackPeek.Domain.Resources.Services;
|
|
|
using RackPeek.Domain.Resources.SystemResources;
|
|
|
+using RackPeek.Yaml;
|
|
|
using YamlDotNet.Core;
|
|
|
using YamlDotNet.Serialization;
|
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
|
|
-namespace RackPeek.Yaml;
|
|
|
-
|
|
|
-public sealed class YamlResourceCollection(string filePath)
|
|
|
+public sealed class YamlResourceCollection : IResourceCollection
|
|
|
{
|
|
|
- private readonly Lock _fileLock = new();
|
|
|
- private readonly List<Resource> _resources = LoadFromFile(filePath);
|
|
|
+ private readonly string _filePath;
|
|
|
+ private readonly ITextFileStore _fileStore;
|
|
|
+ private readonly SemaphoreSlim _fileLock = new(1, 1);
|
|
|
+ private readonly List<Resource> _resources = new();
|
|
|
+
|
|
|
+ public YamlResourceCollection(string filePath, ITextFileStore fileStore)
|
|
|
+ {
|
|
|
+ _filePath = filePath;
|
|
|
+ _fileStore = fileStore;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task LoadAsync()
|
|
|
+ {
|
|
|
+ var loaded = await LoadFromFileAsync();
|
|
|
+ _resources.Clear();
|
|
|
+ _resources.AddRange(loaded);
|
|
|
+ }
|
|
|
|
|
|
public IReadOnlyList<Hardware> HardwareResources =>
|
|
|
_resources.OfType<Hardware>().ToList();
|
|
|
@@ -23,11 +39,12 @@ public sealed class YamlResourceCollection(string filePath)
|
|
|
public IReadOnlyList<Service> ServiceResources =>
|
|
|
_resources.OfType<Service>().ToList();
|
|
|
|
|
|
- // --- CRUD ---
|
|
|
+ public Resource? GetByName(string name) =>
|
|
|
+ _resources.FirstOrDefault(r =>
|
|
|
+ r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
|
|
|
|
|
- public void Add(Resource resource)
|
|
|
- {
|
|
|
- UpdateWithLock(list =>
|
|
|
+ public Task AddAsync(Resource resource) =>
|
|
|
+ UpdateWithLockAsync(list =>
|
|
|
{
|
|
|
if (list.Any(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase)))
|
|
|
throw new InvalidOperationException($"'{resource.Name}' already exists.");
|
|
|
@@ -35,27 +52,25 @@ public sealed class YamlResourceCollection(string filePath)
|
|
|
resource.Kind = GetKind(resource);
|
|
|
list.Add(resource);
|
|
|
});
|
|
|
- }
|
|
|
|
|
|
- public void Update(Resource resource)
|
|
|
- {
|
|
|
- UpdateWithLock(list =>
|
|
|
+ public Task UpdateAsync(Resource resource) =>
|
|
|
+ UpdateWithLockAsync(list =>
|
|
|
{
|
|
|
var index = list.FindIndex(r => r.Name.Equals(resource.Name, StringComparison.OrdinalIgnoreCase));
|
|
|
if (index == -1) throw new InvalidOperationException("Not found.");
|
|
|
+
|
|
|
+ resource.Kind = GetKind(resource);
|
|
|
list[index] = resource;
|
|
|
});
|
|
|
- }
|
|
|
-
|
|
|
- public void Delete(string name)
|
|
|
- {
|
|
|
- UpdateWithLock(list => { list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); });
|
|
|
- }
|
|
|
|
|
|
+ public Task DeleteAsync(string name) =>
|
|
|
+ UpdateWithLockAsync(list =>
|
|
|
+ list.RemoveAll(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase)));
|
|
|
|
|
|
- private void UpdateWithLock(Action<List<Resource>> action)
|
|
|
+ private async Task UpdateWithLockAsync(Action<List<Resource>> action)
|
|
|
{
|
|
|
- lock (_fileLock)
|
|
|
+ await _fileLock.WaitAsync();
|
|
|
+ try
|
|
|
{
|
|
|
action(_resources);
|
|
|
|
|
|
@@ -68,64 +83,26 @@ public sealed class YamlResourceCollection(string filePath)
|
|
|
["resources"] = _resources.Select(SerializeResource).ToList()
|
|
|
};
|
|
|
|
|
|
- File.WriteAllText(filePath, serializer.Serialize(payload));
|
|
|
+ await _fileStore.WriteAllTextAsync(
|
|
|
+ _filePath,
|
|
|
+ serializer.Serialize(payload));
|
|
|
}
|
|
|
- }
|
|
|
-
|
|
|
- private string GetKind(Resource resource)
|
|
|
- {
|
|
|
- return 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}")
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
- private OrderedDictionary SerializeResource(Resource resource)
|
|
|
- {
|
|
|
- var map = new OrderedDictionary
|
|
|
+ finally
|
|
|
{
|
|
|
- ["kind"] = GetKind(resource)
|
|
|
- };
|
|
|
-
|
|
|
- var serializer = new SerializerBuilder()
|
|
|
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
- .Build();
|
|
|
-
|
|
|
- var yaml = serializer.Serialize(resource);
|
|
|
-
|
|
|
- var props = new DeserializerBuilder()
|
|
|
- .Build()
|
|
|
- .Deserialize<Dictionary<string, object?>>(yaml);
|
|
|
-
|
|
|
- foreach (var (key, value) in props)
|
|
|
- if (key != "kind")
|
|
|
- map[key] = value;
|
|
|
-
|
|
|
- return map;
|
|
|
+ _fileLock.Release();
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- private static List<Resource> LoadFromFile(string filePath)
|
|
|
+ private async Task<List<Resource>> LoadFromFileAsync()
|
|
|
{
|
|
|
- // 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 yaml = await _fileStore.ReadAllTextAsync(_filePath);
|
|
|
+ if (string.IsNullOrWhiteSpace(yaml))
|
|
|
+ return new();
|
|
|
|
|
|
var deserializer = new DeserializerBuilder()
|
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
.WithCaseInsensitivePropertyMatching()
|
|
|
.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>
|
|
|
@@ -146,25 +123,56 @@ public sealed class YamlResourceCollection(string filePath)
|
|
|
|
|
|
try
|
|
|
{
|
|
|
- // 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 root?.Resources ?? new();
|
|
|
}
|
|
|
catch (YamlException)
|
|
|
{
|
|
|
- // Handle malformed YAML here or return empty list
|
|
|
- return new List<Resource>();
|
|
|
+ return new();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- public Resource? GetByName(string name)
|
|
|
+ private string GetKind(Resource resource) => 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}")
|
|
|
+ };
|
|
|
+
|
|
|
+ private OrderedDictionary SerializeResource(Resource resource)
|
|
|
{
|
|
|
- return _resources.FirstOrDefault(r => r.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
|
|
+ var map = new OrderedDictionary
|
|
|
+ {
|
|
|
+ ["kind"] = GetKind(resource)
|
|
|
+ };
|
|
|
+
|
|
|
+ var serializer = new SerializerBuilder()
|
|
|
+ .WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
|
+ .Build();
|
|
|
+
|
|
|
+ var yaml = serializer.Serialize(resource);
|
|
|
+
|
|
|
+ var props = new DeserializerBuilder()
|
|
|
+ .Build()
|
|
|
+ .Deserialize<Dictionary<string, object?>>(yaml);
|
|
|
+
|
|
|
+ foreach (var (key, value) in props)
|
|
|
+ if (key != "kind")
|
|
|
+ map[key] = value;
|
|
|
+
|
|
|
+ return map;
|
|
|
}
|
|
|
|
|
|
- // Simple wrapper class to match the YAML structure
|
|
|
private class YamlRoot
|
|
|
{
|
|
|
public List<Resource>? Resources { get; set; }
|
|
|
}
|
|
|
-}
|
|
|
+}
|