2
0

Scope.cs 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Diagnostics.CodeAnalysis;
  4. using System.Text.Json;
  5. namespace Terminal.Gui.Configuration;
  6. /// <summary>
  7. /// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define
  8. /// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings.
  9. /// <para>
  10. /// When constructed, the dictionary will be populated with uninitialized configuration properties for the
  11. /// scope (<see cref="ConfigProperty.HasValue"/> will be <see langword="false"/>).
  12. /// </para>
  13. /// </summary>
  14. public class Scope<T> : ConcurrentDictionary<string, ConfigProperty>
  15. {
  16. /// <summary>
  17. /// Creates a new instance. The dictionary will be populated with uninitialized (<see cref="ConfigProperty.HasValue"/>
  18. /// will be <see langword="false"/>).
  19. /// </summary>
  20. [RequiresUnreferencedCode (
  21. "Uses cached configuration properties filtered by type T. This is AOT-safe as long as T is one of the known scope types (SettingsScope, ThemeScope, AppSettingsScope).")]
  22. public Scope () : base (StringComparer.InvariantCultureIgnoreCase)
  23. {
  24. }
  25. /// <summary>
  26. /// INTERNAL: Adds a new ConfigProperty given a <paramref name="value"/>. Determines the correct PropertyInfo etc... by retrieving the
  27. /// hard coded value for <paramref name="name"/>.
  28. /// </summary>
  29. /// <param name="name"></param>
  30. /// <param name="value"></param>
  31. internal void AddValue (string name, object? value)
  32. {
  33. ConfigProperty? configProperty = GetHardCodedProperty (name);
  34. if (configProperty is null)
  35. {
  36. throw new InvalidOperationException ($@"{name} is not a hard coded property.");
  37. }
  38. TryAdd (name, ConfigProperty.CreateCopy (configProperty));
  39. this [name].PropertyValue = configProperty.PropertyValue;
  40. }
  41. internal ConfigProperty? GetHardCodedProperty (string name)
  42. {
  43. ConfigProperty? configProperty = ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!
  44. .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name).Value;
  45. if (configProperty is null)
  46. {
  47. return null;
  48. }
  49. ConfigProperty copy = ConfigProperty.CreateCopy (configProperty);
  50. copy.PropertyValue = configProperty.PropertyValue;
  51. return copy;
  52. }
  53. internal ConfigProperty GetUninitializedProperty (string name)
  54. {
  55. ConfigProperty? configProperty = ConfigurationManager.GetUninitializedConfigPropertiesByScope (typeof (T).Name)!
  56. .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name).Value;
  57. if (configProperty is null)
  58. {
  59. throw new InvalidOperationException ($@"{name} is not a ConfigProperty.");
  60. }
  61. ConfigProperty copy = ConfigProperty.CreateCopy (configProperty);
  62. copy.PropertyValue = configProperty.PropertyValue;
  63. return copy;
  64. }
  65. /// <summary>
  66. /// INTERNAL: Updates the values of the properties of this scope to their corresponding static
  67. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  68. /// </summary>
  69. [RequiresDynamicCode ("Uses reflection to retrieve property values")]
  70. internal void LoadCurrentValues ()
  71. {
  72. foreach (KeyValuePair<string, ConfigProperty> validProperties in this.Where (cp => cp.Value.PropertyInfo is { }))
  73. {
  74. validProperties.Value.UpdateToCurrentValue ();
  75. }
  76. }
  77. /// <summary>
  78. /// INTERNAL: Updates the values of all properties of this scope to their corresponding hard-coded original values.
  79. /// </summary>
  80. internal void LoadHardCodedDefaults ()
  81. {
  82. foreach (KeyValuePair<string, ConfigProperty> hardCodedKeyValuePair in ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!)
  83. {
  84. ConfigProperty copy = ConfigProperty.CreateCopy (hardCodedKeyValuePair.Value);
  85. TryAdd (hardCodedKeyValuePair.Key, copy);
  86. this [hardCodedKeyValuePair.Key].PropertyValue = hardCodedKeyValuePair.Value.PropertyValue;
  87. }
  88. }
  89. /// <summary>
  90. /// INTERNAL: Updates this scope with the values in <paramref name="scope"/> using a deep clone.
  91. /// </summary>
  92. /// <param name="scope"></param>
  93. /// <returns>The updated scope (this).</returns>
  94. [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  95. [RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  96. internal Scope<T>? UpdateFrom (Scope<T> scope)
  97. {
  98. foreach (KeyValuePair<string, ConfigProperty> prop in scope)
  99. {
  100. if (!prop.Value.HasValue)
  101. {
  102. continue;
  103. }
  104. if (!ContainsKey (prop.Key))
  105. {
  106. if (!prop.Value.HasValue)
  107. {
  108. continue;
  109. }
  110. // Add an empty (HasValue = false) property to this scope
  111. ConfigProperty copy = ConfigProperty.CreateCopy (prop.Value);
  112. copy.PropertyValue = prop.Value.PropertyValue;
  113. TryAdd (prop.Key, copy);
  114. }
  115. // Update the property value
  116. this [prop.Key].UpdateFrom (prop.Value.PropertyValue);
  117. }
  118. return this;
  119. }
  120. /// <summary>
  121. /// INTERNAL: Applies the values of the properties of this scope to their corresponding
  122. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  123. /// </summary>
  124. /// <returns><see langword="true"/> if one or more property value was applied; <see langword="false"/> otherwise.</returns>
  125. [RequiresDynamicCode ("Uses reflection to get and set property values")]
  126. [RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepClone<T>(T)")]
  127. internal bool Apply ()
  128. {
  129. if (!ConfigurationManager.IsEnabled)
  130. {
  131. Logging.Warning ("Apply called when CM is not Enabled. This should only be done from unit tests where side-effects are managed.");
  132. }
  133. var set = false;
  134. foreach (KeyValuePair<string, ConfigProperty> propWithValue in this.Where (t => t.Value.HasValue))
  135. {
  136. if (propWithValue.Value.PropertyInfo != null)
  137. {
  138. object? currentValue = propWithValue.Value.PropertyInfo.GetValue (null);
  139. // QUESTION: Should we avoid setting if currentValue == newValue?
  140. if (propWithValue.Value.PropertyValue is Scope<T> scopeSource && currentValue is Scope<T> scopeDest)
  141. {
  142. propWithValue.Value.PropertyInfo.SetValue (null, scopeDest.UpdateFrom (scopeSource));
  143. }
  144. else
  145. {
  146. // Use DeepCloner to create a deep copy of the property value
  147. object? val = DeepCloner.DeepClone (propWithValue.Value.PropertyValue);
  148. propWithValue.Value.PropertyInfo.SetValue (null, val);
  149. }
  150. set = true;
  151. }
  152. }
  153. return set;
  154. }
  155. internal virtual void Validate ()
  156. {
  157. if (IsEmpty)
  158. {
  159. //throw new JsonException ($@"Empty!");
  160. }
  161. }
  162. }