Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## Version 1.1.1

### Fixes

- Fixed an issue where an asset binding specifying a null instance was considered invalid, causing an incompatible asset type error, instead of a missing dependency error.

### Tests

- Added a regression test for asset binding validation with null instance, ensuring null instance binding is not invalidated.

## Version 1.1.0

### Changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,28 @@ private static void ValidateBinding(
List<InjectionError> errors = new();

if (!existingBindings.Add(bindingNode))
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: "Duplicate or ambiguous binding within same Scope detected"
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "Duplicate or ambiguous binding within same Scope detected"
)
);

switch (bindingNode)
{
case GlobalComponentBindingNode globalBinding:
{
if (existingGlobals.TryGetValue(globalBinding.ConcreteType, out GlobalComponentBindingNode existingGlobal))
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: $"Duplicate global binding '{globalBinding.ConcreteType.Name}' declared by '{globalBinding.ScopeNode.ScopeType.Name}'. Already owned by '{existingGlobal.ScopeNode.ScopeType.Name}'. Only one global per type is allowed."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: $"Duplicate global binding '{globalBinding.ConcreteType.Name}' declared by '{globalBinding.ScopeNode.ScopeType.Name}'. Already owned by '{existingGlobal.ScopeNode.ScopeType.Name}'. Only one global per type is allowed."
)
);
else
existingGlobals.Add(globalBinding.ConcreteType, globalBinding);

Expand All @@ -67,86 +73,116 @@ private static void ValidateBinding(
if (componentBinding.RuntimeProxyConfig != null)
{
if (componentBinding.InterfaceType == null)
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: "RuntimeProxy bindings require an interface type. Use BindComponent<IInterface, Concrete>().FromProxy()."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "RuntimeProxy bindings require an interface type. Use BindComponent<IInterface, Concrete>().FromProxy()."
)
);

if (componentBinding.ConcreteType == null)
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: "RuntimeProxy bindings require an interface type. Use BindComponent<IInterface, Concrete>().FromProxy()."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "RuntimeProxy bindings require an interface type. Use BindComponent<IInterface, Concrete>().FromProxy()."
)
);

if (componentBinding.IsCollectionBinding)
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: "RuntimeProxy bindings must be single-value only. Collections cannot be resolved via a RuntimeProxy."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "RuntimeProxy bindings must be single-value only. Collections cannot be resolved via a RuntimeProxy."
)
);
}

if (componentBinding.ConcreteType != null && !typeof(Component).IsAssignableFrom(componentBinding.ConcreteType))
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: $"Component binding type '{componentBinding.ConcreteType.Name}' is not a Unity Component. Component bindings must resolve UnityEngine.Component types."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: $"Component binding type '{componentBinding.ConcreteType.Name}' is not a Unity Component. Component bindings must resolve UnityEngine.Component types."
)
);

break;
}

case AssetBindingNode assetBinding:
{
if (assetBinding.ConcreteType != null && typeof(Component).IsAssignableFrom(assetBinding.ConcreteType))
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: $"Asset binding type '{assetBinding.ConcreteType.Name}' derives from Component. Assets must be ScriptableObjects, prefabs, or other UnityEngine.Object assets."
));
else if (assetBinding.ResolveFromInstances != null && assetBinding.ResolveFromInstances.Any(x => !EditorUtility.IsPersistent(x)))
errors.Add(new InvalidBindingError
new InvalidBindingError
(
bindingNode: bindingNode,
reason: $"Asset binding type '{assetBinding.ConcreteType.Name}' derives from Component. Assets must be ScriptableObjects, prefabs, or other UnityEngine.Object assets."
)
);
else if (assetBinding.ResolveFromInstances != null && assetBinding.ResolveFromInstances.Any(x => x != null && !EditorUtility.IsPersistent(x)))
errors.Add
(
bindingNode: bindingNode,
reason: "Asset binding configured with non-asset objects."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "Asset binding configured with non-asset objects."
)
);

break;
}
}

if (bindingNode.FromMethodException != null)
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: "FromMethod(...) threw an exception.",
exception: bindingNode.FromMethodException
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "FromMethod(...) threw an exception.",
exception: bindingNode.FromMethodException
)
);

if (bindingNode.InterfaceType is { IsInterface: false })
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: $"Binding interface type '{bindingNode.InterfaceType.FullName}' is not an interface."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: $"Binding interface type '{bindingNode.InterfaceType.FullName}' is not an interface."
)
);

if (bindingNode.InterfaceType != null && bindingNode.ConcreteType != null &&
bindingNode.InterfaceType.IsInterface &&
!bindingNode.InterfaceType.IsAssignableFrom(bindingNode.ConcreteType))
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: $"Concrete type '{bindingNode.ConcreteType.Name}' does not implement interface '{bindingNode.InterfaceType.Name}'."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: $"Concrete type '{bindingNode.ConcreteType.Name}' does not implement interface '{bindingNode.InterfaceType.Name}'."
)
);

if (!bindingNode.LocatorStrategySpecified)
errors.Add(new InvalidBindingError
errors.Add
(
bindingNode: bindingNode,
reason: "Binding has no locator strategy (e.g. FromScopeSelf, FromAnywhere)."
));
new InvalidBindingError
(
bindingNode: bindingNode,
reason: "Binding has no locator strategy (e.g. FromScopeSelf, FromAnywhere)."
)
);

context.RegisterErrors(errors);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ private static IEnumerable<Object> LocateAssetCandidates(
.Select(assetPath => AssetDatabase.LoadAssetAtPath(assetPath, bindingNode.ConcreteType))
.Where(obj => obj != null),

AssetLoadType.Instance => bindingNode.ResolveFromInstances,
AssetLoadType.Instance => bindingNode.ResolveFromInstances.Where(x => x != null),

_ => throw new ArgumentOutOfRangeException()
};
Expand Down
2 changes: 1 addition & 1 deletion UnityProject/Saneject/Assets/Plugins/Saneject/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "com.alexanderlarsen.saneject",
"author": "Alexander Larsen",
"displayName": "Saneject",
"version": "1.1.0",
"version": "1.1.1",
"description": "Inject dependencies in the Unity Editor, not Play Mode, by writing them directly into serialized fields at edit-time using familiar DI APIs, so everything stays visible in the Inspector, including interfaces.\n\nNo runtime container. No startup cost. No hidden wiring. No weird lifecycles. Just simple, deterministic edit-time DI that works with Unity, not around it.",
"documentationUrl": "https://github.com/alexanderlarsen/Saneject/blob/main/README.md",
"changelogUrl": "https://github.com/alexanderlarsen/Saneject/blob/main/CHANGELOG.md",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,26 @@ public void BindAsset_TConcrete_DuplicateWithinSameScope_IsInvalid()
// Inject
InjectionRunner.Run(scene.Roots, ContextWalkFilter.SceneObjects);
}

[Test]
public void BindAsset_TConcrete_ToNullInstance_IsNotInvalid()
{
// Expect logs
LogAssert.Expect(LogType.Warning, new Regex("^Saneject: Unused binding"));
LogAssert.Expect(LogType.Warning, new Regex("^Saneject: Injection complete"));


// Set up scene
TestScene scene = TestScene.Create(roots: 1, width: 1, depth: 1);
TestScope scope = scene.Add<TestScope>("Root 1");

// Bind
scope.BindAsset<AssetDependency>().FromInstance(null);

// Inject
InjectionRunner.Run(scene.Roots, ContextWalkFilter.SceneObjects);

LogAssert.NoUnexpectedReceived();
}
}
}
Loading