ScopeJsonConverter.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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 ex)
  83. {
  84. // Logging.Trace ($"scopeT Read: {ex}");
  85. }
  86. }
  87. }
  88. else
  89. {
  90. // It is not a config property. Maybe it's just a property on the Scope with [JsonInclude]
  91. // like ScopeSettings.$schema...
  92. PropertyInfo? property = scope!.GetType ()
  93. .GetProperties ()
  94. .Where (
  95. p =>
  96. {
  97. var jia =
  98. p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as
  99. JsonIncludeAttribute;
  100. if (jia is { })
  101. {
  102. var jpna =
  103. p.GetCustomAttribute (
  104. typeof (JsonPropertyNameAttribute)
  105. ) as
  106. JsonPropertyNameAttribute;
  107. if (jpna?.Name == propertyName)
  108. {
  109. // Bit of a hack, modifying propertyName in an enumerator...
  110. propertyName = p.Name;
  111. return true;
  112. }
  113. return p.Name == propertyName;
  114. }
  115. return false;
  116. }
  117. )
  118. .FirstOrDefault ();
  119. if (property is { })
  120. {
  121. PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!;
  122. prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, SerializerContext));
  123. }
  124. else
  125. {
  126. // Unknown property
  127. throw new JsonException ($"Unknown property name \"{propertyName}\".");
  128. }
  129. }
  130. }
  131. throw new JsonException ();
  132. }
  133. public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options)
  134. {
  135. writer.WriteStartObject ();
  136. IEnumerable<PropertyInfo> properties = scope!.GetType ()
  137. .GetProperties ()
  138. .Where (
  139. p => p.GetCustomAttribute (typeof (JsonIncludeAttribute))
  140. != null
  141. );
  142. foreach (PropertyInfo p in properties)
  143. {
  144. writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
  145. object? prop = scope.GetType ().GetProperty (p.Name)?.GetValue (scope);
  146. JsonSerializer.Serialize (writer, prop, prop!.GetType (), SerializerContext);
  147. }
  148. foreach (KeyValuePair<string, ConfigProperty> p in from p in scope
  149. .Where (
  150. cp =>
  151. cp.Value.PropertyInfo?.GetCustomAttribute (
  152. typeof (
  153. SerializableConfigurationProperty)
  154. )
  155. is
  156. SerializableConfigurationProperty scp
  157. && scp?.Scope == typeof (scopeT)
  158. )
  159. where p.Value.PropertyValue != null
  160. select p)
  161. {
  162. writer.WritePropertyName (p.Key);
  163. Type? propertyType = p.Value.PropertyInfo?.PropertyType;
  164. if (propertyType != null
  165. && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute
  166. jca)
  167. {
  168. object converter = Activator.CreateInstance (jca.ConverterType!)!;
  169. if (converter.GetType ().BaseType == typeof (JsonConverterFactory))
  170. {
  171. var factory = (JsonConverterFactory)converter;
  172. if (factory.CanConvert (propertyType))
  173. {
  174. converter = factory.CreateConverter (propertyType, options)!;
  175. }
  176. }
  177. if (p.Value.PropertyValue is { })
  178. {
  179. converter.GetType ()
  180. .GetMethod ("Write")
  181. ?.Invoke (converter, new [] { writer, p.Value.PropertyValue, options });
  182. }
  183. }
  184. else
  185. {
  186. object? prop = p.Value.PropertyValue;
  187. JsonSerializer.Serialize (writer, prop, prop!.GetType (), SerializerContext);
  188. }
  189. }
  190. writer.WriteEndObject ();
  191. }
  192. // See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
  193. internal abstract class ReadHelper
  194. {
  195. public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
  196. }
  197. internal class ReadHelper<converterT> : ReadHelper
  198. {
  199. private readonly ReadDelegate _readDelegate;
  200. [RequiresUnreferencedCode ("Calls System.Delegate.CreateDelegate(Type, Object, String)")]
  201. public ReadHelper (object converter) { _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); }
  202. public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
  203. {
  204. return _readDelegate.Invoke (ref reader, type, options);
  205. }
  206. private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
  207. }
  208. }