#nullable enable
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);
}
}