ScopeJsonConverter.cs 5.9 KB

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