Răsfoiți Sursa

Added doc migrator 1.0.3

Tim Jones 1 lună în urmă
părinte
comite
d5ecf77888

+ 71 - 0
RackPeek.Domain/Persistence/Yaml/RackPeekConfigMigrationDeserializer.cs

@@ -0,0 +1,71 @@
+using DocMigrator.Yaml;
+using Microsoft.Extensions.Logging;
+using RackPeek.Domain.Resources;
+using RackPeek.Domain.Resources.AccessPoints;
+using RackPeek.Domain.Resources.Desktops;
+using RackPeek.Domain.Resources.Firewalls;
+using RackPeek.Domain.Resources.Laptops;
+using RackPeek.Domain.Resources.Routers;
+using RackPeek.Domain.Resources.Servers;
+using RackPeek.Domain.Resources.Services;
+using RackPeek.Domain.Resources.Switches;
+using RackPeek.Domain.Resources.SystemResources;
+using RackPeek.Domain.Resources.UpsUnits;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+
+namespace RackPeek.Domain.Persistence.Yaml;
+
+public class RackPeekConfigMigrationDeserializer : YamlMigrationDeserializer<YamlRoot>
+{
+    public RackPeekConfigMigrationDeserializer(IServiceProvider serviceProvider,
+        ILogger<YamlMigrationDeserializer<YamlRoot>> logger) :
+        base(serviceProvider, logger, 
+            new List<Func<IServiceProvider, Dictionary<object,object>, ValueTask>>{
+                EnsureSchemaVersionExists,
+            },
+            "version",
+            new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance)
+                .WithCaseInsensitivePropertyMatching()
+                .WithTypeConverter(new StorageSizeYamlConverter())
+                .WithTypeConverter(new NotesStringYamlConverter())
+                .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) }
+                    });
+                }), 
+            new SerializerBuilder()
+                .WithNamingConvention(CamelCaseNamingConvention.Instance)
+                .WithTypeConverter(new StorageSizeYamlConverter())
+                .WithTypeConverter(new NotesStringYamlConverter())
+                .ConfigureDefaultValuesHandling(
+                    DefaultValuesHandling.OmitNull |
+                    DefaultValuesHandling.OmitEmptyCollections
+                )) {}
+
+    #region Migrations
+
+    // Define migration functions here
+    public static ValueTask EnsureSchemaVersionExists(IServiceProvider serviceProvider, Dictionary<object, object> obj)
+    {
+        if (!obj.ContainsKey("version"))
+        {
+            obj["version"] = 0;
+        }
+        
+        return ValueTask.CompletedTask;
+    }
+
+    #endregion
+}

+ 23 - 100
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -26,7 +26,8 @@ public class ResourceCollection
 public sealed class YamlResourceCollection(
     string filePath,
     ITextFileStore fileStore,
-    ResourceCollection resourceCollection)
+    ResourceCollection resourceCollection,
+    RackPeekConfigMigrationDeserializer _deserializer)
     : IResourceCollection
 {
     // Bump this when your YAML schema changes, and add a migration step below.
@@ -108,34 +109,35 @@ public sealed class YamlResourceCollection(
             return;
         }
 
-        var root = DeserializeRoot(yaml);
-        if (root == null)
-        {
-            // Keep behavior aligned with your previous code: if YAML is invalid, treat as empty.
-            resourceCollection.Resources.Clear();
-            return;
-        }
-
+        var version = _deserializer.GetSchemaVersion(yaml); 
+        
         // Guard: config is newer than this app understands.
-        if (root.Version > CurrentSchemaVersion)
+        if (version > CurrentSchemaVersion)
         {
             throw new InvalidOperationException(
-                $"Config schema version {root.Version} is newer than this application supports ({CurrentSchemaVersion}).");
+                $"Config schema version {version} is newer than this application supports ({CurrentSchemaVersion}).");
         }
 
+        YamlRoot? root;
         // If older, backup first, then migrate step-by-step, then save.
-        if (root.Version < CurrentSchemaVersion)
+        if (version < CurrentSchemaVersion)
         {
             await BackupOriginalAsync(yaml);
-
-            root = await MigrateAsync(root);
-
+            root = await _deserializer.Deserialize(yaml);
+            
             // Ensure we persist the migrated root (with updated version)
             await SaveRootAsync(root);
         }
-
+        else
+        {
+            root = await _deserializer.Deserialize(yaml);
+        }
+        
         resourceCollection.Resources.Clear();
-        resourceCollection.Resources.AddRange(root.Resources ?? []);
+        if (root?.Resources != null)
+        {
+            resourceCollection.Resources.AddRange(root.Resources);
+        }
     }
 
     public Task AddAsync(Resource resource)
