using System; using System.Linq; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; #nullable enable namespace Terminal.Gui; /// /// Converts instances to/from JSON. Does all the heavy lifting of reading/writing /// config data to/from JSON documents. /// /// internal 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 (\"{{ \"propName\" : ... }}\"), 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 (); } }