ScopeJsonConverter.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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 is { } && 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 is { } && 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!, _serializerContext);
  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 is { })
  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 is { })
  116. {
  117. PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!;
  118. prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, _serializerContext));
  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. object? prop = scope.GetType ().GetProperty (p.Name)?.GetValue (scope);
  142. JsonSerializer.Serialize (writer, prop, prop!.GetType (), _serializerContext);
  143. }
  144. foreach (KeyValuePair<string, ConfigProperty> p in from p in scope
  145. .Where (
  146. cp =>
  147. cp.Value.PropertyInfo?.GetCustomAttribute (
  148. typeof (
  149. SerializableConfigurationProperty)
  150. )
  151. is
  152. SerializableConfigurationProperty scp
  153. && scp?.Scope == typeof (scopeT)
  154. )
  155. where p.Value.PropertyValue != null
  156. select p)
  157. {
  158. writer.WritePropertyName (p.Key);
  159. Type? propertyType = p.Value.PropertyInfo?.PropertyType;
  160. if (propertyType != null
  161. && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute
  162. jca)
  163. {
  164. object converter = Activator.CreateInstance (jca.ConverterType!)!;
  165. if (converter.GetType ().BaseType == typeof (JsonConverterFactory))
  166. {
  167. var factory = (JsonConverterFactory)converter;
  168. if (factory.CanConvert (propertyType))
  169. {
  170. converter = factory.CreateConverter (propertyType, options)!;
  171. }
  172. }
  173. if (p.Value.PropertyValue is { })
  174. {
  175. converter.GetType ()
  176. .GetMethod ("Write")
  177. ?.Invoke (converter, new [] { writer, p.Value.PropertyValue, options });
  178. }
  179. }
  180. else
  181. {
  182. object? prop = p.Value.PropertyValue;
  183. JsonSerializer.Serialize (writer, prop, prop!.GetType (), _serializerContext);
  184. }
  185. }
  186. writer.WriteEndObject ();
  187. }
  188. // See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection
  189. internal abstract class ReadHelper
  190. {
  191. public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
  192. }
  193. internal class ReadHelper<converterT> : ReadHelper
  194. {
  195. private readonly ReadDelegate _readDelegate;
  196. public ReadHelper (object converter) { _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); }
  197. public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
  198. {
  199. return _readDelegate.Invoke (ref reader, type, options);
  200. }
  201. private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options);
  202. }
  203. }