Skip to content

Commit

Permalink
feat: Add WeakReference to store reference types in a default provider
Browse files Browse the repository at this point in the history
  • Loading branch information
CoreyAlexander committed Dec 21, 2024
1 parent 68c490d commit 312b9de
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ public static TValue DependOn<TValue>(
// all the other dependency resolution methods.
var state = dependent.MixinState.Get<DependentState>();
if (state.ProviderFakes.TryGetValue(typeof(TValue), out var fakeProvider)) {
return fakeProvider.Value();
if (fakeProvider is IProvide<TValue> faker) {
return faker.Value();
}
}

// Lookup dependency, per usual, respecting any fallback values if there
Expand All @@ -145,15 +147,16 @@ public static TValue DependOn<TValue>(
if (providerNode is IProvide<TValue> provider) {
return provider.Value();
}
else if (providerNode is DefaultProvider defaultProvider) {
else if (providerNode is DefaultProvider<TValue> defaultProvider) {
return defaultProvider.Value();
}
}
else if (fallback is not null) {
// See if we were given a fallback.
var provider = new DefaultProvider(fallback());
var value = fallback();
var provider = new DefaultProvider<TValue>(value, fallback);
state.Dependencies.Add(typeof(TValue), provider);
return (TValue)provider.Value();
return provider.Value();
}

throw new ProviderNotFoundException(typeof(TValue));
Expand Down Expand Up @@ -283,15 +286,47 @@ void onProviderInitialized(IBaseProvider provider) {
// for fallback values.
}

public class DefaultProvider : IBaseProvider {
private readonly dynamic _value;
public class DefaultProvider<TValue> : IBaseProvider {
private object _value;
private readonly Func<TValue>? _fallback;
public ProviderState ProviderState { get; }

public DefaultProvider(dynamic value) {
_value = value;
// When working with reference types, we must wrap the value in a WeakReference<>()
// to allow the garbage collection to work when the assembly is being unloaded or reloaded;
// such as in the case of rebuilding within the Godot Editor if you've instantiated a node
// and run it as a tool script.
public DefaultProvider(object value, Func<TValue>? fallback = default) {
_fallback = fallback;

if (value == null) {
throw new ArgumentNullException(nameof(fallback), "Value cannot be null.");
}

_value = value.GetType().IsValueType ? value : new WeakReference<object>(value);
ProviderState = new() { IsInitialized = true };
}

public dynamic Value() => _value;
public TValue Value() {
if (_value is WeakReference<object> weakReference) {
//Try to return a reference type.
if (weakReference.TryGetTarget(out var target)) {
return (TValue)target;
}
else {
//If value was garbage collected, make a new one.
if (_fallback == null) {
throw new InvalidOperationException("Fallback method is null.");
}

var value = _fallback() ?? throw new InvalidOperationException("Fallback cannot create a null value");
_value = new WeakReference<object>(value);
return value;
}
}
else {
//Return a value type.
return (TValue)_value;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public class DependentState {
/// allows nodes being unit-tested to provide fake providers during unit tests
/// that return mock or faked values.
/// </summary>
public Dictionary<Type, DependencyResolver.DefaultProvider> ProviderFakes {
public DependencyResolver.DependencyTable ProviderFakes {
get;
} = [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,6 @@ this is IIntrospective introspective &&
public void FakeDependency<T>(T value) where T : notnull {
AddStateIfNeeded();
MixinState.Get<DependentState>().ProviderFakes[typeof(T)] =
new DependencyResolver.DefaultProvider(value);
new DependencyResolver.DefaultProvider<T>(value);
}
}
19 changes: 19 additions & 0 deletions Chickensoft.AutoInject.Tests/test/fixtures/Dependents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ public void OnResolved() {
}
}

[Meta(typeof(IAutoOn), typeof(IDependent))]
public partial class ReferenceDependentFallback : Node {
public override void _Notification(int what) => this.Notify(what);

[Dependency]
public object MyDependency => this.DependOn(() => FallbackValue);

public object FallbackValue { get; set; } = new Resource();
public bool OnResolvedCalled { get; private set; }
public object ResolvedValue { get; set; } = null!;

public void OnReady() { }

public void OnResolved() {
OnResolvedCalled = true;
ResolvedValue = MyDependency;
}
}

[Meta(typeof(IAutoOn), typeof(IDependent))]
public partial class IntDependent : Node {
public override void _Notification(int what) => this.Notify(what);
Expand Down
2 changes: 1 addition & 1 deletion Chickensoft.AutoInject.Tests/test/src/MiscTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public void IDependentOnResolvedDoesNothing() {

[Test]
public void DefaultProviderState() {
var defaultProvider = new DependencyResolver.DefaultProvider("hi");
var defaultProvider = new DependencyResolver.DefaultProvider<string>("hi");
defaultProvider.ProviderState.ShouldNotBeNull();
}
}
16 changes: 15 additions & 1 deletion Chickensoft.AutoInject.Tests/test/src/ResolutionTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public void ThrowsWhenNoProviderFound() {
}

[Test]
public void UsesFallbackValueWhenNoProviderFound() {
public void UsesValueFallbackValueWhenNoProviderFound() {
var fallback = "Hello, world!";
var dependent = new StringDependentFallback {
FallbackValue = fallback
Expand All @@ -142,6 +142,20 @@ public void UsesFallbackValueWhenNoProviderFound() {
dependent.ResolvedValue.ShouldBe(fallback);
dependent.MyDependency.ShouldBe(fallback);
}

[Test]
public void UsesReferenceFallbackValueWhenNoProviderFound() {
var fallback = new Resource();
var dependent = new ReferenceDependentFallback {
FallbackValue = fallback
};

dependent._Notification((int)Node.NotificationReady);

dependent.ResolvedValue.ShouldBe(fallback);
dependent.MyDependency.ShouldBe(fallback);
}

[Test]
public void ThrowsOnDependencyTableThatWasTamperedWith() {
var fallback = "Hello, world!";
Expand Down

0 comments on commit 312b9de

Please sign in to comment.