using System; using System.Collections.Generic; using System.Reflection; using Newtonsoft.Json; #if NET6_0_OR_GREATER using System.Diagnostics.CodeAnalysis; #endif namespace MLEM.Data.Json { /// /// A that doesn't actually serialize the object, but instead serializes the name given to it by the underlying . /// Optionally, the name of a can be passed to this converter when used in the by passing the arguments for the constructor as . /// /// The type of the object to convert public class StaticJsonConverter : JsonConverter { /// /// The entries, or registry, that this static json converter uses. /// protected readonly Dictionary Entries; /// /// The inverse of this static json converter's , mapping the values to their respective keys. /// Note that this dictionary is only created once and is not automatically updated should the be modified. /// protected readonly Dictionary Inverse; /// /// Whether to throw a in if a key is missing, or throw a if a stored json value is not a string. /// If this is , returns instead. /// protected readonly bool ThrowOnRead; /// /// Creates a new static json converter using the given underlying . /// /// The entries, or registry, that this static json converter uses. /// Whether to throw a in if a key is missing, or throw a if a stored json value is not a string. If this is , returns instead. public StaticJsonConverter(Dictionary entries, bool throwOnRead = false) { this.Entries = entries; this.Inverse = StaticJsonConverter.CreateInverse(entries); this.ThrowOnRead = throwOnRead; } /// /// Creates a new static json converter by finding the underlying from the given type and member name /// /// The type that the dictionary is declared in /// The name of the dictionary itself public StaticJsonConverter( #if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] #endif Type type, string memberName) : this(StaticJsonConverter.GetEntries(type, memberName)) {} /// /// Creates a new static json converter by finding the underlying from the given type and member name /// /// The type that the dictionary is declared in /// The name of the dictionary itself /// Whether to throw a in if a key is missing, or throw a if a stored json value is not a string. If this is , returns instead. public StaticJsonConverter( #if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] #endif Type type, string memberName, bool throwOnRead) : this(StaticJsonConverter.GetEntries(type, memberName), throwOnRead) {} /// Writes the JSON representation of the object. /// The to write to. /// The value. /// The calling serializer. public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer) { if (value == null) { writer.WriteNull(); return; } if (!this.Inverse.TryGetValue(value, out var key)) throw new KeyNotFoundException($"Cannot write {value} that is not a registered entry"); writer.WriteValue(key); } /// Reads the JSON representation of the object. /// The to read from. /// Type of the object. /// The existing value of object being read. If there is no existing value then null will be used. /// The existing value has a value. /// The calling serializer. /// The object value. public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer) { if (reader.Value == null) return default; if (reader.TokenType != JsonToken.String) { if (this.ThrowOnRead) throw new JsonSerializationException($"Expected a string value for {reader.Value}, got a {reader.TokenType}"); return default; } if (!this.Entries.TryGetValue((string) reader.Value, out var ret) && this.ThrowOnRead) throw new KeyNotFoundException($"Could not find registered entry for {reader.Value}"); return ret; } /// /// Returns the entries, or registry, that this static json converter should use, using reflection to find the passed in the passed . /// /// The type to find the entries in. /// The name of the field or property that the entries are in within the given . /// The found property or field's value. /// Thrown if there is no property or field with the in the given . /// Thrown if the value of the is not of the expected type. protected static Dictionary GetEntries( #if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties | DynamicallyAccessedMemberTypes.PublicFields | DynamicallyAccessedMemberTypes.NonPublicFields)] #endif Type type, string memberName) { const BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic; var value = type.GetProperty(memberName, flags)?.GetValue(null) ?? type.GetField(memberName, flags)?.GetValue(null); if (value == null) throw new ArgumentException($"There is no property or field value for name {memberName}", nameof(memberName)); return value as Dictionary ?? throw new InvalidCastException($"{value} is not of expected type {typeof(T)}"); } /// /// Creates the inverse version of the passed , mapping the values to their keys, and throws an exception if there are any duplicate values. /// /// The entries to create an inverse of. /// The inverse of the given , mapping the values to their keys. /// Thrown if the contains duplicate values, making it impossible to create a valid inverse. protected static Dictionary CreateInverse(Dictionary entries) { var ret = new Dictionary(); foreach (var entry in entries) { if (ret.ContainsKey(entry.Value)) throw new ArgumentException($"Cannot create a static json converter with duplicate value {entry.Value}"); ret.Add(entry.Value, entry.Key); } return ret; } } }