@@ -200,88 +202,8 @@ public sealed class YamlResourceCollection(
         var backupPath = $"{filePath}.bak.{DateTime.UtcNow:yyyyMMddHHmmss}";
         await fileStore.WriteAllTextAsync(backupPath, originalYaml);
     }
-
-    private Task<YamlRoot> MigrateAsync(YamlRoot root)
-    {
-        // Step-by-step migrations until we reach CurrentSchemaVersion
-        while (root.Version < CurrentSchemaVersion)
-        {
-            root = root.Version switch
-            {
-                0 => MigrateV0ToV1(root),
-                _ => throw new InvalidOperationException(
-                    $"No migration is defined from version {root.Version} to {root.Version + 1}.")
-            };
-        }
-
-        return Task.FromResult(root);
-    }
-
-    private YamlRoot MigrateV0ToV1(YamlRoot root)
-    {
-        // V0 -> V1 example migration:
-        // - Ensure 'kind' is normalized on all resources
-        // - Ensure tags collections aren’t null
-        if (root.Resources != null)
-        {
-            foreach (var r in root.Resources)
-            {
-                r.Kind = GetKind(r);
-                r.Tags ??= [];
-            }
-        }
-
-        root.Version = 1;
-        return root;
-    }
-
-    // ----------------------------
-    // YAML read/write
-    // ----------------------------
-
-    private YamlRoot? DeserializeRoot(string yaml)
-    {
-        var deserializer = new DeserializerBuilder()
-            .WithNamingConvention(CamelCaseNamingConvention.Instance)
-            .WithCaseInsensitivePropertyMatching()
-            .WithTypeConverter(new StorageSizeYamlConverter())
-            .WithTypeConverter(new NotesStringYamlConverter())
-            .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();
-
-        try
-        {
-            // If 'version' is missing, int defaults to 0 => treated as V0.
-            var root = deserializer.Deserialize<YamlRoot>(yaml);
-
-            // If YAML had only "resources:" previously, this will still work.
-            root ??= new YamlRoot { Version = 0, Resources = new List<Resource>() };
-            root.Resources ??= new List<Resource>();
-
-            return root;
-        }
-        catch (YamlException)
-        {
-            return null;
-        }
-    }
-
-    private async Task SaveRootAsync(YamlRoot root)
+    
+    private async Task SaveRootAsync(YamlRoot? root)
     {
         var serializer = new SerializerBuilder()
             .WithNamingConvention(CamelCaseNamingConvention.Instance)
@@ -349,10 +271,11 @@ public sealed class YamlResourceCollection(
 
         return map;
     }
+
 }
 
 public class YamlRoot
 {
-    public int Version { get; set; } // <- NEW: YAML schema version
+    public int Version { get; set; }
     public List<Resource>? Resources { get; set; }
 }

+ 4 - 2
RackPeek.Domain/RackPeek.Domain.csproj

@@ -7,8 +7,10 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2"/>
-        <PackageReference Include="YamlDotNet" Version="16.3.0"/>
+        <PackageReference Include="DocMigrator.Yaml" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+        <PackageReference Include="YamlDotNet" Version="16.3.0" />
     </ItemGroup>
 
 </Project>

+ 7 - 3
RackPeek.Web.Viewer/Program.cs

@@ -5,6 +5,8 @@ using RackPeek.Domain;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence.Yaml;
 using Shared.Rcl;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
 
 namespace RackPeek.Web.Viewer;
 
@@ -33,15 +35,17 @@ public class Program
 
         var resources = new ResourceCollection();
         builder.Services.AddSingleton(resources);
-
+        
         var yamlDir = builder.Configuration.GetValue<string>("RPK_YAML_DIR") ?? "config";
         var yamlFilePath = $"{yamlDir}/config.yaml";
+        builder.Services.AddScoped<RackPeekConfigMigrationDeserializer>();
 
         builder.Services.AddScoped<IResourceCollection>(sp =>
             new YamlResourceCollection(
                 yamlFilePath,
                 sp.GetRequiredService<ITextFileStore>(),
-                sp.GetRequiredService<ResourceCollection>()));
+                sp.GetRequiredService<ResourceCollection>(),
+                sp.GetRequiredService<RackPeekConfigMigrationDeserializer>()));
 
         builder.Services.AddYamlRepos();
         builder.Services.AddCommands();
@@ -53,4 +57,4 @@ public class Program
 
         await builder.Build().RunAsync();
     }
-}
+}

