using System; using System.Collections.Generic; using Newtonsoft.Json.Linq; namespace Logof_Client; /// /// Handles schema versioning and migrations for JSON data structures. /// This system allows tracking schema changes and automatically migrating /// old data to new formats when the schema version changes. /// public static class SchemaMigration { // Current schema version - increment when you make breaking changes public const int CURRENT_SCHEMA_VERSION = 3; // Dictionary of migration functions, keyed by the version they migrate TO private static readonly Dictionary> Migrations = new() { // Example: Migration from v1 to v2 // When loading v1 data, this will be called to upgrade to v2 // { 2, MigrateV1ToV2 }, // Example: Migration from v2 to v3 // { 3, MigrateV2ToV3 }, }; /// /// Checks the schema version of loaded JSON and applies migrations if needed. /// public static JObject MigrateIfNeeded(JObject jsonObject, int? loadedVersion) { // If no version found, assume it's version 1 (legacy data) int sourceVersion = loadedVersion ?? 1; // If versions match, no migration needed if (sourceVersion == CURRENT_SCHEMA_VERSION) return jsonObject; // If loaded version is newer than current, warn and return as-is if (sourceVersion > CURRENT_SCHEMA_VERSION) { Logger.Log($"Warning: Data schema version {sourceVersion} is newer than application schema version {CURRENT_SCHEMA_VERSION}. " + "This might indicate data was created with a newer version of the application.", Logger.LogType.Warning); return jsonObject; } // Apply migrations sequentially from source version to current version Logger.Log($"Migrating data from schema version {sourceVersion} to {CURRENT_SCHEMA_VERSION}", Logger.LogType.Info); for (int targetVersion = sourceVersion + 1; targetVersion <= CURRENT_SCHEMA_VERSION; targetVersion++) { if (Migrations.TryGetValue(targetVersion, out var migration)) { try { jsonObject = migration(jsonObject); Logger.Log($"Successfully migrated to schema version {targetVersion}", Logger.LogType.Info); } catch (Exception ex) { Logger.Log($"Error during migration to version {targetVersion}: {ex.Message}", Logger.LogType.Error); throw; } } else { Logger.Log($"No migration defined from version {targetVersion - 1} to {targetVersion}", Logger.LogType.Warning); } } return jsonObject; } /// /// Example migration function: from v1 to v2. /// Rename 'old_field_name' to 'new_field_name' /// private static JObject MigrateV1ToV2(JObject obj) { // Example: Rename a field if (obj.TryGetValue("old_field_name", out var value)) { obj["new_field_name"] = value; obj.Remove("old_field_name"); } return obj; } /// /// Example migration function: from v2 to v3. /// Add a new required field with a default value. /// private static JObject MigrateV2ToV3(JObject obj) { // Example: Add a new field that didn't exist before if (!obj.ContainsKey("new_required_field")) { obj["new_required_field"] = "default_value"; } return obj; } /// /// Registers a new migration function that will be called when upgrading to a specific version. /// Call this during application startup to register your custom migrations. /// /// The schema version this migration upgrades TO /// The migration function that transforms the JObject public static void RegisterMigration(int targetVersion, Func migration) { if (targetVersion <= 1) throw new ArgumentException("Target version must be greater than 1"); if (Migrations.ContainsKey(targetVersion)) Logger.Log($"Warning: Overwriting existing migration for version {targetVersion}", Logger.LogType.Warning); Migrations[targetVersion] = migration; Logger.Log($"Registered migration to schema version {targetVersion}", Logger.LogType.Info); } /// /// Helper method: Add a new property to an object if it doesn't exist /// public static void AddPropertyIfMissing(JObject obj, string propertyName, object defaultValue) { if (!obj.ContainsKey(propertyName)) { obj[propertyName] = JToken.FromObject(defaultValue); } } /// /// Helper method: Rename a property in a JSON object /// public static void RenameProperty(JObject obj, string oldName, string newName) { if (obj.TryGetValue(oldName, out var value)) { obj[newName] = value; obj.Remove(oldName); } } /// /// Helper method: Move a nested property to the root level /// public static void FlattenProperty(JObject obj, string sourcePath, string targetPropertyName) { var value = obj.SelectToken(sourcePath); if (value != null) { obj[targetPropertyName] = value; } } /// /// Helper method: Merge a root property into a nested object /// public static void NestProperty(JObject obj, string propertyName, string nestedPath) { if (obj.TryGetValue(propertyName, out var value)) { var parts = nestedPath.Split('.'); JObject current = obj; for (int i = 0; i < parts.Length - 1; i++) { if (!current.ContainsKey(parts[i])) current[parts[i]] = new JObject(); current = (JObject)current[parts[i]]; } current[parts[^1]] = value; obj.Remove(propertyName); } } }