Răsfoiți Sursa

Add DocMigrator to handle deserialization and migrations

Signed-off-by: Stephen Reaves <reaves735@gmail.com>
Stephen Reaves 1 lună în urmă
părinte
comite
e4ba7a9f23

+ 63 - 11
RackPeek.Domain/Persistence/Yaml/YamlResourceCollection.cs

@@ -1,4 +1,7 @@
 using System.Collections.Specialized;
+using System.Reflection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.DependencyInjection;
 using RackPeek.Domain.Resources;
 using RackPeek.Domain.Resources.AccessPoints;
 using RackPeek.Domain.Resources.Desktops;
@@ -11,6 +14,7 @@ using RackPeek.Domain.Resources.Services;
 using RackPeek.Domain.Resources.Switches;
 using RackPeek.Domain.Resources.SystemResources;
 using RackPeek.Domain.Resources.UpsUnits;
+using DocMigrator.Yaml;
 using YamlDotNet.Core;
 using YamlDotNet.Serialization;
 using YamlDotNet.Serialization.NamingConventions;
@@ -117,14 +121,14 @@ public sealed class YamlResourceCollection(
         }
 
         // Guard: config is newer than this app understands.
-        if (root.Version > CurrentSchemaVersion)
+        if (root.SchemaVersion > CurrentSchemaVersion)
         {
             throw new InvalidOperationException(
-                $"Config schema version {root.Version} is newer than this application supports ({CurrentSchemaVersion}).");
+                $"Config schema version {root.SchemaVersion} is newer than this application supports ({CurrentSchemaVersion}).");
         }
 
         // If older, backup first, then migrate step-by-step, then save.
-        if (root.Version < CurrentSchemaVersion)
+        if (root.SchemaVersion < CurrentSchemaVersion)
         {
             await BackupOriginalAsync(yaml);
 
@@ -178,7 +182,7 @@ public sealed class YamlResourceCollection(
             // Always write current schema version when app writes the file.
             var root = new YamlRoot
             {
-                Version = CurrentSchemaVersion,
+                SchemaVersion = CurrentSchemaVersion,
                 Resources = resourceCollection.Resources
             };
 
@@ -204,13 +208,13 @@ public sealed class YamlResourceCollection(
     private Task<YamlRoot> MigrateAsync(YamlRoot root)
     {
         // Step-by-step migrations until we reach CurrentSchemaVersion
-        while (root.Version < CurrentSchemaVersion)
+        while (root.SchemaVersion < CurrentSchemaVersion)
         {
-            root = root.Version switch
+            root = root.SchemaVersion switch
             {
                 0 => MigrateV0ToV1(root),
                 _ => throw new InvalidOperationException(
-                    $"No migration is defined from version {root.Version} to {root.Version + 1}.")
+                    $"No migration is defined from version {root.SchemaVersion} to {root.SchemaVersion + 1}.")
             };
         }
 
@@ -231,7 +235,7 @@ public sealed class YamlResourceCollection(
             }
         }
 
-        root.Version = 1;
+        root.SchemaVersion = 1;
         return root;
     }
 
@@ -264,13 +268,16 @@ public sealed class YamlResourceCollection(
             })
             .Build();
 
+            // var rootDeserializer = Setup();
+            // var rootDeserializer = new YamlRootDeserializer();
+
         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 ??= new YamlRoot { SchemaVersion = 0, Resources = new List<Resource>() };
             root.Resources ??= new List<Resource>();
 
             return root;
@@ -296,7 +303,7 @@ public sealed class YamlResourceCollection(
         // Preserve ordering: version first, then resources
         var payload = new OrderedDictionary
         {
-            ["version"] = root.Version,
+            ["schemaVersion"] = root.SchemaVersion,
             ["resources"] = (root.Resources ?? new List<Resource>()).Select(SerializeResource).ToList()
         };
 
@@ -349,10 +356,55 @@ public sealed class YamlResourceCollection(
 
         return map;
     }
+
+    public YamlMigrator Setup(DeserializerBuilder deserializerBuilder)
+    {
+        var services = new ServiceCollection();
+        services.AddLogging();
+        services.AddYamlMigrator(Assembly.GetExecutingAssembly());
+        var scope = services.BuildServiceProvider().CreateScope();
+        return scope.ServiceProvider.GetRequiredService<YamlMigrator>();
+        // return new YamlRootDeserializer(scope.ServiceProvider, scope.ServiceProvider.GetRequiredService<ILogger<YamlRootDeserializer>>(), deserializerBuilder);
+    }
+
+    // TODO: Wrap this in an 'instance' so we don't needlessly rebuild this?
+    //       Similar to CamelCaseNamingConvention.instance
+    public class YamlRootDeserializer : YamlMigrationDeserializer<YamlRoot>
+    {
+        public YamlRootDeserializer(IServiceProvider serviceProvider,
+                ILogger<YamlRootDeserializer> logger,
+                DeserializerBuilder deserializerBuilder) :
+            base(serviceProvider, logger, new List<Func<IServiceProvider, Dictionary<object,object>, ValueTask>>{
+                    // List migrations here
+                    EnsureSchemaVersionExists,
+                },
+                deserializerBuilder) {}
+
+        #region Migrations
+
+        // Define migration functions here
+
+        public static ValueTask EnsureSchemaVersionExists(IServiceProvider serviceProvider, Dictionary<object, object> obj)
+        {
+            if (!obj.ContainsKey("schemaVersion"))
+            {
+                obj["schemaVersion"] = 0;
+                if (obj.ContainsKey("version"))
+                {
+                    obj["schemaVersion"] = obj["version"];
+                }
+            }
+
+            obj.Remove("version");
+            return ValueTask.CompletedTask;
+        }
+
+        #endregion
+    }
 }
 
 public class YamlRoot
 {
-    public int Version { get; set; } // <- NEW: YAML schema version
+    public int SchemaVersion { get; set; } // <- NEW: YAML schema version
     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.1" />
+        <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>

+ 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>

+ 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>

+ 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>

+ 16 - 10
Tests/Tests.csproj

@@ -8,22 +8,28 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="coverlet.collector" Version="6.0.4"/>
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1"/>
-        <PackageReference Include="NSubstitute" Version="5.3.0"/>
-        <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="coverlet.collector" Version="8.0.0">
+          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+          <PrivateAssets>all</PrivateAssets>
+        </PackageReference>
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
+        <PackageReference Include="NSubstitute" Version="5.3.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>
+        <PackageReference Include="Spectre.Console.Testing" Version="0.54.0" />
+        <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
+        <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
     </ItemGroup>
 
     <ItemGroup>
-        <Using Include="Xunit"/>
+        <Using Include="Xunit" />
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj"/>
+        <ProjectReference Include="..\Shared.Rcl\Shared.Rcl.csproj" />
     </ItemGroup>
 
     <ItemGroup>