Skip to content

Commit

Permalink
feat: add support for declaring ProvidesParametersFor via base classe…
Browse files Browse the repository at this point in the history
…s and interfaces

Closes: #194
  • Loading branch information
bdunderscore committed Mar 16, 2024
1 parent bfdfc68 commit 5b2b7e5
Show file tree
Hide file tree
Showing 13 changed files with 242 additions and 15 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [unreleased]

### Added
- Added support for declaring ProvidesParametersFor via base classes and interfaces (#198)

### Fixed

Expand Down
83 changes: 69 additions & 14 deletions Editor/EnhancerDatabase.cs
@@ -1,7 +1,9 @@
#region

using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
Expand All @@ -11,14 +13,23 @@

namespace nadena.dev.ndmf
{
internal static class EnhancerDatabase<T, Interface> where T : Attribute
internal class EnhancerDatabase<T, Interface> where T : Attribute
{
internal delegate Interface Creator(Component c);

private static readonly PropertyInfo forTypeProp = typeof(T).GetProperty("ForType");
private static readonly Task<IImmutableDictionary<Type, Creator>> TaskDatabase = Task.Run(Init);
private static readonly Task<EnhancerDatabase<T, Interface>> TaskDatabase = Task.Run(() => new EnhancerDatabase<T, Interface>());

private static IImmutableDictionary<Type, Creator> Init()
private readonly IImmutableDictionary<Type, Creator> _attributes = FindAttributes();
private Dictionary<Type, Creator> _resolved;

private EnhancerDatabase()
{
_resolved = new Dictionary<Type, EnhancerDatabase<T, Interface>.Creator>();
}


private static IImmutableDictionary<Type, Creator> FindAttributes()
{
var builder = ImmutableDictionary.CreateBuilder<Type, Creator>();
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
Expand Down Expand Up @@ -66,26 +77,70 @@ internal static class EnhancerDatabase<T, Interface> where T : Attribute
builder.Add(forType, lambda.Compile());
}

public static IImmutableDictionary<Type, Creator> Mappings
public static bool Query(Component c, out Interface iface)
{
iface = default;
if (!TaskDatabase.Result.DoQuery(c, out var creator)) return false;

iface = creator(c);

return true;
}

private bool DoQuery(Component c, out Creator creator)
{
get
if (_resolved.TryGetValue(c.GetType(), out creator))
{
TaskDatabase.Wait();
return TaskDatabase.Result;
return true;
}
}

public static bool Query(Component c, out Interface iface)
{
if (!Mappings.TryGetValue(c.GetType(), out var creator))
var tmp = WalkTypeTree(c.GetType())
.Where((kvp) => _attributes.ContainsKey(kvp.Item1)).ToList();

// Perform breadth-first search on base classes and interfaces, prioritizing more specific declarations.
using (var it = WalkTypeTree(c.GetType())
.Where((kvp) => _attributes.ContainsKey(kvp.Item1))
.OrderBy((kvp) => kvp.Item2)
.Take(2)
.GetEnumerator())
{
iface = default;
return false;

if (!it.MoveNext()) return false;
var first = it.Current;
if (it.MoveNext() && it.Current.Item2 == first.Item2)
{
Debug.LogError("Multiple candidate " + typeof(T) +
" attributes found for base types and interfaces of " + c.GetType());
return false;
}

creator = _attributes[first.Item1];
_resolved[c.GetType()] = creator; // cache resolved type
}


iface = creator(c);
return true;
}

private IEnumerable<(Type, int)> WalkTypeTree(Type type)
{
int depth = 0;
while (type != null)
{
yield return (type, depth);

foreach (var i in type.GetInterfaces())
{
yield return (i, depth + 1);
}

if (type.BaseType == type) break;

type = type.BaseType;
depth++;
}
}


public static void AsyncInit()
{
Expand Down
14 changes: 14 additions & 0 deletions Runtime/TestSupport/PTCConflictComponent.cs
@@ -0,0 +1,14 @@
using UnityEngine;

namespace nadena.dev.ndmf.UnitTestSupport
{
interface ITestInterface2
{
}

[AddComponentMenu("")]
internal class PTCConflictComponent : MonoBehaviour, ITestInterface1, ITestInterface2
{

}
}
3 changes: 3 additions & 0 deletions Runtime/TestSupport/PTCConflictComponent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Runtime/TestSupport/PTCDepthResolutionComponent.cs
@@ -0,0 +1,7 @@
namespace nadena.dev.ndmf.UnitTestSupport
{
internal class PTCDepthResolutionComponent : PTCDepthResolutionComponentBase, ITestInterface2
{

}
}
3 changes: 3 additions & 0 deletions Runtime/TestSupport/PTCDepthResolutionComponent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions Runtime/TestSupport/PTCDepthResolutionComponentBase.cs
@@ -0,0 +1,11 @@
using UnityEngine;

namespace nadena.dev.ndmf.UnitTestSupport
{
internal abstract class PTCDepthResolutionComponentBase : PTCDepthResolutionComponentBase2
{
}

internal abstract class PTCDepthResolutionComponentBase2 : MonoBehaviour
{ }
}
3 changes: 3 additions & 0 deletions Runtime/TestSupport/PTCDepthResolutionComponentBase.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions Runtime/TestSupport/PTCInheritanceComponent.cs
@@ -0,0 +1,14 @@
using UnityEngine;

namespace nadena.dev.ndmf.UnitTestSupport
{
interface ITestInterface1
{
}

[AddComponentMenu("")]
internal class PTCInheritanceComponent : MonoBehaviour, ITestInterface1
{

}
}
3 changes: 3 additions & 0 deletions Runtime/TestSupport/PTCInheritanceComponent.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions UnitTests~/ParameterIntrospection/InheritanceTests.cs
@@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Text.RegularExpressions;
using nadena.dev.ndmf;
using nadena.dev.ndmf.UnitTestSupport;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;

#if NDMF_VRCSDK3_AVATARS

namespace UnitTests.Parameters
{
[ParameterProviderFor(typeof(ITestInterface1))]
internal class TestInterface1Provider : IParameterProvider
{
public TestInterface1Provider(ITestInterface1 _)
{

}

public IEnumerable<ProvidedParameter> GetSuppliedParameters(BuildContext context)
{
return Array.Empty<ProvidedParameter>();
}

public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context)
{
}
}

[ParameterProviderFor(typeof(ITestInterface2))]
internal class TestInterface2Provider : IParameterProvider
{
public TestInterface2Provider(ITestInterface2 _)
{

}

public IEnumerable<ProvidedParameter> GetSuppliedParameters(BuildContext context)
{
return Array.Empty<ProvidedParameter>();
}

public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context)
{
}
}

[ParameterProviderFor(typeof(PTCDepthResolutionComponentBase2))]
internal class DepthResolutionProvider : IParameterProvider
{
public DepthResolutionProvider(PTCDepthResolutionComponentBase2 _)
{

}

public IEnumerable<ProvidedParameter> GetSuppliedParameters(BuildContext context)
{
return Array.Empty<ProvidedParameter>();
}

public void RemapParameters(ref ImmutableDictionary<(ParameterNamespace, string), ParameterMapping> nameMap, BuildContext context)
{
}
}

public class InheritanceTest : TestBase
{
[Test]
public void ResolvesInterface()
{
var root = CreateRoot("root");
var obj = root.AddComponent<PTCInheritanceComponent>();

Assert.IsTrue(EnhancerDatabase<ParameterProviderFor, IParameterProvider>.Query(
obj, out var provider
));

Assert.IsTrue(provider is TestInterface1Provider);
}

[Test]
public void DoesNotResolveAmbiguous()
{
var root = CreateRoot("root");
var obj = root.AddComponent<PTCConflictComponent>();

Assert.IsFalse(EnhancerDatabase<ParameterProviderFor, IParameterProvider>.Query(
obj, out var provider
));
LogAssert.Expect(LogType.Error, new Regex("Multiple candidate .*ParameterProviderFor attributes"));
}

[Test]
public void ResolvesByDepth()
{
var root = CreateRoot("root");
var obj = root.AddComponent<PTCDepthResolutionComponent>();

Assert.IsTrue(EnhancerDatabase<ParameterProviderFor, IParameterProvider>.Query(
obj, out var provider
));

Assert.IsTrue(provider is TestInterface2Provider);
}
}
}

#endif
3 changes: 3 additions & 0 deletions UnitTests~/ParameterIntrospection/InheritanceTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion UnitTests~/PluginResolverTests/BeforeAfterPlugin.cs
Expand Up @@ -2,7 +2,6 @@
using System.Linq;
using nadena.dev.ndmf;
using NUnit.Framework;
using PlasticPipe.PlasticProtocol.Messages;

namespace UnitTests.PluginResolverTests
{
Expand Down

0 comments on commit 5b2b7e5

Please sign in to comment.