mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-30 09:47:58 +01:00
* Replace all 342 `== null` with `is null`
* Replace 354 `!= null` with `is { }`
* Wrap these in conditionals since they break tests against Release configuration
The members they depend on do not exist in Release configuration
* Split these up and dispose properly
This test needs to be revisited for several reasons at some point.
* Fix release configuration tests
* Declare interface these already support
* Annotate constructor properly and use throw helper
* Move class to its own file
* Rename these files so they nest in the solution explorer
* Make this a record type and remove now-redundant/illegal members
* Reference passing to avoid some struct copies
* Simplify this
* Carry reference passing through as appropriate
* Turn this into a record struct
* Remove unused internal constructor and its test
It was only used by that test.
* Simplify this constructor
* This should be a property
* Simplify constructor
* Simplify GetHashCode
* Mark this ignored just in case
* Missed a couple of opportunities for reference passing
* record struct already does this by value
* Remove unused class
* Simplify the type initializer and Reset method
* Implement INotifyCollectionChanged and IDictionary by delegating to ColorSchemes
* Fix for reflection-based configuration
* Make CI build happy by disambiguiating this attribute
234 lines
11 KiB
C#
234 lines
11 KiB
C#
#nullable enable
|
|
using System.Diagnostics;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace Terminal.Gui;
|
|
|
|
/// <summary>
|
|
/// Converts <see cref="Scope{T}"/> instances to/from JSON. Does all the heavy lifting of reading/writing config
|
|
/// data to/from <see cref="ConfigurationManager"/> JSON documents.
|
|
/// </summary>
|
|
/// <typeparam name="scopeT"></typeparam>
|
|
internal class ScopeJsonConverter<scopeT> : JsonConverter<scopeT> where scopeT : Scope<scopeT>
|
|
{
|
|
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}\".");
|
|
}
|
|
|
|
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!, options);
|
|
}
|
|
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, 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 ();
|
|
|
|
IEnumerable<PropertyInfo> properties = scope!.GetType ()
|
|
.GetProperties ()
|
|
.Where (
|
|
p => p.GetCustomAttribute (typeof (JsonIncludeAttribute))
|
|
!= null
|
|
);
|
|
|
|
foreach (PropertyInfo p in properties)
|
|
{
|
|
writer.WritePropertyName (ConfigProperty.GetJsonPropertyName (p));
|
|
JsonSerializer.Serialize (writer, scope.GetType ().GetProperty (p.Name)?.GetValue (scope), options);
|
|
}
|
|
|
|
foreach (KeyValuePair<string, ConfigProperty> 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
|
|
{
|
|
JsonSerializer.Serialize (writer, p.Value.PropertyValue, options);
|
|
}
|
|
}
|
|
|
|
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<converterT> : ReadHelper
|
|
{
|
|
private readonly ReadDelegate _readDelegate;
|
|
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);
|
|
}
|
|
}
|