+ 8 - 8
RackPeek.Web.Viewer/RackPeek.Web.Viewer.csproj

@@ -8,20 +8,20 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0"/>
-        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all"/>
-        <PackageReference Include="Spectre.Console.Cli" Version="0.53.1"/>
-        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0"/>
+        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.3" />
+        <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.3" PrivateAssets="all" />
+        <PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
+        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0" />
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj"/>
+        <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj" />
     </ItemGroup>
 
     <ItemGroup>
-        <_ContentIncludedByDefault Remove="Layout\MainLayout.razor"/>
-        <_ContentIncludedByDefault Remove="Layout\NavMenu.razor"/>
-        <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json"/>
+        <_ContentIncludedByDefault Remove="Layout\MainLayout.razor" />
+        <_ContentIncludedByDefault Remove="Layout\NavMenu.razor" />
+        <_ContentIncludedByDefault Remove="wwwroot\sample-data\weather.json" />
     </ItemGroup>
 
 </Project>

+ 7 - 4
RackPeek.Web/Program.cs

@@ -4,6 +4,8 @@ using RackPeek.Domain;
 using RackPeek.Domain.Persistence;
 using RackPeek.Domain.Persistence.Yaml;
 using RackPeek.Web.Components;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
 using Shared.Rcl;
 
 namespace RackPeek.Web;
@@ -46,8 +48,7 @@ public class Program
         }
 
         builder.Services.AddScoped<ITextFileStore, PhysicalTextFileStore>();
-
-
+        
         builder.Services.AddScoped(sp =>
         {
             var nav = sp.GetRequiredService<NavigationManager>();
@@ -60,12 +61,14 @@ public class Program
 
         var resources = new ResourceCollection();
         builder.Services.AddSingleton(resources);
+        builder.Services.AddScoped<RackPeekConfigMigrationDeserializer>();
 
         builder.Services.AddScoped<IResourceCollection>(sp =>
             new YamlResourceCollection(
                 yamlFilePath,
                 sp.GetRequiredService<ITextFileStore>(),
-                sp.GetRequiredService<ResourceCollection>()));
+                sp.GetRequiredService<ResourceCollection>(),
+                sp.GetRequiredService<RackPeekConfigMigrationDeserializer>()));
 
         // Infrastructure
         builder.Services.AddYamlRepos();
@@ -108,4 +111,4 @@ public class Program
         var app = await BuildApp(builder);
         await app.RunAsync();
     }
-}
+}

+ 12 - 12
RackPeek/RackPeek.csproj

@@ -9,21 +9,21 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2"/>
-        <PackageReference Include="Spectre.Console" Version="0.54.0"/>
-        <PackageReference Include="Spectre.Console.Cli" Version="0.53.1"/>
-        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0"/>
-        <PackageReference Include="YamlDotNet" Version="16.3.0"/>
+        <PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
+        <PackageReference Include="Spectre.Console" Version="0.54.0" />
+        <PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
+        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0" />
+        <PackageReference Include="YamlDotNet" Version="16.3.0" />
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj"/>
+        <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj" />
     </ItemGroup>
 
 </Project>

+ 12 - 3
Shared.Rcl/CliBootstrap.cs

@@ -1,4 +1,8 @@
 using System.ComponentModel.DataAnnotations;
+using DocMigrator.Yaml;
+using YamlDotNet.Serialization;
+using YamlDotNet.Serialization.NamingConventions;
+using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain;
@@ -78,10 +82,15 @@ public static class CliBootstrap
 
         if (!File.Exists(fullYamlPath)) await File.WriteAllTextAsync(fullYamlPath, "");
 
+        services.AddLogging();
+        services.AddScoped<RackPeekConfigMigrationDeserializer>();
+
         var collection = new YamlResourceCollection(
             fullYamlPath,
             new PhysicalTextFileStore(),
-            new ResourceCollection());
+            new ResourceCollection(),
+            // TODO: Is this right?
+            services.BuildServiceProvider().GetRequiredService<RackPeekConfigMigrationDeserializer>());
 
         await collection.LoadAsync();
         services.AddSingleton<IResourceCollection>(collection);
@@ -576,7 +585,7 @@ public static class CliBootstrap
         });
     }
 
-    private static int HandleException(Exception ex, ITypeResolver? arg2)
+    private static int HandleException(Exception ex, Spectre.Console.Cli.ITypeResolver? arg2)
     {
         switch (ex)
         {
@@ -638,4 +647,4 @@ public static class CliBootstrap
             _showingHelp = false;
         }
     }
-}
+}

+ 6 - 6
Shared.Rcl/Shared.Rcl.csproj

