Skip to content

Commit

Permalink
Add support for parsing "union" types
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
jorgenpt committed Aug 17, 2023
1 parent 5b58cd5 commit fd8c261
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 8 deletions.
19 changes: 19 additions & 0 deletions src/DarkConfig/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// When parsing the parent type, if the key is <paramref name="key"/> then this type will be
/// parsed instead.
/// </summary>
/// <param name="key">The substitute key</param>
public ConfigUnionAttribute(string key) {
if (string.IsNullOrWhiteSpace(key)) {
throw new ArgumentNullException(nameof(key));
}
Key = key.Trim();
}
}
Expand Down
42 changes: 38 additions & 4 deletions src/DarkConfig/Internal/ReflectionCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ internal class TypeInfo {
public MethodInfo FromDoc;
public MethodInfo PostDoc;

// lookup of keys for polymorphic unions
public Dictionary<string, Type> UnionKeys;

// Source Info
public string SourceInfoMemberName;

Expand Down Expand Up @@ -44,10 +47,25 @@ internal TypeInfo GetTypeInfo(Type type) {
////////////////////////////////////////////

readonly Dictionary<Type, TypeInfo> cachedTypeInfo = new Dictionary<Type, TypeInfo>();
readonly HashSet<Assembly> 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)
Expand All @@ -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 _:
Expand All @@ -66,13 +85,28 @@ TypeInfo CacheTypeInfo(Type type) {
case ConfigAllowMissingAttribute _:
typeHasOptionalAttribute = true;
break;
case ConfigUnionAttribute UnionAttribute:
typeUnionKey = UnionAttribute.Key.ToLowerInvariant();
break;
}
}

if (typeHasMandatoryAttribute && typeHasOptionalAttribute) {
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<string, Type>();
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);
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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;
}
}

Expand Down
28 changes: 24 additions & 4 deletions src/DarkConfig/Internal/TypeReifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -580,8 +599,9 @@ void SetFieldsOnObject(Type type, ref object obj, DocNode doc, ReificationOption
if (setMemberHashes.Count != doc.Count) {
var extraDocFields = new List<string>();
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);
}
}
Expand Down

0 comments on commit fd8c261

Please sign in to comment.