From f38c0275f749b277f2977e71baf129f2b707eeef Mon Sep 17 00:00:00 2001 From: hatayama Date: Tue, 7 Jul 2026 11:38:15 +0900 Subject: [PATCH] Split editor settings storage from domain values Keep editor settings behavior and JSON shape unchanged while moving JsonUtility storage into an infrastructure DTO and making the domain settings record init-only. Add the Unity IsExternalInit marker needed for init accessors in the Domain assembly. --- .../Editor/OnionAssemblyDependencyTests.cs | 7 ++- ...UnityCliLoopEditorSettingsRecoveryTests.cs | 59 ++++++++++++------ Packages/src/Editor/Domain/IsExternalInit.cs | 7 +++ .../src/Editor/Domain/IsExternalInit.cs.meta | 11 ++++ .../Domain/UnityCliLoopEditorSettingsData.cs | 19 +++--- .../UnityCliLoopEditorSettingsJsonData.cs | 60 +++++++++++++++++++ ...UnityCliLoopEditorSettingsJsonData.cs.meta | 11 ++++ .../UnityCliLoopEditorSettingsRepository.cs | 8 ++- 8 files changed, 149 insertions(+), 33 deletions(-) create mode 100644 Packages/src/Editor/Domain/IsExternalInit.cs create mode 100644 Packages/src/Editor/Domain/IsExternalInit.cs.meta create mode 100644 Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs create mode 100644 Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs.meta diff --git a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs index 67c7b582ae..bd8157839d 100644 --- a/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs +++ b/Assets/Tests/Editor/OnionAssemblyDependencyTests.cs @@ -206,18 +206,21 @@ public void ScriptChangesDuringPlayOptions_WhenLoaded_CompilesUnderDomainAssembl } [Test] - public void ToolSettings_WhenLoaded_PinPortsToDomainAndStorageDtoToInfrastructure() + public void Settings_WhenLoaded_PinPortsToDomainAndStorageDtosToInfrastructure() { - // Tests that tool-specific settings ports stay in the domain layer while JSON storage DTOs stay in infrastructure. + // Tests that settings ports and value objects stay in the domain layer while JSON storage DTOs stay in infrastructure. string editorSettingsDataAssemblyName = typeof(UnityCliLoopEditorSettingsData).Assembly.GetName().Name; string toolSettingsPortAssemblyName = typeof(IToolSettingsPort).Assembly.GetName().Name; string editorSettingsPortAssemblyName = typeof(IUnityCliLoopEditorSettingsPort).Assembly.GetName().Name; string toolSettingsDataAssemblyName = typeof(ToolSettingsData).Assembly.GetName().Name; + string editorSettingsJsonDataAssemblyName = + typeof(UnityCliLoopEditorSettingsJsonData).Assembly.GetName().Name; Assert.That(editorSettingsDataAssemblyName, Is.EqualTo(DomainAssemblyName)); Assert.That(toolSettingsPortAssemblyName, Is.EqualTo(DomainAssemblyName)); Assert.That(editorSettingsPortAssemblyName, Is.EqualTo(DomainAssemblyName)); Assert.That(toolSettingsDataAssemblyName, Is.EqualTo(InfrastructureAssemblyName)); + Assert.That(editorSettingsJsonDataAssemblyName, Is.EqualTo(InfrastructureAssemblyName)); } [Test] diff --git a/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs b/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs index 5869a1c57a..866c5188d5 100644 --- a/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs +++ b/Assets/Tests/Editor/UnityCliLoopEditorSettingsRecoveryTests.cs @@ -84,15 +84,14 @@ public void TearDown() public void RecoverSettingsFileIfNeeded_WhenPrimaryMissingAndBackupExists_ShouldRestoreBackup() { UnityCliLoopEditorSettingsData backupData = new() { showDeveloperTools = true }; - File.WriteAllText(BackupFilePath, JsonUtility.ToJson(backupData, true)); + File.WriteAllText(BackupFilePath, SerializeSettingsData(backupData)); _editorSettingsPort.RecoverSettingsFileIfNeeded(); Assert.IsTrue(File.Exists(SettingsFilePath), "Primary settings file should be restored from backup"); Assert.IsFalse(File.Exists(BackupFilePath), "Backup should be consumed after recovery"); - UnityCliLoopEditorSettingsData restored = JsonUtility.FromJson( - File.ReadAllText(SettingsFilePath)); + UnityCliLoopEditorSettingsJsonData restored = DeserializeSettingsJson(File.ReadAllText(SettingsFilePath)); Assert.AreEqual(backupData.showDeveloperTools, restored.showDeveloperTools); } @@ -101,8 +100,8 @@ public void RecoverSettingsFileIfNeeded_WhenPrimaryMissingAndTempExists_ShouldPr { UnityCliLoopEditorSettingsData oldData = new() { showDeveloperTools = false }; UnityCliLoopEditorSettingsData newData = new() { showDeveloperTools = true }; - File.WriteAllText(BackupFilePath, JsonUtility.ToJson(oldData, true)); - File.WriteAllText(TempFilePath, JsonUtility.ToJson(newData, true)); + File.WriteAllText(BackupFilePath, SerializeSettingsData(oldData)); + File.WriteAllText(TempFilePath, SerializeSettingsData(newData)); _editorSettingsPort.RecoverSettingsFileIfNeeded(); @@ -110,8 +109,7 @@ public void RecoverSettingsFileIfNeeded_WhenPrimaryMissingAndTempExists_ShouldPr Assert.IsFalse(File.Exists(BackupFilePath), "Backup should be removed after temp recovery"); Assert.IsFalse(File.Exists(TempFilePath), "Temp file should be consumed after recovery"); - UnityCliLoopEditorSettingsData restored = JsonUtility.FromJson( - File.ReadAllText(SettingsFilePath)); + UnityCliLoopEditorSettingsJsonData restored = DeserializeSettingsJson(File.ReadAllText(SettingsFilePath)); Assert.AreEqual(newData.showDeveloperTools, restored.showDeveloperTools); } @@ -119,17 +117,16 @@ public void RecoverSettingsFileIfNeeded_WhenPrimaryMissingAndTempExists_ShouldPr public void RecoverSettingsFileIfNeeded_WhenPrimaryExists_ShouldCleanStaleSidecars() { UnityCliLoopEditorSettingsData primaryData = new() { showDeveloperTools = true }; - File.WriteAllText(SettingsFilePath, JsonUtility.ToJson(primaryData, true)); - File.WriteAllText(BackupFilePath, JsonUtility.ToJson(new UnityCliLoopEditorSettingsData { showDeveloperTools = false }, true)); - File.WriteAllText(TempFilePath, JsonUtility.ToJson(new UnityCliLoopEditorSettingsData { showDeveloperTools = false }, true)); + File.WriteAllText(SettingsFilePath, SerializeSettingsData(primaryData)); + File.WriteAllText(BackupFilePath, SerializeSettingsData(new UnityCliLoopEditorSettingsData { showDeveloperTools = false })); + File.WriteAllText(TempFilePath, SerializeSettingsData(new UnityCliLoopEditorSettingsData { showDeveloperTools = false })); _editorSettingsPort.RecoverSettingsFileIfNeeded(); Assert.IsFalse(File.Exists(BackupFilePath), "Backup should not linger once primary exists"); Assert.IsFalse(File.Exists(TempFilePath), "Temp should not linger once primary exists"); - UnityCliLoopEditorSettingsData restored = JsonUtility.FromJson( - File.ReadAllText(SettingsFilePath)); + UnityCliLoopEditorSettingsJsonData restored = DeserializeSettingsJson(File.ReadAllText(SettingsFilePath)); Assert.AreEqual(primaryData.showDeveloperTools, restored.showDeveloperTools); } @@ -142,7 +139,7 @@ public void GetSettings_WhenLegacySetupWizardStateExists_ShouldMigrateVersionSta lastSeenSetupWizardVersion = "2.1.1", suppressSetupWizardAutoShow = false }; - File.WriteAllText(LegacySettingsFilePath, JsonUtility.ToJson(legacyData, true)); + File.WriteAllText(LegacySettingsFilePath, SerializeSettingsData(legacyData)); string lastSeenVersion = _editorSettingsPort.GetLastSeenSetupWizardVersion(); bool suppressAutoShow = _editorSettingsPort.GetSuppressSetupWizardAutoShow(); @@ -162,7 +159,7 @@ public void GetSettings_WhenLegacySetupWizardAutoShowWasSuppressed_ShouldMigrate lastSeenSetupWizardVersion = "2.1.1", suppressSetupWizardAutoShow = true }; - File.WriteAllText(LegacySettingsFilePath, JsonUtility.ToJson(legacyData, true)); + File.WriteAllText(LegacySettingsFilePath, SerializeSettingsData(legacyData)); bool suppressAutoShow = _editorSettingsPort.GetSuppressSetupWizardAutoShow(); @@ -184,8 +181,8 @@ public void GetSettings_WhenCurrentSetupWizardVersionExistsBeforeMigration_Shoul lastSeenSetupWizardVersion = "2.1.1", suppressSetupWizardAutoShow = false }; - File.WriteAllText(SettingsFilePath, JsonUtility.ToJson(currentData, true)); - File.WriteAllText(LegacySettingsFilePath, JsonUtility.ToJson(legacyData, true)); + File.WriteAllText(SettingsFilePath, SerializeSettingsData(currentData)); + File.WriteAllText(LegacySettingsFilePath, SerializeSettingsData(legacyData)); string lastSeenVersion = _editorSettingsPort.GetLastSeenSetupWizardVersion(); bool suppressAutoShow = _editorSettingsPort.GetSuppressSetupWizardAutoShow(); @@ -210,8 +207,8 @@ public void GetSettings_WhenLegacySetupWizardStateAlreadyMigrated_ShouldKeepCurr lastSeenSetupWizardVersion = "2.1.1", suppressSetupWizardAutoShow = true }; - File.WriteAllText(SettingsFilePath, JsonUtility.ToJson(currentData, true)); - File.WriteAllText(LegacySettingsFilePath, JsonUtility.ToJson(legacyData, true)); + File.WriteAllText(SettingsFilePath, SerializeSettingsData(currentData)); + File.WriteAllText(LegacySettingsFilePath, SerializeSettingsData(legacyData)); string lastSeenVersion = _editorSettingsPort.GetLastSeenSetupWizardVersion(); bool suppressAutoShow = _editorSettingsPort.GetSuppressSetupWizardAutoShow(); @@ -232,6 +229,22 @@ public void GetInstallSkillsFlat_WhenMissingFromSettings_DefaultsToTrue() Assert.IsTrue(installSkillsFlat); } + [Test] + public void GetSettings_WhenStringFieldIsExplicitNull_ShouldPreserveJsonUtilityEmptyStringMaterializationAndApplyMissingDefaults() + { + // Verifies that DTO-to-VO conversion preserves JsonUtility's empty-string materialization. + File.WriteAllText(SettingsFilePath, "{\"lastSeenSetupWizardVersion\":null}"); + _editorSettingsRepository.InvalidateCache(); + + UnityCliLoopEditorSettingsData settings = _editorSettingsPort.GetSettings(); + + Assert.That(settings.lastSeenSetupWizardVersion, Is.Empty); + Assert.That(settings.lastSeenSetupWizardMinimumDispatcherVersion, Is.Empty); + Assert.That(settings.showUnityCliLoopSecuritySetting, Is.True); + Assert.That(settings.showToolSettings, Is.True); + Assert.That(settings.installSkillsFlat, Is.True); + } + [Test] public void RecoverSettingsFileIfNeeded_WhenLegacyPortFieldsExist_ShouldRemoveThem() { @@ -327,6 +340,16 @@ private static void DeleteIfExists(string path) } } + private static string SerializeSettingsData(UnityCliLoopEditorSettingsData settings) + { + return JsonUtility.ToJson(UnityCliLoopEditorSettingsJsonData.FromDomain(settings), true); + } + + private static UnityCliLoopEditorSettingsJsonData DeserializeSettingsJson(string json) + { + return JsonUtility.FromJson(json); + } + /// /// Test support type used by editor and play mode fixtures. /// diff --git a/Packages/src/Editor/Domain/IsExternalInit.cs b/Packages/src/Editor/Domain/IsExternalInit.cs new file mode 100644 index 0000000000..3692db7061 --- /dev/null +++ b/Packages/src/Editor/Domain/IsExternalInit.cs @@ -0,0 +1,7 @@ +namespace System.Runtime.CompilerServices +{ + // Unity's .NET profile does not provide this marker type, but init accessors require it. + internal static class IsExternalInit + { + } +} diff --git a/Packages/src/Editor/Domain/IsExternalInit.cs.meta b/Packages/src/Editor/Domain/IsExternalInit.cs.meta new file mode 100644 index 0000000000..d7aa3c886a --- /dev/null +++ b/Packages/src/Editor/Domain/IsExternalInit.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 741f87577bc7e49629afddda7918d8e9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs b/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs index 4aec9a856d..10f042d7fd 100644 --- a/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs +++ b/Packages/src/Editor/Domain/UnityCliLoopEditorSettingsData.cs @@ -1,17 +1,14 @@ -using System; - namespace io.github.hatayama.UnityCliLoop.Domain { - [Serializable] public record UnityCliLoopEditorSettingsData { - public bool showDeveloperTools = false; - public string lastSeenSetupWizardVersion = ""; - public string lastSeenSetupWizardMinimumDispatcherVersion = ""; - public bool suppressSetupWizardAutoShow = false; - public bool legacySetupWizardStateMigrated = false; - public bool showUnityCliLoopSecuritySetting = true; - public bool showToolSettings = true; - public bool installSkillsFlat = true; + public bool showDeveloperTools { get; init; } = false; + public string lastSeenSetupWizardVersion { get; init; } = ""; + public string lastSeenSetupWizardMinimumDispatcherVersion { get; init; } = ""; + public bool suppressSetupWizardAutoShow { get; init; } = false; + public bool legacySetupWizardStateMigrated { get; init; } = false; + public bool showUnityCliLoopSecuritySetting { get; init; } = true; + public bool showToolSettings { get; init; } = true; + public bool installSkillsFlat { get; init; } = true; } } diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs new file mode 100644 index 0000000000..ef92633827 --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs @@ -0,0 +1,60 @@ +using System; +using System.Diagnostics; + +using io.github.hatayama.UnityCliLoop.Domain; + +namespace io.github.hatayama.UnityCliLoop.Infrastructure +{ + /// + /// JsonUtility storage DTO for Unity CLI Loop editor settings. + /// + [Serializable] + internal sealed class UnityCliLoopEditorSettingsJsonData + { + public bool showDeveloperTools = false; + public string lastSeenSetupWizardVersion = ""; + public string lastSeenSetupWizardMinimumDispatcherVersion = ""; + public bool suppressSetupWizardAutoShow = false; + public bool legacySetupWizardStateMigrated = false; + public bool showUnityCliLoopSecuritySetting = true; + public bool showToolSettings = true; + public bool installSkillsFlat = true; + + internal UnityCliLoopEditorSettingsData ToDomain() + { + return new UnityCliLoopEditorSettingsData + { + showDeveloperTools = showDeveloperTools, + lastSeenSetupWizardVersion = lastSeenSetupWizardVersion, + lastSeenSetupWizardMinimumDispatcherVersion = lastSeenSetupWizardMinimumDispatcherVersion, + suppressSetupWizardAutoShow = suppressSetupWizardAutoShow, + legacySetupWizardStateMigrated = legacySetupWizardStateMigrated, + showUnityCliLoopSecuritySetting = showUnityCliLoopSecuritySetting, + showToolSettings = showToolSettings, + installSkillsFlat = installSkillsFlat + }; + } + + internal static UnityCliLoopEditorSettingsJsonData FromDomain(UnityCliLoopEditorSettingsData settings) + { + Debug.Assert(settings != null, "settings must not be null"); + + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + return new UnityCliLoopEditorSettingsJsonData + { + showDeveloperTools = settings.showDeveloperTools, + lastSeenSetupWizardVersion = settings.lastSeenSetupWizardVersion, + lastSeenSetupWizardMinimumDispatcherVersion = settings.lastSeenSetupWizardMinimumDispatcherVersion, + suppressSetupWizardAutoShow = settings.suppressSetupWizardAutoShow, + legacySetupWizardStateMigrated = settings.legacySetupWizardStateMigrated, + showUnityCliLoopSecuritySetting = settings.showUnityCliLoopSecuritySetting, + showToolSettings = settings.showToolSettings, + installSkillsFlat = settings.installSkillsFlat + }; + } + } +} diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs.meta b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs.meta new file mode 100644 index 0000000000..01e62e3a8d --- /dev/null +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsJsonData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe2c1cc6743e74e32a4cb12b4e7c9b15 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs index 4e1ab1349a..c958200a28 100644 --- a/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs +++ b/Packages/src/Editor/Infrastructure/Settings/UnityCliLoopEditorSettingsRepository.cs @@ -86,7 +86,9 @@ public void SaveSettings(UnityCliLoopEditorSettingsData settings) Directory.CreateDirectory(directory); } - string json = JsonUtility.ToJson(settings, true); + UnityCliLoopEditorSettingsJsonData jsonData = + UnityCliLoopEditorSettingsJsonData.FromDomain(settings); + string json = JsonUtility.ToJson(jsonData, true); // Security: Validate JSON content size if (json.Length > UnityCliLoopConstants.MAX_SETTINGS_SIZE_BYTES) @@ -192,7 +194,9 @@ private void LoadSettings() throw new InvalidDataException("Settings file contains invalid JSON content"); } - _cachedSettings = JsonUtility.FromJson(json); + UnityCliLoopEditorSettingsJsonData loadedSettings = + JsonUtility.FromJson(json); + _cachedSettings = loadedSettings.ToDomain(); } else {