ScopeJsonConverter.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. #nullable enable
  2. using System.Diagnostics;
  3. using System.Diagnostics.CodeAnalysis;
  4. using System.Reflection;
  5. using System.Text.Json;
  6. using System.Text.Json.Serialization;
  7. namespace Terminal.Gui;
  8. /// <summary>
  9. /// Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing config
  10. /// data to/from <see cref="ConfigurationManager"/> JSON documents.
  11. /// </summary>
  12. /// <typeparam name="scopeT"></typeparam>
  13. internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT>
  14. {
  15. [RequiresDynamicCode ("Calls System.Type.MakeGenericType(params Type[])")]
  16. #pragma warning disable IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.
  17. public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  18. #pragma warning restore IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides.
  19. {
  20. if (reader.TokenType != JsonTokenType.StartObject)
  21. {
  22. throw new JsonException (
  23. $"Expected a JSON object (\"{{ \"propName\" : ... }}\"), but got \"{reader.TokenType}\"."
  24. );
  25. }
  26. var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!;
  27. while (reader.Read ())
  28. {
  29. if (reader.TokenType == JsonTokenType.EndObject)
  30. {
  31. return scope!;
  32. }
  33. if (reader.TokenType != JsonTokenType.PropertyName)
  34. {
  35. throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\".");
  36. }
  37. string? propertyName = reader.GetString ();
  38. reader.Read ();
  39. if (propertyName is { } && scope!.TryGetValue (propertyName, out ConfigProperty? configProp))
  40. {
  41. // This property name was found in the Scope's ScopeProperties dictionary
  42. // Figure out if it needs a JsonConverter and if so, create one
  43. Type? propertyType = configProp?.PropertyInfo?.PropertyType!;
  44. if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is
  45. JsonConverterAttribute jca)
  46. {
  47. object? converter = Activator.CreateInstance (jca.ConverterType!)!;
  48. if (converter.GetType ().BaseType == typeof (JsonConverterFactory))
  49. {
  50. var factory = (JsonConverterFactory)converter;
  51. if (propertyType is { } && factory.CanConvert (propertyType))
  52. {
  53. converter = factory.CreateConverter (propertyType, options);
  54. }
  55. }
  56. var readHelper = Activator.CreateInstance (
  57. (Type?)typeof (ReadHelper<>).MakeGenericType (
  58. typeof (scopeT),
  59. propertyType!
  60. )!,
  61. converter
  62. ) as ReadHelper;
  63. try
  64. {
  65. scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options);
  66. }
  67. catch (NotSupportedException e)
  68. {
  69. throw new JsonException (
  70. $"Error reading property \"{propertyName}\" of type \"{propertyType?.Name}\".",
  71. e
  72. );
  73. }
  74. }
  75. else
  76. {
  77. try
  78. {
  79. scope! [propertyName].PropertyValue =
  80. JsonSerializer.Deserialize (ref reader, propertyType!, SerializerContext);
  81. }
  82. catch (Exception)
  83. {
  84. // Logging.Trace ($"scopeT Read: {ex}");
  85. }
  86. }
  87. //Logging.Warning ($"{propertyName} = {scope! [propertyName].PropertyValue}");
  88. }
  89. else
  90. {
  91. // It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
  92. // like ScopeSettings.$schema...
  93. PropertyInfo? property = scope!.GetType ()
  94. .GetProperties ()
  95. .Where (
  96. p =>
  97. {
  98. var jia =
  99. p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as
  100. JsonIncludeAttribute;
  101. if (jia is { })
  102. {
  103. var jpna =
  104. p.GetCustomAttribute (
  105. typeof (JsonPropertyNameAttribute)
  106. ) as
  107. JsonPropertyNameAttribute;
  108. if (jpna?.Name == propertyName)
  109. {
  110. // Bit of a hack, modifying propertyName in an enumerator...
  111. propertyName = p.Name;
  112. return true;
  113. }
  114. return p.Name == propertyName;
  115. }
  116. return false;
  117. }
  118. )
  119. .FirstOrDefault ();
  120. if (property is { })
  121. {
  122. PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!;
  123. prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, SerializerContext));
  124. }
  125. else
  126. {
  127. // Unknown property
  128. throw new JsonException ($"Unknown property name \"{propertyName}\".");
  129. }
  130. }
  131. }
  132. throw new JsonException ("ScopeJsonConverter");
  133. }
  134. public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
  135. {
  136. writer.WriteStartObject ();
  137. IEnumerable<PropertyInfo> properties = scope!.GetType ()
  138. .GetProperties ()
  139. .Where (
  140. p => p.GetCustomAttribute (typeof (JsonIncludeAttribute))
  141. != null
  142. );
  143. foreach (PropertyInfo p in properties)
  144. {
  145. writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
  146. object? prop = scope.GetType ().GetProperty (p.Name)?.GetValue (scope);
  147. JsonSerializer.Serialize (writer, prop, prop!.GetType (), SerializerContext);
  148. }
  149. foreach (KeyValuePair<string, ConfigProperty> p in from p in scope
  150. .Where (
  151. cp =>
  152. cp.Value.PropertyInfo?.GetCustomAttribute (
  153. typeof (
  154. SerializableConfigurationProperty)
  155. )
  156. is
  157. SerializableConfigurationProperty scp
  158. && scp?.Scope == typeof (scopeT)
  159. )
  160. where p.Value.PropertyValue != null
  161. select p)
  162. {
  163. writer.WritePropertyName (p.Key);
  164. Type? propertyType = p.Value.PropertyInfo?.PropertyType;
  165. if (propertyType != null
  166. && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute
  167. jca)
  168. {
  169. object converter = Activator.CreateInstance (jca.ConverterType!)!;
  170. if (converter.GetType ().BaseType == typeof (JsonConverterFactory))
  171. {
  172. var factory = (JsonConverterFactory)converter;
  173. if (factory.CanConvert (propertyType))
  174. {
  175. converter = factory.CreateConverter (propertyType, options)!;
  176. }
  177. }
  178. if (p.Value.PropertyValue is { })
  179. {
  180. converter.GetType ()
  181. .GetMethod ("Write")
  182. ?.Invoke (converter, new [] { writer, p.Value.PropertyValue, options });
  183. }
  184. }
  185. else
  186. {
  187. object? prop = p.Value.PropertyValue;
  188. JsonSerializer.Serialize (writer, prop, prop!.GetType (), SerializerContext);
  189. }
  190. }
  191. writer.WriteEndObject ();
  192. }
  193. // See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
  194. internal abstract class ReadHelper
  195. {
  196. public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
  197. }
  198. internal class ReadHelper<converterT> : ReadHelper
  199. {
  200. private readonly ReadDelegate _readDelegate;
  201. [RequiresUnreferencedCode ("Calls System.Delegate.CreateDelegate(Type, Object, String)")]
  202. public ReadHelper (object converter) { _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); }
  203. public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
  204. {
  205. return _readDelegate.Invoke (ref reader, type, options);
  206. }
  207. private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
  208. }
  209. }