From fd8c2613401b67ae052a12a0d9b8865940f7a7bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20Tjern=C3=B8?= Date: Thu, 17 Aug 2023 11:41:33 -0700 Subject: [PATCH] Add support for parsing "union" types Adds a new `[ConfigUnion("key")]` attribute to classes that allows the parent class to be parsed into the concrete subclass if key is specified, e.g. the following CS will make the following YAML parse when trying to parse a field of `BaseClass` type: ```cs [ConfigUnion("myType")] class MyType : BaseClass { public string field; } ``` ```yaml myType: field: somevalue ``` (Extracted from CL 37878) --- src/DarkConfig/Attributes.cs | 19 ++++++++++ src/DarkConfig/Internal/ReflectionCache.cs | 42 +++++++++++++++++++--- src/DarkConfig/Internal/TypeReifier.cs | 28 ++++++++++++--- 3 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/DarkConfig/Attributes.cs b/src/DarkConfig/Attributes.cs index aeb687f..9799f69 100644 --- a/src/DarkConfig/Attributes.cs +++ b/src/DarkConfig/Attributes.cs @@ -43,6 +43,25 @@ public ConfigKeyAttribute(string key) { if (string.IsNullOrWhiteSpace(key)) { throw new ArgumentNullException(nameof(key)); } + + Key = key.Trim(); + } + } + + /// Marks this type as a polymorphic union of it's parent type and indicates it's key + [AttributeUsage(AttributeTargets.Class)] + public class ConfigUnionAttribute : Attribute { + public string Key; + + /// + /// When parsing the parent type, if the key is then this type will be + /// parsed instead. + /// + /// The substitute key + public ConfigUnionAttribute(string key) { + if (string.IsNullOrWhiteSpace(key)) { + throw new ArgumentNullException(nameof(key)); + } Key = key.Trim(); } } diff --git a/src/DarkConfig/Internal/ReflectionCache.cs b/src/DarkConfig/Internal/ReflectionCache.cs index 44a3343..a469635 100644 --- a/src/DarkConfig/Internal/ReflectionCache.cs +++ b/src/DarkConfig/Internal/ReflectionCache.cs @@ -11,6 +11,9 @@ internal class TypeInfo { public MethodInfo FromDoc; public MethodInfo PostDoc; + // lookup of keys for polymorphic unions + public Dictionary UnionKeys; + // Source Info public string SourceInfoMemberName; @@ -44,10 +47,25 @@ internal TypeInfo GetTypeInfo(Type type) { //////////////////////////////////////////// readonly Dictionary cachedTypeInfo = new Dictionary(); + readonly HashSet prechachedAssemblies = new(); //////////////////////////////////////////// + // Precache everything in this assembly that requires iterating all types to resolve + void PrecacheAssembly(Assembly sourceAssembly) { + if (!prechachedAssemblies.Contains(sourceAssembly)) { + prechachedAssemblies.Add(sourceAssembly); + foreach (Type type in sourceAssembly.GetTypes()) { + if (type.GetCustomAttributes(typeof(ConfigUnionAttribute), false).Length > 0) { + GetTypeInfo(type); + } + } + } + } + TypeInfo CacheTypeInfo(Type type) { + PrecacheAssembly(type.Assembly); + var info = new TypeInfo { FromDoc = type.GetMethod("FromDoc", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static), PostDoc = type.GetMethod("PostDoc", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static) @@ -58,6 +76,7 @@ TypeInfo CacheTypeInfo(Type type) { // Read class attributes bool typeHasMandatoryAttribute = false; bool typeHasOptionalAttribute = false; + string typeUnionKey = null; foreach (object attribute in type.GetCustomAttributes(true)) { switch (attribute) { case ConfigMandatoryAttribute _: @@ -66,6 +85,9 @@ TypeInfo CacheTypeInfo(Type type) { case ConfigAllowMissingAttribute _: typeHasOptionalAttribute = true; break; + case ConfigUnionAttribute UnionAttribute: + typeUnionKey = UnionAttribute.Key.ToLowerInvariant(); + break; } } @@ -73,6 +95,18 @@ TypeInfo CacheTypeInfo(Type type) { throw new Exception($"Type {type.Name} has both ConfigAllowMissing and ConfigMandatory attributes."); } + // if type is a union, register it with it's base type + if (typeUnionKey != null) { + if (type.BaseType == typeof(Object) || type.BaseType == null) { + throw new Exception($"Type {type.Name} has ConfigUnion but is not a child type"); + } + TypeInfo parentInfo = GetTypeInfo(type.BaseType); + parentInfo.UnionKeys ??= new Dictionary(); + if (!parentInfo.UnionKeys.TryAdd(typeUnionKey, type)) { + throw new Exception($"Type {type.Name} has ConfigUnion with duplicate key {typeUnionKey}"); + } + } + const BindingFlags MEMBER_BINDING_FLAGS = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; var properties = type.GetProperties(MEMBER_BINDING_FLAGS); var fields = type.GetFields(MEMBER_BINDING_FLAGS); @@ -113,8 +147,8 @@ TypeInfo CacheTypeInfo(Type type) { numRequirementAttributes++; } else if (attribute is ConfigSourceInformationAttribute) { sourceInfo = true; - } else if (attribute is ConfigKeyAttribute) { - propertyName = ((ConfigKeyAttribute) attribute).Key; + } else if (attribute is ConfigKeyAttribute keyAttribute) { + propertyName = keyAttribute.Key; } } @@ -191,8 +225,8 @@ TypeInfo CacheTypeInfo(Type type) { numRequirementAttributes++; } else if (attribute is ConfigSourceInformationAttribute) { sourceInfo = true; - } else if (attribute is ConfigKeyAttribute) { - fieldName = ((ConfigKeyAttribute) attribute).Key; + } else if (attribute is ConfigKeyAttribute keyAttribute) { + fieldName = keyAttribute.Key; } } diff --git a/src/DarkConfig/Internal/TypeReifier.cs b/src/DarkConfig/Internal/TypeReifier.cs index f7f04b2..d1cca5b 100644 --- a/src/DarkConfig/Internal/TypeReifier.cs +++ b/src/DarkConfig/Internal/TypeReifier.cs @@ -439,8 +439,27 @@ void ReadArray(DocNode current, int currentRank) { throw; } } else { - existing ??= Activator.CreateInstance(targetType); - SetFieldsOnObject(targetType, ref existing, doc, options); + if (typeInfo.UnionKeys != null) { + Object subTypeObject = null; + foreach (var unionKey in typeInfo.UnionKeys) { + if (doc.TryGetValue(unionKey.Key, true, out var subTypeDoc)) { + if (subTypeObject == null) { + subTypeObject = ReadValueOfType(unionKey.Value, null, subTypeDoc, options); + } else { + throw new ParseException(doc, $"Union document {targetType} contains multiple subtype keys. Expected only one."); + } + } + } + + if (subTypeObject == null) { + throw new ParseException(doc, $"Union document {targetType} did not resolve to a subtype"); + } + + existing = subTypeObject; + } else { + existing ??= Activator.CreateInstance(targetType); + SetFieldsOnObject(targetType, ref existing, doc, options); + } } // Call a PostDoc function for this type if it exists. @@ -580,8 +599,9 @@ void SetFieldsOnObject(Type type, ref object obj, DocNode doc, ReificationOption if (setMemberHashes.Count != doc.Count) { var extraDocFields = new List(); foreach (var kv in doc.Pairs) { - int docKeyHash = (ignoreCase ? kv.Key.ToLowerInvariant() : kv.Key).GetHashCode(); - if (!setMemberHashes.Contains(docKeyHash)) { + string docKey = (ignoreCase ? kv.Key.ToLowerInvariant() : kv.Key); + int docKeyHash = docKey.GetHashCode(); + if (!setMemberHashes.Contains(docKeyHash) && (typeInfo.UnionKeys == null || !typeInfo.UnionKeys.ContainsKey(docKey))) { extraDocFields.Add(kv.Key); } }