@@ -10,18 +10,18 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <SupportedPlatform Include="browser"/>
+        <SupportedPlatform Include="browser" />
     </ItemGroup>
 
     <ItemGroup>
-        <PackageReference Include="Markdig" Version="0.45.0"/>
-        <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.0"/>
-        <PackageReference Include="Spectre.Console.Cli" Version="0.53.1"/>
-        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0"/>
+        <PackageReference Include="Markdig" Version="1.0.0" />
+        <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.3" />
+        <PackageReference Include="Spectre.Console.Cli" Version="0.53.1" />
+        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0" />
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Include="..\RackPeek.Domain\RackPeek.Domain.csproj"/>
+        <ProjectReference Include="..\RackPeek.Domain\RackPeek.Domain.csproj" />
     </ItemGroup>
 
 

+ 17 - 11
Tests.E2e/Tests.E2e.csproj

@@ -8,23 +8,29 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="coverlet.collector" Version="6.0.4"/>
-        <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3"/>
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
-        <PackageReference Include="Microsoft.Playwright" Version="1.58.0"/>
-        <PackageReference Include="Microsoft.Playwright.Xunit" Version="1.58.0"/>
-        <PackageReference Include="Testcontainers" Version="4.10.0"/>
-        <PackageReference Include="xunit" Version="2.9.3"/>
-        <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
+        <PackageReference Include="coverlet.collector" Version="8.0.0">
+          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+          <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
+        <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.3" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
+        <PackageReference Include="Microsoft.Playwright" Version="1.58.0" />
+        <PackageReference Include="Microsoft.Playwright.Xunit" Version="1.58.0" />
+        <PackageReference Include="Testcontainers" Version="4.10.0" />
+        <PackageReference Include="xunit" Version="2.9.3" />
+        <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
+          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+          <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
     </ItemGroup>
 
     <ItemGroup>
-        <Using Include="Xunit"/>
+        <Using Include="Xunit" />
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Include="..\RackPeek.Web.Viewer\RackPeek.Web.Viewer.csproj"/>
-        <ProjectReference Include="..\RackPeek.Web\RackPeek.Web.csproj"/>
+        <ProjectReference Include="..\RackPeek.Web.Viewer\RackPeek.Web.Viewer.csproj" />
+        <ProjectReference Include="..\RackPeek.Web\RackPeek.Web.csproj" />
     </ItemGroup>
 
 </Project>

+ 13 - 15
Tests/Tests.csproj

@@ -15,8 +15,8 @@
         <PackageReference Include="xunit" Version="2.9.3"/>
         <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4"/>
         <PackageReference Include="Spectre.Console.Testing" Version="0.54.0"/>
-        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2"/>
-        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.2"/>
+        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3"/>
+        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3"/>
     </ItemGroup>
 
     <ItemGroup>
@@ -33,7 +33,6 @@
       <Folder Include="EndToEnd\ServerTests\" />
       <Folder Include="EndToEnd\ServiceTests\" />
     </ItemGroup>
-
     <ItemGroup>
         <None Include="TestConfigs\**\*.yaml">
             <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
@@ -44,48 +43,47 @@
         </None>
 
         <None Update="TestConfigs\v1\valid-config-1.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\01-server.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\02-firewall.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\03-router.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\04-switch.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\05-accesspoint.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\06-ups.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\07-desktop.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\08-laptop.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\09-service.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
 
         <None Update="TestConfigs\v1\10-system.yaml">
-          <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+            <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
         </None>
     </ItemGroup>
-    
 </Project>

+ 0 - 478
Tests/Yaml/HardwareDeserializationTests.cs

