#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; 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<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] scopeT> : JsonConverter where scopeT : Scope { [RequiresDynamicCode ("Calls System.Type.MakeGenericType(params Type[])")] #pragma warning disable IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. public override scopeT Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) #pragma warning restore IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. { 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}\"."); } string? propertyName = reader.GetString (); reader.Read (); if (propertyName is { } && scope!.TryGetValue (propertyName, out ConfigProperty? configProp)) { // This property name was found in the Scope's ScopeProperties dictionary // Figure out if it needs a JsonConverter and if so, create one Type? propertyType = configProp?.PropertyInfo?.PropertyType!; if (configProp?.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) { object? converter = Activator.CreateInstance (jca.ConverterType!)!; if (converter.GetType ().BaseType == typeof (JsonConverterFactory)) { var factory = (JsonConverterFactory)converter; if (propertyType is { } && 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!, _serializerContext); } catch (Exception ex) { 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... PropertyInfo? property = scope!.GetType () .GetProperties () .Where ( p => { var jia = p.GetCustomAttribute (typeof (JsonIncludeAttribute)) as JsonIncludeAttribute; if (jia is { }) { 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 is { }) { PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!; prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, _serializerContext)); } 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 (); IEnumerable properties = scope!.GetType () .GetProperties () .Where ( p => p.GetCustomAttribute (typeof (JsonIncludeAttribute)) != null ); foreach (PropertyInfo p in properties) { writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p)); object? prop = scope.GetType ().GetProperty (p.Name)?.GetValue (scope); JsonSerializer.Serialize (writer, prop, prop!.GetType (), _serializerContext); } foreach (KeyValuePair 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); Type? propertyType = p.Value.PropertyInfo?.PropertyType; if (propertyType != null && p.Value.PropertyInfo?.GetCustomAttribute (typeof (JsonConverterAttribute)) is JsonConverterAttribute jca) { object 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 is { }) { converter.GetType () .GetMethod ("Write") ?.Invoke (converter, new [] { writer, p.Value.PropertyValue, options }); } } else { object? prop = p.Value.PropertyValue; JsonSerializer.Serialize (writer, prop, prop!.GetType (), _serializerContext); } } writer.WriteEndObject (); } // 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; [RequiresUnreferencedCode ("Calls System.Delegate.CreateDelegate(Type, Object, String)")] public ReadHelper (object converter) { _readDelegate = (ReadDelegate)Delegate.CreateDelegate (typeof (ReadDelegate), converter, "Read"); } public override object? Read (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) { return _readDelegate.Invoke (ref reader, type, options); } private delegate converterT ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options); } }