using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using static Terminal.Gui.ConfigurationManager; #nullable enable namespace Terminal.Gui { public static partial class ConfigurationManager { /// /// Defines a configuration settings scope. Classes that inherit from this abstract class can be used to define /// scopes for configuration settings. Each scope is a JSON object that contains a set of configuration settings. /// public class Scope : Dictionary { //, IScope> { /// /// Crates a new instance. /// public Scope () : base (StringComparer.InvariantCultureIgnoreCase) { foreach (var p in GetScopeProperties ()) { Add (p.Key, new ConfigProperty () { PropertyInfo = p.Value.PropertyInfo, PropertyValue = null }); } } private IEnumerable> GetScopeProperties () { return ConfigurationManager._allConfigProperties!.Where (cp => (cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty)) as SerializableConfigurationProperty)?.Scope == GetType ()); } /// /// Updates this instance from the specified source scope. /// /// /// The updated scope (this). public Scope? Update (Scope source) { foreach (var prop in source) { if (ContainsKey (prop.Key)) this [prop.Key].PropertyValue = this [prop.Key].UpdateValueFrom (prop.Value.PropertyValue!); else { this [prop.Key].PropertyValue = prop.Value.PropertyValue; } } return this; } /// /// Retrieves the values of the properties of this scope from their corresponding static properties. /// public void RetrieveValues () { foreach (var p in this.Where (cp => cp.Value.PropertyInfo != null)) { p.Value.RetrieveValue (); } } /// /// Applies the values of the properties of this scope to their corresponding static properties. /// /// internal virtual bool Apply () { bool set = false; foreach (var p in this.Where (t => t.Value != null && t.Value.PropertyValue != null)) { if (p.Value.Apply ()) { set = true; } } return set; } } /// /// Converts instances to/from JSON. Does all the heavy lifting of reading/writing /// config data to/from JSON documents. /// /// public class ScopeJsonConverter : JsonConverter where scopeT : Scope { // See: https://stackoverflow.com/questions/60830084/how-to-pass-an-argument-by-reference-using-reflection internal abstract class ReadHelper { public abstract object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options); } internal class ReadHelper : ReadHelper { private readonly ReadDelegate _readDelegate; private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options); public ReadHelper (object converter) => _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) => _readDelegate.Invoke (ref reader, type, options); } /// public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartObject) { throw new JsonException ($"Expected a JSON object, but got \"{reader.TokenType}\"."); } var scope = (scopeT)Activator.CreateInstance (typeof (scopeT))!; while (reader.Read ()) { if (reader.TokenType == JsonTokenType.EndObject) { return scope!; } if (reader.TokenType != JsonTokenType.PropertyName) { throw new JsonException ($"Expected a JSON property name, but got \"{reader.TokenType}\"."); } var propertyName = reader.GetString (); reader.Read (); if (propertyName != null && scope!.TryGetValue (propertyName, out var configProp)) { // This property name was found in the Scope's ScopeProperties dictionary // Figure out if it needs a JsonConverter and if so, create one var propertyType = configProp?.PropertyInfo?.PropertyType!; if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) { var converter = Activator.CreateInstance (jca.ConverterType!)!; if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) { var factory = (JsonConverterFactory)converter; if (propertyType != null && factory.CanConvert (propertyType)) { converter = factory.CreateConverter (propertyType, options); } } var readHelper = Activator.CreateInstance ((Type?)typeof (ReadHelper<>).MakeGenericType (typeof (scopeT), propertyType!)!, converter) as ReadHelper; try { scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options); } catch (NotSupportedException e) { throw new JsonException ($"Error reading property \"{propertyName}\" of type \"{propertyType?.Name}\".", e); } } else { try { scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, options); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine ($"scopeT Read: {ex}"); } } } else { // It is not a config property. Maybe it's just a property on the Scope with [JsonInclude] // like ScopeSettings.$schema... var property = scope!.GetType ().GetProperties ().Where (p => { var jia = p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as JsonIncludeAttribute; if (jia != null) { var jpna = p.GetCustomAttribute (typeof (JsonPropertyNameAttribute)) as JsonPropertyNameAttribute; if (jpna?.Name == propertyName) { // Bit of a hack, modifying propertyName in an enumerator... propertyName = p.Name; return true; } return p.Name == propertyName; } return false; }).FirstOrDefault (); if (property != null) { var prop = scope.GetType ().GetProperty (propertyName!)!; prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, options)); } else { // Unknown property throw new JsonException ($"Unknown property name \"{propertyName}\"."); } } } throw new JsonException (); } /// public override void Write (Utf8JsonWriter writer, scopeT scope, JsonSerializerOptions options) { writer.WriteStartObject (); var properties = scope!.GetType ().GetProperties ().Where (p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null); foreach (var p in properties) { writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p)); JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options); } foreach (var p in from p in scope .Where (cp => cp.Value.PropertyInfo?.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp && scp?.Scope == typeof (scopeT)) where p.Value.PropertyValue != null select p) { writer.WritePropertyName (p.Key); var propertyType = p.Value.PropertyInfo?.PropertyType; if (propertyType != null && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) { var converter = Activator.CreateInstance (jca.ConverterType!)!; if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) { var factory = (JsonConverterFactory)converter; if (factory.CanConvert (propertyType)) { converter = factory.CreateConverter (propertyType, options)!; } } if (p.Value.PropertyValue != null) { converter.GetType ().GetMethod ("Write")?.Invoke (converter, new object [] { writer, p.Value.PropertyValue, options }); } } else { JsonSerializer.Serialize (writer, p.Value.PropertyValue, options); } } writer.WriteEndObject (); } } } }