| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256 |
- using System.Diagnostics.CodeAnalysis;
- using System.Reflection;
- using System.Text.Json;
- using System.Text.Json.Serialization;
- namespace Terminal.Gui.Configuration;
- /// <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="TScopeT"></typeparam>
- [RequiresUnreferencedCode ("AOT")]
- internal class ScopeJsonConverter<[DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] TScopeT> : JsonConverter<TScopeT>
- where TScopeT : Scope<TScopeT>
- {
- [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 = "<Pending>")]
- public override void Write (Utf8JsonWriter writer, TScopeT 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));
- object? prop = scope.GetType ().GetProperty (p.Name)?.GetValue (scope);
- JsonSerializer.Serialize (writer, prop, prop!.GetType (), ConfigurationManager.SerializerContext);
- }
- foreach (KeyValuePair<string, ConfigProperty> 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<TConverter> (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);
- }
- }
|