diff --git a/CHANGELOG.md b/CHANGELOG.md index 4449806..897c67c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Fixes ### MLEM.Data Additions - Allow RuntimeTexturePacker to automatically dispose submitted textures when packing +- Added JsonTypeSafeWrapper and JsonTypeSafeGenericDataHolder Improvements - Use TitleContainer for opening streams where possible diff --git a/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs b/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs new file mode 100644 index 0000000..bfb98b5 --- /dev/null +++ b/MLEM.Data/Json/JsonTypeSafeGenericDataHolder.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using MLEM.Misc; +using Newtonsoft.Json; + +namespace MLEM.Data.Json { + /// + /// An represents an object that can hold generic key-value based data. + /// This class uses for each object stored to ensure that objects with a custom get deserialized as an instance of their original type if is not set to . + /// + [DataContract] + public class JsonTypeSafeGenericDataHolder : IGenericDataHolder { + + [DataMember(EmitDefaultValue = false)] + private Dictionary data; + + /// + public void SetData(string key, T data) { + if (EqualityComparer.Default.Equals(data, default)) { + if (this.data != null) + this.data.Remove(key); + } else { + if (this.data == null) + this.data = new Dictionary(); + this.data[key] = new JsonTypeSafeWrapper(data); + } + } + + /// + public T GetData(string key) { + if (this.data != null && this.data.TryGetValue(key, out var val)) + return (T) val.Value; + return default; + } + + /// + public IEnumerable GetDataKeys() { + if (this.data == null) + return Array.Empty(); + return this.data.Keys; + } + + } +} \ No newline at end of file diff --git a/MLEM.Data/Json/JsonTypeSafeWrapper.cs b/MLEM.Data/Json/JsonTypeSafeWrapper.cs new file mode 100644 index 0000000..f6191ec --- /dev/null +++ b/MLEM.Data/Json/JsonTypeSafeWrapper.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Newtonsoft.Json; + +namespace MLEM.Data.Json { + /// + /// A json type-safe wrapper can be used to wrap any objects of any type before submitting them to a non-specifically typed or that will be serialized using a in cases where is not set to . + /// If any object is not wrapped in this manner, and it has a custom , the value deserialized from it might not have the same type as the originally serialized object. This behavior can be observed, for example, when serializing a of entries, one of which is a : The will be serialized as a and, upon deserialization, will remain a . + /// In general, wrapping objects in this manner is only useful in rare cases, where custom data of an unexpected or unknown type is stored. + /// See for an example of how this class and can be used, and see this stackoverflow answer for more information on the problem that this class solves: https://stackoverflow.com/a/38798114. + /// + public abstract class JsonTypeSafeWrapper { + + /// + /// Returns this json type-safe wrapper's value as an . + /// + public abstract object Value { get; } + + } + + /// + [DataContract] + public class JsonTypeSafeWrapper : JsonTypeSafeWrapper { + + /// + public override object Value => this.value; + [DataMember] + private readonly T value; + + /// + /// Creates a new json type-safe wrapper instance that wraps the given . + /// + /// The value to wrap + public JsonTypeSafeWrapper(T value) { + this.value = value; + } + + } +} \ No newline at end of file diff --git a/MLEM.Ui/Style/UiStyle.cs b/MLEM.Ui/Style/UiStyle.cs index 6901f05..47d9acb 100644 --- a/MLEM.Ui/Style/UiStyle.cs +++ b/MLEM.Ui/Style/UiStyle.cs @@ -11,7 +11,7 @@ namespace MLEM.Ui.Style { /// /// The style settings for a . /// Each uses these style settings by default, however you can also change these settings per element using the elements' individual style settings. - /// Note that this class is a , meaning additional styles for custom components can easily be added using + /// Note that this class is a , meaning additional styles for custom components can easily be added using /// public class UiStyle : GenericDataHolder { diff --git a/MLEM/Misc/GenericDataHolder.cs b/MLEM/Misc/GenericDataHolder.cs index c36a7e9..120d3d9 100644 --- a/MLEM/Misc/GenericDataHolder.cs +++ b/MLEM/Misc/GenericDataHolder.cs @@ -11,8 +11,8 @@ namespace MLEM.Misc { private Dictionary data; /// - public void SetData(string key, object data) { - if (data == default) { + public void SetData(string key, T data) { + if (EqualityComparer.Default.Equals(data, default)) { if (this.data != null) this.data.Remove(key); } else { @@ -30,7 +30,7 @@ namespace MLEM.Misc { } /// - public IReadOnlyCollection GetDataKeys() { + public IEnumerable GetDataKeys() { if (this.data == null) return Array.Empty(); return this.data.Keys; @@ -49,7 +49,8 @@ namespace MLEM.Misc { /// /// The key to store the data by /// The data to store in the object - void SetData(string key, object data); + /// The type of the data to store + void SetData(string key, T data); /// /// Returns a piece of generic data of the given type on this object. @@ -63,7 +64,7 @@ namespace MLEM.Misc { /// Returns all of the generic data that this object stores. /// /// The generic data on this object - IReadOnlyCollection GetDataKeys(); + IEnumerable GetDataKeys(); } } \ No newline at end of file diff --git a/Tests/DataTests.cs b/Tests/DataTests.cs index ba210e1..4d602f2 100644 --- a/Tests/DataTests.cs +++ b/Tests/DataTests.cs @@ -92,6 +92,31 @@ namespace Tests { Assert.AreEqual(Or(flags[24], flags[43]).ToString(), "Flag24 | Flag43"); } + [Test] + public void TestJsonTypeSafety() { + var serializer = new JsonSerializer {TypeNameHandling = TypeNameHandling.Auto}; + + // normal generic data holder should crush the time span down to a string due to its custom serializer + var data = new GenericDataHolder(); + data.SetData("Time", TimeSpan.FromMinutes(5)); + var read = SerializeAndDeserialize(serializer, data); + Assert.IsNotInstanceOf(read.GetData("Time")); + Assert.Throws(() => read.GetData("Time")); + + // json type safe generic data holder should wrap the time span to ensure that it stays a time span + var safeData = new JsonTypeSafeGenericDataHolder(); + safeData.SetData("Time", TimeSpan.FromMinutes(5)); + var safeRead = SerializeAndDeserialize(serializer, safeData); + Assert.IsInstanceOf(safeRead.GetData("Time")); + Assert.DoesNotThrow(() => safeRead.GetData("Time")); + } + + private static T SerializeAndDeserialize(JsonSerializer serializer, T t) { + var writer = new StringWriter(); + serializer.Serialize(writer, t); + return serializer.Deserialize(new JsonTextReader(new StringReader(writer.ToString()))); + } + private class TestObject { public Vector2 Vec;