using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; namespace Terminal.Gui.Configuration; /// /// Converts instances to/from JSON. Does all the heavy lifting of reading/writing config /// data to/from JSON documents. /// /// [RequiresUnreferencedCode ("AOT")] internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TScopeT> : JsonConverter where TScopeT : Scope { [RequiresDynamicCode ("Calls System.Type.MakeGenericType(params Type[])")] #pragma warning disable IL3051 // 'RequiresDynamicCodeAttribute' annotations must match across all interface implementations or overrides. public override TScopeT 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 = (TScopeT)Activator.CreateInstance (typeof (TScopeT))!; var propertyName = string.Empty; while (reader.Read ()) { if (reader.TokenType == JsonTokenType.EndObject) { return scope!; } if (reader.TokenType != JsonTokenType.PropertyName) { throw new JsonException ($"After {propertyName}: Expected a JSON property name, but got \"{reader.TokenType}\""); } propertyName = reader.GetString (); reader.Read (); // Get the hardcoded property from the TscopeT (e.g. ThemeScope.GetHardCodedProperty) ConfigProperty? configProperty = scope.GetHardCodedProperty (propertyName!); if (propertyName is { } && configProperty is { }) { // This property name was found in the cached hard-coded scope dict. // Add it, with no value configProperty.HasValue = false; configProperty.PropertyValue = null; scope.TryAdd (propertyName, configProperty); // Figure out if it needs a JsonConverter and if so, create one Type? propertyType = configProperty?.PropertyInfo?.PropertyType!; if (configProperty?.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); } } try { var type = (Type?)typeof (ReadHelper<>).MakeGenericType (typeof (TScopeT), propertyType!); var readHelper = Activator.CreateInstance (type!, converter) as ReadHelper; scope! [propertyName].PropertyValue = readHelper?.Read (ref reader, propertyType!, options); } catch (NotSupportedException e) { throw new JsonException ( $"{propertyName}: Error reading property of type \"{propertyType?.Name}\".", e ); } catch (TargetInvocationException) { // QUESTION: Should we try/catch here? scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, options); } } else { // QUESTION: Should we try/catch here? scope! [propertyName].PropertyValue = JsonSerializer.Deserialize (ref reader, propertyType!, ConfigurationManager.SerializerContext); } //Logging.Warning ($"{propertyName} = {scope! [propertyName].PropertyValue}"); } else { // It is not a config property. Maybe it's just a property on the Scope with [JsonInclude] // like ScopeSettings.$schema. // If so, don't add it to the dictionary but apply it to the underlying property on // the scopeT. // BUGBUG: This is terrible design. The only time it's used is for $schema though. PropertyInfo? property = scope!.GetType () .GetProperties () .Where (p => { if (p.GetCustomAttribute (typeof (JsonIncludeAttribute)) is JsonIncludeAttribute { } jia) { var jsonPropertyNameAttribute = p.GetCustomAttribute ( typeof (JsonPropertyNameAttribute) ) as JsonPropertyNameAttribute; if (jsonPropertyNameAttribute?.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 { }) { // Set the value of propertyName on the scopeT. PropertyInfo prop = scope.GetType ().GetProperty (propertyName!)!; prop.SetValue (scope, JsonSerializer.Deserialize (ref reader, prop.PropertyType, ConfigurationManager.SerializerContext)); } else { // Unknown property // TODO: To support forward compatibility, we should just ignore unknown properties? // TODO: Eg if we read an unknown property, it's possible that the property was added in a later version throw new JsonException ($"{propertyName}: Unknown property name."); } } } throw new JsonException ($"{propertyName}: Json error in ScopeJsonConverter"); } [UnconditionalSuppressMessage ( "AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")] public override void Write (Utf8JsonWriter writer, TScopeT 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 (), ConfigurationManager.SerializerContext); } foreach (KeyValuePair p in from p in scope .Where (cp => cp.Value.PropertyInfo?.GetCustomAttribute ( typeof ( ConfigurationPropertyAttribute) ) is ConfigurationPropertyAttribute scp && scp?.Scope == typeof (TScopeT) ) where p.Value.HasValue 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, [writer, p.Value.PropertyValue, options]); } } else { object? prop = p.Value.PropertyValue; if (prop == null) { writer.WriteNullValue (); } else { JsonSerializer.Serialize (writer, prop, prop.GetType (), ConfigurationManager.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); } [method: RequiresUnreferencedCode ("Calls System.Delegate.CreateDelegate(Type, Object, String)")] internal class ReadHelper (object converter) : ReadHelper { private readonly ReadDelegate _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 TConverter ReadDelegate (ref Utf8JsonReader reader, Type type, JsonSerializerOptions options); } }