diff --git a/src/GameCult.Caching/CultInspectorModel.cs b/src/GameCult.Caching/CultInspectorModel.cs index 7075c10a..7b6eaf84 100644 --- a/src/GameCult.Caching/CultInspectorModel.cs +++ b/src/GameCult.Caching/CultInspectorModel.cs @@ -345,14 +345,19 @@ namespace GameCult.Caching return new CultInspectorEdit(record, Clone(record.Document, record.Descriptor.DocumentType)); } + // Whether CreateDefault makes a value: a string, struct, list or dictionary, or a concrete class with a parameterless constructor. + public bool CanCreate(Type type) => + type == typeof(string) || type.IsValueType || + ShapeOf(type).Kind is CultInspectorValueKind.List or CultInspectorValueKind.Dictionary || + !type.IsAbstract && !type.IsInterface && type.GetConstructor(Type.EmptyTypes) != null; + public object? CreateDefault(Type type) { + if (!CanCreate(type)) return null; if (type == typeof(string)) return string.Empty; - if (type.IsValueType) return Activator.CreateInstance(type); var shape = ShapeOf(type); - if (shape.Kind == CultInspectorValueKind.List && type.IsArray) return Array.CreateInstance(shape.ElementType!, 0); - if (shape.Kind == CultInspectorValueKind.Dictionary) return Activator.CreateInstance(shape.BuildType!); - return type.IsAbstract || type.IsInterface || type.GetConstructor(Type.EmptyTypes) == null ? null : Activator.CreateInstance(type); + if (type.IsArray) return Array.CreateInstance(shape.ElementType!, 0); + return Activator.CreateInstance(shape.Kind == CultInspectorValueKind.Dictionary ? shape.BuildType! : type); } // What a new value for a slot of elementType (a list element, a dictionary value, a union pick) may be: a union's @@ -366,11 +371,10 @@ namespace GameCult.Caching // A new value of choice for a slot of elementType, or null with a notice when choice cannot be made. public object? CreateElement(Type elementType, Type choice, out string? notice) { - if (!ElementChoices(elementType).Contains(choice)) + // A slot's own type is always offered; an abstract one is refused with the notice below. + if (choice != elementType && !ElementChoices(elementType).Contains(choice)) throw new ArgumentException($"{choice.Name} is not a choice for {elementType.Name}.", nameof(choice)); - var created = choice == elementType ? CreateDefault(choice) - : choice.GetConstructor(Type.EmptyTypes) == null ? null - : Activator.CreateInstance(choice); + var created = CreateDefault(choice); // Null is a value only for a Nullable slot; anywhere else it is nothing made. notice = created == null && !choice.IsValueType ? $"{choice.Name} is abstract or has no parameterless constructor; nothing was created." diff --git a/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs b/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs index 5e6474dc..6c9fedf8 100644 --- a/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs +++ b/src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs @@ -138,9 +138,10 @@ namespace GameCult.Unity.Caching.Editor { var descriptor = _selectedType == null ? null : _cache.Registry.GetRequired(_selectedType); var selected = Selected(); + var cannotAdd = descriptor == null || ReadOnly || !_model.CanCreate(_selectedType); using (new EditorGUILayout.HorizontalScope()) { - using (new EditorGUI.DisabledScope(descriptor == null || descriptor.IsGlobal || ReadOnly)) + using (new EditorGUI.DisabledScope(cannotAdd || descriptor.IsGlobal)) { if (GUILayout.Button("Add", EditorStyles.miniButtonLeft)) Add(); } @@ -169,7 +170,7 @@ namespace GameCult.Unity.Caching.Editor using (new EditorGUILayout.HorizontalScope()) { EditorGUILayout.LabelField("Global is absent."); - using (new EditorGUI.DisabledScope(ReadOnly)) + using (new EditorGUI.DisabledScope(cannotAdd)) { if (GUILayout.Button("Create", GUILayout.Width(56))) Add(); } diff --git a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs index b4f22601..f30b63fe 100644 --- a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs +++ b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs @@ -85,6 +85,11 @@ namespace GameCult.Caching.Tests Assert.That(bare, Does.Contain("parameterless"), "a plain element type that cannot be made says so; nothing is added"); Assert.That(model.CreateElement(typeof(int?), typeof(int?), out var nullable), Is.Null); Assert.That(nullable, Is.Null, "null is a Nullable element, not a failure"); + + Assert.That(model.CreateElement(typeof(InspectShape), typeof(InspectShape), out var @abstract), Is.Null); + Assert.That(@abstract, Does.Contain("abstract"), "a union's own type cannot be made; it says so instead of throwing"); + Assert.That(new[] { typeof(InspectShape), typeof(InspectNeedsArgs), typeof(IList) }.Any(model.CanCreate), Is.False); + Assert.That(new[] { typeof(InspectItem), typeof(string), typeof(int[]), typeof(IDictionary), typeof(InspectOpenStruct) }.All(model.CanCreate), Is.True); } [Test]