Scope.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #nullable enable
  2. using System.Collections.Concurrent;
  3. using System.Diagnostics;
  4. using System.Diagnostics.CodeAnalysis;
  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. /// <summary>
  24. /// INTERNAL: Adds a new ConfigProperty given a <paramref name="value"/>. Determines the correct PropertyInfo etc... by
  25. /// retrieving the
  26. /// hard coded value for <paramref name="name"/>.
  27. /// </summary>
  28. /// <param name="name"></param>
  29. /// <param name="value"></param>
  30. internal void AddValue (string name, object? value)
  31. {
  32. ConfigProperty? configProperty = GetHardCodedProperty (name);
  33. if (configProperty is null)
  34. {
  35. throw new InvalidOperationException ($@"{name} is not a hard coded property.");
  36. }
  37. TryAdd (name, ConfigProperty.CreateCopy (configProperty));
  38. this [name].PropertyValue = configProperty.PropertyValue;
  39. }
  40. internal ConfigProperty? GetHardCodedProperty (string name)
  41. {
  42. ConfigProperty? configProperty = ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!
  43. .FirstOrDefault (hardCodedKeyValuePair => hardCodedKeyValuePair.Key == name)
  44. .Value;
  45. if (configProperty is null)
  46. {
  47. return null;
  48. }
  49. var 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)
  57. .Value;
  58. if (configProperty is null)
  59. {
  60. throw new InvalidOperationException ($@"{name} is not a ConfigProperty.");
  61. }
  62. var copy = ConfigProperty.CreateCopy (configProperty);
  63. copy.PropertyValue = configProperty.PropertyValue;
  64. return copy;
  65. }
  66. /// <summary>
  67. /// INTERNAL: Updates the values of the properties of this scope to their corresponding static
  68. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  69. /// </summary>
  70. [RequiresDynamicCode ("Uses reflection to retrieve property values")]
  71. internal void UpdateToCurrentValues ()
  72. {
  73. foreach (KeyValuePair<string, ConfigProperty> validProperties in this.Where (cp => cp.Value.PropertyInfo is { }))
  74. {
  75. validProperties.Value.UpdateToCurrentValue ();
  76. }
  77. }
  78. /// <summary>
  79. /// INTERNAL: Updates the values of all properties of this scope to their corresponding hard-coded original values.
  80. /// </summary>
  81. internal void LoadHardCodedDefaults ()
  82. {
  83. foreach (KeyValuePair<string, ConfigProperty> hardCodedKeyValuePair in ConfigurationManager.GetHardCodedConfigPropertiesByScope (typeof (T).Name)!)
  84. {
  85. var copy = ConfigProperty.CreateCopy (hardCodedKeyValuePair.Value);
  86. TryAdd (hardCodedKeyValuePair.Key, copy);
  87. this [hardCodedKeyValuePair.Key].PropertyValue = hardCodedKeyValuePair.Value.PropertyValue;
  88. }
  89. }
  90. /// <summary>
  91. /// INTERNAL: Updates this scope with the values in <paramref name="scope"/> using a deep clone.
  92. /// </summary>
  93. /// <param name="scope"></param>
  94. /// <returns>The updated scope (this).</returns>
  95. [RequiresUnreferencedCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  96. [RequiresDynamicCode ("Calls Terminal.Gui.ConfigProperty.UpdateFrom(Object)")]
  97. internal Scope<T>? UpdateFrom (Scope<T> scope)
  98. {
  99. foreach (KeyValuePair<string, ConfigProperty> prop in scope)
  100. {
  101. if (!prop.Value.HasValue)
  102. {
  103. continue;
  104. }
  105. if (!ContainsKey (prop.Key))
  106. {
  107. if (!prop.Value.HasValue)
  108. {
  109. continue;
  110. }
  111. // Add an empty (HasValue = false) property to this scope
  112. var copy = ConfigProperty.CreateCopy (prop.Value);
  113. copy.PropertyValue = prop.Value.PropertyValue;
  114. TryAdd (prop.Key, copy);
  115. }
  116. // Update the property value
  117. this [prop.Key].UpdateFrom (prop.Value.PropertyValue);
  118. }
  119. return this;
  120. }
  121. /// <summary>
  122. /// INTERNAL: Applies the values of the properties of this scope to their corresponding
  123. /// <see cref="ConfigurationPropertyAttribute"/> properties.
  124. /// </summary>
  125. /// <returns><see langword="true"/> if one or more property value was applied; <see langword="false"/> otherwise.</returns>
  126. [RequiresDynamicCode ("Uses reflection to get and set property values")]
  127. [RequiresUnreferencedCode ("Calls Terminal.Gui.DeepCloner.DeepClone<T>(T)")]
  128. internal bool Apply ()
  129. {
  130. if (!ConfigurationManager.IsEnabled)
  131. {
  132. Logging.Warning ("Apply called when CM is not Enabled. This should only be done from unit tests where side-effects are managed.");
  133. }
  134. var set = false;
  135. foreach (KeyValuePair<string, ConfigProperty> propWithValue in this.Where (t => t.Value.HasValue))
  136. {
  137. if (propWithValue.Value.PropertyInfo != null)
  138. {
  139. object? currentValue = propWithValue.Value.PropertyInfo.GetValue (null);
  140. object? newValue = null;
  141. // QUESTION: Should we avoid setting if currentValue == newValue?
  142. if (propWithValue.Value.PropertyValue is Scope<T> scopeSource && currentValue is Scope<T> scopeDest)
  143. {
  144. newValue = scopeDest.UpdateFrom (scopeSource);
  145. }
  146. else
  147. {
  148. // Use DeepCloner to create a deep copy of the property value
  149. newValue = DeepCloner.DeepClone (propWithValue.Value.PropertyValue);
  150. }
  151. // Logging.Debug($"{propWithValue.Key}: {currentValue} -> {newValue}");
  152. Debug.Assert (!propWithValue.Value.Immutable);
  153. propWithValue.Value.PropertyInfo.SetValue (null, newValue);
  154. set = true;
  155. }
  156. }
  157. return set;
  158. }
  159. }