From 924b66d51668d96c41b4ef185a4dff5ab71acae4 Mon Sep 17 00:00:00 2001 From: Anairkoen Schno Date: Thu, 12 Dec 2019 22:04:49 -0600 Subject: [PATCH] Added Attributes ant their support --- IPA.Loader/Config/SelfConfig.cs | 8 +- IPA.Loader/Config/Stores/Attributes.cs | 68 +++++++++++++++++ IPA.Loader/Config/Stores/GeneratedStore.cs | 86 ++++++++++++++++++---- IPA.Loader/Loader/DisabledConfig.cs | 1 + 4 files changed, 148 insertions(+), 15 deletions(-) diff --git a/IPA.Loader/Config/SelfConfig.cs b/IPA.Loader/Config/SelfConfig.cs index a085c508..49dd6727 100644 --- a/IPA.Loader/Config/SelfConfig.cs +++ b/IPA.Loader/Config/SelfConfig.cs @@ -4,6 +4,7 @@ using IPA.Utilities; using IPA.Config.Stores; // END: section ignore using Newtonsoft.Json; +using IPA.Config.Stores.Attributes; namespace IPA.Config { @@ -79,6 +80,9 @@ namespace IPA.Config && CommandLineValues.Updates.AutoCheckUpdates; } + // LINE: ignore + [NonNullable] + [JsonProperty(Required = Required.DisallowNull)] // the JsonProperty annotations are for the generated schema public virtual Updates_ Updates { get; set; } = new Updates_(); public class Debug_ @@ -117,6 +121,9 @@ namespace IPA.Config || CommandLineValues.Debug.ShowTrace; } + // LINE: ignore + [NonNullable] + [JsonProperty(Required = Required.DisallowNull)] public virtual Debug_ Debug { get; set; } = new Debug_(); public virtual bool YeetMods { get; set; } = true; @@ -124,7 +131,6 @@ namespace IPA.Config public static bool YeetMods_ => (Instance?.YeetMods ?? true) && CommandLineValues.YeetMods; - [JsonProperty(Required = Required.Default)] public virtual string LastGameVersion { get; set; } = null; // LINE: ignore public static string LastGameVersion_ => Instance?.LastGameVersion; diff --git a/IPA.Loader/Config/Stores/Attributes.cs b/IPA.Loader/Config/Stores/Attributes.cs index 420e7b2a..5c8e22b1 100644 --- a/IPA.Loader/Config/Stores/Attributes.cs +++ b/IPA.Loader/Config/Stores/Attributes.cs @@ -6,5 +6,73 @@ using System.Threading.Tasks; namespace IPA.Config.Stores.Attributes { + /// + /// Causes a field or property in an object being wrapped by to be + /// ignored during serialization and deserialization. + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class IgnoreAttribute : Attribute { } + + /// + /// Indicates that a field or property in an object being wrapped by + /// that would otherwise be nullable (i.e. a reference type or a type) should never be null, and the + /// member will be ignored if the deserialized value is . + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class NonNullableAttribute : Attribute { } + + /// + /// Specifies a name for the serialized field or property in an object being wrapped by + /// that is different from the member name itself. + /// + /// + /// + /// When serializing the following object, we might get the JSON that follows. + /// + /// public class PluginConfig + /// { + /// public virtual bool BooleanField { get; set; } = true; + /// } + /// + /// + /// { + /// "BooleanField": true + /// } + /// + /// + /// + /// However, if we were to add a to that field, we would get the following. + /// + /// public class PluginConfig + /// { + /// [SerializedName("bool")] + /// public virtual bool BooleanField { get; set; } = true; + /// } + /// + /// + /// { + /// "bool": true + /// } + /// + /// + /// + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)] + public sealed class SerializedNameAttribute : Attribute + { + /// + /// Gets the name to replace the member name with. + /// + public string Name { get; private set; } + + /// + /// Creates a new with the given . + /// + /// the value to assign to + public SerializedNameAttribute(string name) + { + Name = name; + } + } + } diff --git a/IPA.Loader/Config/Stores/GeneratedStore.cs b/IPA.Loader/Config/Stores/GeneratedStore.cs index 3ff8f941..9b63614e 100644 --- a/IPA.Loader/Config/Stores/GeneratedStore.cs +++ b/IPA.Loader/Config/Stores/GeneratedStore.cs @@ -1,4 +1,5 @@ using IPA.Config.Data; +using IPA.Config.Stores.Attributes; using IPA.Logging; using System; using System.Collections.Generic; @@ -13,12 +14,13 @@ using System.Runtime.CompilerServices; using System.IO; using Boolean = IPA.Config.Data.Boolean; using System.Collections; +using IPA.Utilities; #if NET3 using Net3_Proxy; using Array = Net3_Proxy.Array; #endif -[assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedStore.GeneratedAssemblyName)] +[assembly: InternalsVisibleTo(IPA.Config.Stores.GeneratedExtension.AssemblyVisibilityTarget)] namespace IPA.Config.Stores { @@ -48,6 +50,15 @@ namespace IPA.Config.Stores /// /// /// + /// Only fields and properties that are or will be considered, and only properties + /// where both the getter and setter are or are considered. Any fields or properties + /// with an applied to them are also ignored. Having properties be is not strictly + /// necessary, however it allows the generated type to keep track of changes and lock around them so that the config will auto-save. + /// + /// + /// All of the attributes in the namespace are handled as described by them. + /// + /// /// If the declares a or , /// method Changed(), then that method may be called to artificially signal to the runtime that the content of the object /// has changed. That method will also be called after the write locks are released when a property is set anywhere in the owning @@ -222,9 +233,18 @@ namespace IPA.Config.Stores { public string Name; public MemberInfo Member; + public Type Type; + public bool AllowNull; public bool IsVirtual; public bool IsField; - public Type Type; + public bool IsNullable; + + // invalid for objects with IsNullabe false + public Type NullableWrappedType => Nullable.GetUnderlyingType(Type); + // invalid for objects with IsNullabe false + public PropertyInfo Nullable_HasValue => Type.GetProperty(nameof(Nullable.HasValue)); + // invalid for objects with IsNullabe false + public PropertyInfo Nullable_Value => Type.GetProperty(nameof(Nullable.Value)); } private static Func MakeCreator(Type type) @@ -251,15 +271,48 @@ namespace IPA.Config.Stores var structure = new List(); - // TODO: incorporate attributes/base types - // TODO: ignore probs without setter + // TODO: support converters + + bool ProcessAttributesFor(MemberInfo member, Type memberType, out string name, out bool allowNull, out bool isNullable) + { + var attrs = member.GetCustomAttributes(true); + var ignores = attrs.Select(o => o as IgnoreAttribute).NonNull(); + if (ignores.Any()) // we ignore + { + name = null; + allowNull = false; + isNullable = false; + return false; + } + + var nonNullables = attrs.Select(o => o as NonNullableAttribute).NonNull(); + + name = member.Name; + isNullable = memberType.IsConstructedGenericType + && memberType.GetGenericTypeDefinition() == typeof(Nullable<>); + allowNull = !nonNullables.Any() && (!memberType.IsValueType || isNullable); + + var nameAttr = attrs.Select(o => o as SerializedNameAttribute).NonNull().FirstOrDefault(); + if (nameAttr != null) + name = nameAttr.Name; + + return true; + } - // only looks at public properties - foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) + // only looks at public/protected properties + foreach (var prop in type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { + if (prop.GetSetMethod(true)?.IsPrivate ?? true) + { // we enter this block if the setter is inacessible or doesn't exist + continue; // ignore props without setter + } + if (prop.GetGetMethod(true)?.IsPrivate ?? true) + { // we enter this block if the getter is inacessible or doesn't exist + continue; // ignore props without getter + } + var smi = new SerializedMemberInfo { - Name = prop.Name, Member = prop, IsVirtual = (prop.GetGetMethod(true)?.IsVirtual ?? false) || (prop.GetSetMethod(true)?.IsVirtual ?? false), @@ -267,21 +320,26 @@ namespace IPA.Config.Stores Type = prop.PropertyType }; + if (!ProcessAttributesFor(smi.Member, smi.Type, out smi.Name, out smi.AllowNull, out smi.IsNullable)) continue; + structure.Add(smi); } - // only look at public fields - foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public)) + // only look at public/protected fields + foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { + if (field.IsPrivate) continue; + var smi = new SerializedMemberInfo { - Name = field.Name, Member = field, IsVirtual = false, IsField = true, Type = field.FieldType }; + if (!ProcessAttributesFor(smi.Member, smi.Type, out smi.Name, out smi.AllowNull, out smi.IsNullable)) continue; + structure.Add(smi); } #endregion @@ -788,18 +846,18 @@ namespace IPA.Config.Stores } } - // TODO: implement Nullable - EmitLoad(); var endSerialize = il.DefineLabel(); - if (!member.Type.IsValueType) + if (member.AllowNull) { var passedNull = il.DefineLabel(); il.Emit(OpCodes.Dup); il.Emit(OpCodes.Brtrue, passedNull); + // TODO: add special check for nullables + il.Emit(OpCodes.Pop); il.Emit(OpCodes.Ldnull); il.Emit(OpCodes.Br, endSerialize); @@ -972,7 +1030,7 @@ namespace IPA.Config.Stores // TODO: support Nullable - if (member.Type.IsValueType) + if (!member.AllowNull) { il.Emit(OpCodes.Pop); EmitLogError(il, $"Member {member.Name} ({member.Type}) not nullable", tailcall: false, diff --git a/IPA.Loader/Loader/DisabledConfig.cs b/IPA.Loader/Loader/DisabledConfig.cs index 82d6ee3f..b27b3261 100644 --- a/IPA.Loader/Loader/DisabledConfig.cs +++ b/IPA.Loader/Loader/DisabledConfig.cs @@ -1,5 +1,6 @@ using IPA.Config; using IPA.Config.Stores; +using IPA.Config.Stores.Attributes; using IPA.Utilities; using System; using System.Collections.Generic;