1
0
Fork 0
mirror of https://github.com/Ellpeck/MLEM.git synced 2024-05-10 03:28:43 +02:00

Added JsonTypeSafeWrapper and JsonTypeSafeGenericDataHolder

This commit is contained in:
Ell 2021-11-06 23:38:21 +01:00
parent 778d416774
commit 8e83cc06a6
6 changed files with 118 additions and 6 deletions

View file

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

View file

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using MLEM.Misc;
using Newtonsoft.Json;
namespace MLEM.Data.Json {
/// <summary>
/// An <see cref="IGenericDataHolder"/> represents an object that can hold generic key-value based data.
/// This class uses <see cref="JsonTypeSafeWrapper"/> for each object stored to ensure that objects with a custom <see cref="JsonConverter"/> get deserialized as an instance of their original type if <see cref="JsonSerializer.TypeNameHandling"/> is not set to <see cref="TypeNameHandling.None"/>.
/// </summary>
[DataContract]
public class JsonTypeSafeGenericDataHolder : IGenericDataHolder {
[DataMember(EmitDefaultValue = false)]
private Dictionary<string, JsonTypeSafeWrapper> data;
/// <inheritdoc />
public void SetData<T>(string key, T data) {
if (EqualityComparer<T>.Default.Equals(data, default)) {
if (this.data != null)
this.data.Remove(key);
} else {
if (this.data == null)
this.data = new Dictionary<string, JsonTypeSafeWrapper>();
this.data[key] = new JsonTypeSafeWrapper<T>(data);
}
}
/// <inheritdoc />
public T GetData<T>(string key) {
if (this.data != null && this.data.TryGetValue(key, out var val))
return (T) val.Value;
return default;
}
/// <inheritdoc />
public IEnumerable<string> GetDataKeys() {
if (this.data == null)
return Array.Empty<string>();
return this.data.Keys;
}
}
}

View file

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using Newtonsoft.Json;
namespace MLEM.Data.Json {
/// <summary>
/// A json type-safe wrapper can be used to wrap any objects of any type before submitting them to a non-specifically typed <see cref="List{T}"/> or <see cref="Dictionary{T,T}"/> that will be serialized using a <see cref="JsonSerializer"/> in cases where <see cref="JsonSerializer.TypeNameHandling"/> is not set to <see cref="TypeNameHandling.None"/>.
/// If any object is not wrapped in this manner, and it has a custom <see cref="JsonConverter"/>, 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 <see cref="List{T}"/> of <see cref="object"/> entries, one of which is a <see cref="TimeSpan"/>: The <see cref="TimeSpan"/> will be serialized as a <see cref="string"/> and, upon deserialization, will remain a <see cref="string"/>.
/// 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 <see cref="JsonTypeSafeGenericDataHolder"/> for an example of how this class and <see cref="JsonTypeSafeWrapper{T}"/> can be used, and see this stackoverflow answer for more information on the problem that this class solves: https://stackoverflow.com/a/38798114.
/// </summary>
public abstract class JsonTypeSafeWrapper {
/// <summary>
/// Returns this json type-safe wrapper's value as an <see cref="object"/>.
/// </summary>
public abstract object Value { get; }
}
/// <inheritdoc />
[DataContract]
public class JsonTypeSafeWrapper<T> : JsonTypeSafeWrapper {
/// <inheritdoc />
public override object Value => this.value;
[DataMember]
private readonly T value;
/// <summary>
/// Creates a new json type-safe wrapper instance that wraps the given <paramref name="value"/>.
/// </summary>
/// <param name="value">The value to wrap</param>
public JsonTypeSafeWrapper(T value) {
this.value = value;
}
}
}

View file

@ -11,7 +11,7 @@ namespace MLEM.Ui.Style {
/// <summary>
/// The style settings for a <see cref="UiSystem"/>.
/// Each <see cref="Element"/> 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 <see cref="GenericDataHolder"/>, meaning additional styles for custom components can easily be added using <see cref="GenericDataHolder.SetData"/>
/// Note that this class is a <see cref="GenericDataHolder"/>, meaning additional styles for custom components can easily be added using <see cref="GenericDataHolder.SetData{T}"/>
/// </summary>
public class UiStyle : GenericDataHolder {

View file

@ -11,8 +11,8 @@ namespace MLEM.Misc {
private Dictionary<string, object> data;
/// <inheritdoc />
public void SetData(string key, object data) {
if (data == default) {
public void SetData<T>(string key, T data) {
if (EqualityComparer<T>.Default.Equals(data, default)) {
if (this.data != null)
this.data.Remove(key);
} else {
@ -30,7 +30,7 @@ namespace MLEM.Misc {
}
/// <inheritdoc />
public IReadOnlyCollection<string> GetDataKeys() {
public IEnumerable<string> GetDataKeys() {
if (this.data == null)
return Array.Empty<string>();
return this.data.Keys;
@ -49,7 +49,8 @@ namespace MLEM.Misc {
/// </summary>
/// <param name="key">The key to store the data by</param>
/// <param name="data">The data to store in the object</param>
void SetData(string key, object data);
/// <typeparam name="T">The type of the data to store</typeparam>
void SetData<T>(string key, T data);
/// <summary>
/// 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.
/// </summary>
/// <returns>The generic data on this object</returns>
IReadOnlyCollection<string> GetDataKeys();
IEnumerable<string> GetDataKeys();
}
}

View file

@ -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<TimeSpan>(read.GetData<object>("Time"));
Assert.Throws<InvalidCastException>(() => read.GetData<TimeSpan>("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<TimeSpan>(safeRead.GetData<object>("Time"));
Assert.DoesNotThrow(() => safeRead.GetData<TimeSpan>("Time"));
}
private static T SerializeAndDeserialize<T>(JsonSerializer serializer, T t) {
var writer = new StringWriter();
serializer.Serialize(writer, t);
return serializer.Deserialize<T>(new JsonTextReader(new StringReader(writer.ToString())));
}
private class TestObject {
public Vector2 Vec;