@@ -1,478 +0,0 @@
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Persistence.Yaml;
-using RackPeek.Domain.Resources.AccessPoints;
-using RackPeek.Domain.Resources.Desktops;
-using RackPeek.Domain.Resources.Firewalls;
-using RackPeek.Domain.Resources.Hardware;
-using RackPeek.Domain.Resources.Laptops;
-using RackPeek.Domain.Resources.Routers;
-using RackPeek.Domain.Resources.Servers;
-using RackPeek.Domain.Resources.Switches;
-using RackPeek.Domain.Resources.UpsUnits;
-
-namespace Tests.Yaml;
-
-public class HardwareDeserializationTests
-{
-    public async Task<IResourceCollection> CreateSut(string yaml)
-    {
-        var tempDir = Path.Combine(
-            Path.GetTempPath(),
-            "RackPeekTests",
-            Guid.NewGuid().ToString("N"));
-
-        Directory.CreateDirectory(tempDir);
-
-        var filePath = Path.Combine(tempDir, "config.yaml");
-        await File.WriteAllTextAsync(filePath, yaml);
-
-        var yamlResourceCollection =
-            new YamlResourceCollection(filePath, new PhysicalTextFileStore(), new ResourceCollection());
-        await yamlResourceCollection.LoadAsync();
-        return yamlResourceCollection;
-    }
-
-
-    [Theory]
-    [InlineData("Server", typeof(Server))]
-    [InlineData("Switch", typeof(Switch))]
-    [InlineData("Firewall", typeof(Firewall))]
-    [InlineData("Desktop", typeof(Desktop))]
-    [InlineData("Laptop", typeof(Laptop))]
-    [InlineData("Router", typeof(Router))]
-    [InlineData("AccessPoint", typeof(AccessPoint))]
-    [InlineData("Ups", typeof(Ups))]
-    public async Task deserialize_yaml_kind(string kind, Type type)
-    {
-        // Given
-        var yaml = $@"
-resources:
-  - kind: {kind}
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType(type, hardware);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_server()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Server
-    name: dell-c6400-node01
-    cpus:
-        - model: Intel(R) Xeon(R) CPU E3-1270 v6
-          cores: 4
-          threads: 8
-    ram:
-        size: 32gb
-        Mts: 2400
-    drives:
-        - type: hdd
-          size: 2Tb
-        - type: ssd
-          size: 256gb
-    gpus:
-        - model: NVIDIA Tesla T4
-          vram: 16gb
-    nics:
-        - type: rj45
-          speed: 1gb
-          ports: 2
-        - type: sfp+
-          speed: 10gb
-          ports: 2
-    ipmi: true
-";
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Server>(hardware);
-        var server = hardware as Server;
-        Assert.NotNull(server);
-        Assert.Equal("dell-c6400-node01", server.Name);
-        // Cpu
-        Assert.NotNull(server.Cpus);
-        var cpu = server.Cpus[0];
-        Assert.Equal("Intel(R) Xeon(R) CPU E3-1270 v6", cpu.Model);
-        Assert.Equal(4, cpu.Cores);
-        Assert.Equal(8, cpu.Threads);
-
-        // Ram
-        Assert.NotNull(server.Ram);
-        Assert.Equal(32, server.Ram.Size);
-        Assert.Equal(2400, server.Ram.Mts);
-
-        // Drives
-        Assert.NotNull(server.Drives);
-        var hdd = server.Drives[0];
-        Assert.Equal("hdd", hdd.Type);
-        Assert.Equal(2048, hdd.Size);
-        var ssd = server.Drives[1];
-        Assert.Equal("ssd", ssd.Type);
-        Assert.Equal(256, ssd.Size);
-
-        //GPUs
-        Assert.NotNull(server.Gpus);
-        var gpu = server.Gpus[0];
-        Assert.Equal("NVIDIA Tesla T4", gpu.Model);
-        Assert.Equal(16, gpu.Vram);
-
-        // ipmi
-        Assert.True(server.Ipmi);
-
-        // Nics
-        Assert.NotNull(server.Nics);
-        var nic0 = server.Nics[0];
-        Assert.Equal("rj45", nic0.Type);
-        Assert.Equal(1, nic0.Speed);
-        Assert.Equal(2, nic0.Ports);
-        var nic1 = server.Nics[1];
-        Assert.Equal("sfp+", nic1.Type);
-        Assert.Equal(10, nic1.Speed);
-        Assert.Equal(2, nic1.Ports);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_switch()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Switch
-    name: netgear-s24
-    model: GS324
-    ports:
-        - type: rj45
-          speed: 1gb
-          count: 8
-        - type: sfp
-          speed: 10gb
-          count: 2
-    managed: true
-    poe: true
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Switch>(hardware);
-
-        var sw = hardware as Switch;
-        Assert.NotNull(sw);
-
-        Assert.Equal("netgear-s24", sw.Name);
-        Assert.Equal("GS324", sw.Model);
-        Assert.Equal(true, sw.Managed);
-        Assert.Equal(true, sw.Poe);
-
-        // Nics
-        Assert.NotNull(sw.Ports);
-        var nic0 = sw.Ports[0];
-        Assert.Equal("rj45", nic0.Type);
-        Assert.Equal(1, nic0.Speed);
-        Assert.Equal(8, nic0.Count);
-        var nic1 = sw.Ports[1];
-        Assert.Equal("sfp", nic1.Type);
-        Assert.Equal(10, nic1.Speed);
-        Assert.Equal(2, nic1.Count);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_firewall()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Firewall
-    name: pfsense
-    model: pfSense-1100
-    ports:
-        - type: rj45
-          speed: 1gb
-          count: 8
-        - type: sfp
-          speed: 10gb
-          count: 2
-    managed: true
-    poe: true
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Firewall>(hardware);
-
-        var fw = hardware as Firewall;
-        Assert.NotNull(fw);
-
-        Assert.Equal("pfsense", fw.Name);
-        Assert.Equal("pfSense-1100", fw.Model);
-        Assert.Equal(true, fw.Managed);
-        Assert.Equal(true, fw.Poe);
-
-        // Nics
-        Assert.NotNull(fw.Ports);
-        var nic0 = fw.Ports[0];
-        Assert.Equal("rj45", nic0.Type);
-        Assert.Equal(1, nic0.Speed);
-        Assert.Equal(8, nic0.Count);
-        var nic1 = fw.Ports[1];
-        Assert.Equal("sfp", nic1.Type);
-        Assert.Equal(10, nic1.Speed);
-        Assert.Equal(2, nic1.Count);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_router()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Router
-    name: ubiquiti-edge-router
-    model: ER-4
-    ports:
-        - type: rj45
-          speed: 1gb
-          count: 8
-        - type: sfp
-          speed: 10gb
-          count: 2
-    managed: true
-    poe: true
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Router>(hardware);
-
-        var router = hardware as Router;
-        Assert.NotNull(router);
-
-        Assert.Equal("ubiquiti-edge-router", router.Name);
-        Assert.Equal("ER-4", router.Model);
-        Assert.Equal(true, router.Managed);
-        Assert.Equal(true, router.Poe);
-
-        // Nics
-        Assert.NotNull(router.Ports);
-        var nic0 = router.Ports[0];
-        Assert.Equal("rj45", nic0.Type);
-        Assert.Equal(1, nic0.Speed);
-        Assert.Equal(8, nic0.Count);
-        var nic1 = router.Ports[1];
-        Assert.Equal("sfp", nic1.Type);
-        Assert.Equal(10, nic1.Speed);
-        Assert.Equal(2, nic1.Count);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_desktop()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Desktop
-    name: dell-optiplex
-    cpus:
-      - model: Intel(R) Core(TM) i5-9500
-        cores: 6
-        threads: 6
-    ram:
-      size: 16gb
-      mts: 2666
-    drives:
-      - type: ssd
-        size: 512gb
-    nics:
-      - type: rj45
-        speed: 1gb
-        ports: 1
-    gpus:
-       - model: RTX 3080
-         vram: 12gb
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Desktop>(hardware);
-
-        var desktop = hardware as Desktop;
-        Assert.NotNull(desktop);
-
-        Assert.Equal("dell-optiplex", desktop.Name);
-
-        // CPU
-        Assert.NotNull(desktop.Cpus);
-        Assert.Equal("Intel(R) Core(TM) i5-9500", desktop.Cpus[0].Model);
-        Assert.Equal(6, desktop.Cpus[0].Cores);
-        Assert.Equal(6, desktop.Cpus[0].Threads);
-
-        // RAM
-        Assert.NotNull(desktop.Ram);
-        Assert.Equal(16, desktop.Ram.Size);
-        Assert.Equal(2666, desktop.Ram.Mts);
-
-        // Drives
-        Assert.NotNull(desktop.Drives);
-        Assert.Equal("ssd", desktop.Drives[0].Type);
-        Assert.Equal(512, desktop.Drives[0].Size);
-
-        // NIC
-        Assert.NotNull(desktop.Nics);
-        Assert.Equal("rj45", desktop.Nics[0].Type);
-        Assert.Equal(1, desktop.Nics[0].Speed);
-        Assert.Equal(1, desktop.Nics[0].Ports);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_laptop()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Laptop
-    name: thinkpad-x1
-    cpus:
-        - model: Intel(R) Core(TM) i7-10510U
-          cores: 4
-          threads: 8
-    ram:
-        size: 16gb
-        mts: 2666
-    drives:
-        - type: ssd
-          size: 1tb
-    gpus:
-       - model: RTX 3080
-         vram: 12gb
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Laptop>(hardware);
-
-        var laptop = hardware as Laptop;
-        Assert.NotNull(laptop);
-
-        Assert.Equal("thinkpad-x1", laptop.Name);
-
-        // CPU
-        Assert.NotNull(laptop.Cpus);
-        Assert.Equal("Intel(R) Core(TM) i7-10510U", laptop.Cpus[0].Model);
-        Assert.Equal(4, laptop.Cpus[0].Cores);
-        Assert.Equal(8, laptop.Cpus[0].Threads);
-
-        // RAM
-        Assert.NotNull(laptop.Ram);
-        Assert.Equal(16, laptop.Ram.Size);
-        Assert.Equal(2666, laptop.Ram.Mts);
-
-        // Drives
-        Assert.NotNull(laptop.Drives);
-        Assert.Equal("ssd", laptop.Drives[0].Type);
-        Assert.Equal(1024, laptop.Drives[0].Size);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_accesspoint()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: AccessPoint
-    name: lounge-ap
-    model: Unifi-Ap-Pro
-    speed: 2.5Gb
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<AccessPoint>(hardware);
-
-        var accessPoint = hardware as AccessPoint;
-        Assert.NotNull(accessPoint);
-
-        Assert.Equal("lounge-ap", accessPoint.Name);
-        Assert.Equal("Unifi-Ap-Pro", accessPoint.Model);
-        Assert.Equal(2.5, accessPoint.Speed);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_ups()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Ups
-    name: rack-ups
-    model: Volta
-    va: 2200
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Hardware>();
-
-
-        // Then
-        var hardware = Assert.Single(resources);
-        Assert.IsType<Ups>(hardware);
-
-        var ups = hardware as Ups;
-        Assert.NotNull(ups);
-
-        Assert.Equal("rack-ups", ups.Name);
-        Assert.Equal("Volta", ups.Model);
-        Assert.Equal(2200, ups.Va);
-    }
-}

