Scope.cs 8.5 KB

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