ScopeJsonConverter.cs 11 KB

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