+ 0 - 110
Tests/Yaml/LabelsYamlTests.cs

@@ -1,110 +0,0 @@
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Persistence.Yaml;
-using RackPeek.Domain.Resources.Servers;
-
-namespace Tests.Yaml;
-
-/// <summary>
-/// Tests YAML serialization and deserialization of resource labels.
-/// </summary>
-public class LabelsYamlTests
-{
-    private static async Task<IResourceCollection> CreateSut(string yaml)
-    {
-        var tempDir = Path.Combine(
-            Path.GetTempPath(),
-            "RackPeekTests",
-            Guid.NewGuid().ToString("N"));
-
-        Directory.CreateDirectory(tempDir);
-
-        var filePath = Path.Combine(tempDir, "config.yaml");
-        await File.WriteAllTextAsync(filePath, yaml);
-
-        var yamlResourceCollection =
-            new YamlResourceCollection(filePath, new PhysicalTextFileStore(), new ResourceCollection());
-        await yamlResourceCollection.LoadAsync();
-        return yamlResourceCollection;
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_with_labels__resource_has_labels()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Server
-    name: web-01
-    labels:
-      env: production
-      owner: team-a
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var server = await sut.GetByNameAsync<Server>("web-01");
-
-        // Then
-        Assert.NotNull(server);
-        Assert.Equal(2, server.Labels.Count);
-        Assert.Equal("production", server.Labels["env"]);
-        Assert.Equal("team-a", server.Labels["owner"]);
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_without_labels__resource_has_empty_labels()
-    {
-        // Given - legacy YAML without labels section
-        var yaml = @"
-resources:
-  - kind: Server
-    name: web-01
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var server = await sut.GetByNameAsync<Server>("web-01");
-
-        // Then
-        Assert.NotNull(server);
-        Assert.NotNull(server.Labels);
-        Assert.Empty(server.Labels);
-    }
-
-    [Fact]
-    public async Task round_trip_labels__persisted_and_loaded()
-    {
-        // Given - add server with labels via collection, save, reload
-        var tempDir = Path.Combine(
-            Path.GetTempPath(),
-            "RackPeekTests",
-            Guid.NewGuid().ToString("N"));
-        Directory.CreateDirectory(tempDir);
-        var filePath = Path.Combine(tempDir, "config.yaml");
-        await File.WriteAllTextAsync(filePath, "");
-
-        var collection = new ResourceCollection();
-        var yamlCollection = new YamlResourceCollection(filePath, new PhysicalTextFileStore(), collection);
-        await yamlCollection.LoadAsync();
-
-        var server = new Server
-        {
-            Name = "web-01",
-            Labels = new Dictionary<string, string> { ["env"] = "production", ["owner"] = "team-a" }
-        };
-        await yamlCollection.AddAsync(server);
-
-        // When - reload from file
-        var reloaded = new YamlResourceCollection(filePath, new PhysicalTextFileStore(), new ResourceCollection());
-        await reloaded.LoadAsync();
-        var loaded = await reloaded.GetByNameAsync<Server>("web-01");
-
-        // Then
-        Assert.NotNull(loaded);
-        Assert.Equal(2, loaded.Labels.Count);
-        Assert.Equal("production", loaded.Labels["env"]);
-        Assert.Equal("team-a", loaded.Labels["owner"]);
-    }
-}

+ 0 - 64
Tests/Yaml/ServiceDeserializationTests.cs

@@ -1,64 +0,0 @@
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Persistence.Yaml;
-using RackPeek.Domain.Resources.Services;
-
-namespace Tests.Yaml;
-
-public class ServiceDeserializationTests
-{
-    public static async Task<IResourceCollection> CreateSut(string yaml)
-    {
-        var tempDir = Path.Combine(
-            Path.GetTempPath(),
-            "RackPeekTests",
-            Guid.NewGuid().ToString("N"));
-
-        Directory.CreateDirectory(tempDir);
-
-        var filePath = Path.Combine(tempDir, "config.yaml");
-        await File.WriteAllTextAsync(filePath, yaml);
-
-        var yamlResourceCollection =
-            new YamlResourceCollection(filePath, new PhysicalTextFileStore(), new ResourceCollection());
-        await yamlResourceCollection.LoadAsync();
-
-        return yamlResourceCollection;
-    }
-
-
-    [Fact]
-    public async Task deserialize_yaml_kind_Service()
-    {
-        // Given
-        var yaml = @"
-resources:
-  - kind: Service
-    name: immich
-    network:
-      ip: 192.168.0.4
-      port: 8080
-      protocol: TCP
-      url: http://immich.lan:8080
-    runsOn: proxmox-host
-";
-
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<Service>();
-
-        // Then
-        var resource = Assert.Single(resources);
-        var service = Assert.IsType<Service>(resource);
-
-        Assert.Equal("immich", service.Name);
-        Assert.Equal("Service", service.Kind);
-        Assert.Equal("proxmox-host", service.RunsOn);
-
-        Assert.NotNull(service.Network);
-        Assert.Equal("192.168.0.4", service.Network.Ip);
-        Assert.Equal(8080, service.Network.Port);
-        Assert.Equal("TCP", service.Network.Protocol);
-        Assert.Equal("http://immich.lan:8080", service.Network.Url);
-    }
-}

+ 0 - 71
Tests/Yaml/SystemDeserializationTests.cs

@@ -1,71 +0,0 @@
-using RackPeek.Domain.Persistence;
-using RackPeek.Domain.Persistence.Yaml;
-using RackPeek.Domain.Resources.SystemResources;
-
-namespace Tests.Yaml;
-
-public class SystemDeserializationTests
-{
-    public static async Task<IResourceCollection> CreateSut(string yaml)
-    {
-        var tempDir = Path.Combine(
-            Path.GetTempPath(),
-            "RackPeekTests",
-            Guid.NewGuid().ToString("N"));
-
-        Directory.CreateDirectory(tempDir);
-
-        var filePath = Path.Combine(tempDir, "config.yaml");
-        await File.WriteAllTextAsync(filePath, yaml);
-
-        var yamlResourceCollection =
-            new YamlResourceCollection(filePath, new PhysicalTextFileStore(), new ResourceCollection());
-        await yamlResourceCollection.LoadAsync();
-
-
-        return yamlResourceCollection;
-    }
-
-    [Fact]
-    public async Task deserialize_yaml_kind_System()
-    {
-        // type: Hypervisor | Baremetal | VM | Container 
-
-        // Given
-        var yaml = @"
-resources:
-  - kind: System
-    type: Hypervisor
-    name: home-virtualization-host
-    os: proxmox     
-    cores: 2
-    ram: 12.5gb
-    drives:
-        - size: 2Tb
-        - size: 1tb   
-    runsOn: dell-c6400-node-01
-";
-        var sut = await CreateSut(yaml);
-
-        // When
-        var resources = await sut.GetAllOfTypeAsync<SystemResource>();
-
-        // Then
-        var resource = Assert.Single(resources);
-        Assert.IsType<SystemResource>(resource);
-        var system = resource;
-        Assert.NotNull(system);
-        Assert.Equal("Hypervisor", system.Type);
-        Assert.Equal("home-virtualization-host", system.Name);
-        Assert.Equal("proxmox", system.Os);
-        Assert.Equal(2, system.Cores);
-        Assert.Equal(12.5, system.Ram);
-
-        // Drives
-        Assert.NotNull(system.Drives);
-        Assert.Equal(2048, system.Drives[0].Size);
-        Assert.Equal(1024, system.Drives[1].Size);
-
-        Assert.Equal("dell-c6400-node-01", system.RunsOn);
-    }
-}