Skip to content

Commit

Permalink
Fixes #30 Provide more info in missing dep error (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
ncipollo authored Jan 16, 2025
1 parent 3e31aa6 commit 5189cc0
Show file tree
Hide file tree
Showing 10 changed files with 91 additions and 17 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ struct InjectableWithNamedDependency: Equatable {
let dependency: InjectableWithNamedDependency = container.inject()
```

In the above example, we do not need to define a factory for `InjectableWithNamedDependency`. Since this is annotated with the Injectable macro WhoopDI will automatically create it and provide it's dependencies when it is requested via an `inject` or `get` method.
In the above example, we do not need to define a factory for `InjectableWithNamedDependency`. Since this is decorated with the Injectable macro WhoopDI will automatically create it and provide it's dependencies when it is requested via an `inject` or `get` method.

If you need to provide a named dependency, you can use the `@InjectableName` annotation to specify the name of the dependency you want to inject.
If you need to provide a named dependency, you can use the `@InjectableName` macro to specify the name of the dependency you want to inject.


## Register Dependencies
Expand Down
3 changes: 2 additions & 1 deletion Sources/WhoopDIKit/Container/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ public final class Container {
} else if let injectable = T.self as? any Injectable.Type {
return try injectable.inject(container: self) as! T
} else {
throw DependencyError.missingDependency(ServiceKey(T.self, name: name))
throw DependencyError.createMissingDependencyError(missingDependency: ServiceKey(T.self, name: name),
serviceDict: serviceDict)
}
}

Expand Down
36 changes: 31 additions & 5 deletions Sources/WhoopDIKit/DependencyError.swift
Original file line number Diff line number Diff line change
@@ -1,16 +1,42 @@
enum DependencyError: Error, CustomStringConvertible, Equatable {
case badParams(ServiceKey)
case missingDependency(ServiceKey)
case missingDependency(missingDependency: ServiceKey, similarDependencies: Set<ServiceKey>, dependencyCount: Int)
case nilDependency(ServiceKey)

var description: String {
switch self {
case .badParams(let serviceKey):
return "Bad parameters provided for \(serviceKey.type) with name: \(serviceKey.name ?? "<no name>")"
case .missingDependency(let serviceKey):
return "Missing dependency for \(serviceKey.type) with name: \(serviceKey.name ?? "<no name>")"
return "Bad parameters provided for \(serviceKey)"
case .missingDependency(let missingDependency, let similarDependencies, let dependencyCount):
return missingDeendencyDescription(missingDependency, similarDependencies, dependencyCount)
case .nilDependency(let serviceKey):
return "Nil dependency for \(serviceKey.type) with name: \(serviceKey.name ?? "<no name>")"
return "Nil dependency for \(serviceKey)"
}
}

private func missingDeendencyDescription(_ missingDependency: ServiceKey,
_ similarDependencies: Set<ServiceKey>,
_ dependencyCount: Int) -> String {
let similarStrings = similarDependencies.map { "- \($0)" }.sorted()
let similarJoined = similarStrings.joined(separator: "\n")
let similarDescription = similarStrings.isEmpty ? "" : "\nSimilar dependencies:\n\(similarJoined)"

return """
Missing dependency for \(missingDependency)
Container has a total of \(dependencyCount) dependencies.
""" + similarDescription
}

static func createMissingDependencyError(
missingDependency: ServiceKey,
serviceDict: ServiceDictionary<DependencyDefinition>
) -> DependencyError {
let similarDependencies = serviceDict.allKeys().filter { key in
key.name == missingDependency.name || key.type == missingDependency.type
}

return .missingDependency(missingDependency: missingDependency,
similarDependencies: similarDependencies,
dependencyCount: serviceDict.count)
}
}
4 changes: 4 additions & 0 deletions Sources/WhoopDIKit/Service/ServiceDictionary.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public final class ServiceDictionary<Value> {
public func removeAll() {
valuesByType.removeAll()
}

public var count: Int {
valuesByType.count
}
}

public extension ServiceDictionary {
Expand Down
6 changes: 5 additions & 1 deletion Sources/WhoopDIKit/Service/ServiceKey.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
/// Hashable wrapper for a metatype value.
/// See https://stackoverflow.com/questions/42459484/make-a-swift-dictionary-where-the-key-is-type
public struct ServiceKey: Sendable {
public struct ServiceKey: Sendable, CustomStringConvertible {
public let type: Any.Type
public let name: String?

public init(_ type: Any.Type, name: String? = nil) {
self.type = type
self.name = name
}

public var description: String {
"\(type) with name: \(name ?? "<no name>")"
}
}

extension ServiceKey: Equatable, Hashable {
Expand Down
37 changes: 31 additions & 6 deletions Tests/WhoopDIKitTests/DependencyErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import XCTest
@testable import WhoopDIKit

class DependencyErrorTests: XCTestCase {
private let emptyDict = ServiceDictionary<DependencyDefinition>()
private let serviceKey = ServiceKey(String.self)
private let serviceKeyWithName = ServiceKey(String.self, name: "name")

func test_description_badParams_noServiceKeyName() {
let expected = "Bad parameters provided for String with name: <no name>"
let error = DependencyError.badParams(serviceKey)
Expand All @@ -18,17 +19,41 @@ class DependencyErrorTests: XCTestCase {
}

func test_description_missingDependency_noServiceKeyName() {
let expected = "Missing dependency for String with name: <no name>"
let error = DependencyError.missingDependency(serviceKey)
let error = DependencyError.createMissingDependencyError(missingDependency: serviceKey, serviceDict: emptyDict)
let expected = """
Missing dependency for String with name: <no name>
Container has a total of 0 dependencies.
"""
XCTAssertEqual(expected, error.description)
}

func test_description_missingDependency_withServiceKeyName() {
let expected = "Missing dependency for String with name: name"
let error = DependencyError.missingDependency(serviceKeyWithName)
let error = DependencyError.createMissingDependencyError(missingDependency: serviceKeyWithName,
serviceDict: emptyDict)
let expected = """
Missing dependency for String with name: name
Container has a total of 0 dependencies.
"""
XCTAssertEqual(expected, error.description)
}


func test_description_missingDependency_withServiceKeyName_similarDependencies() {
let factory = FactoryDefinition(name: nil, factory: { _ in "" })
let serviceDict: ServiceDictionary<DependencyDefinition> = ServiceDictionary<DependencyDefinition>()
serviceDict[ServiceKey(String.self, name: "other_name")] = factory
serviceDict[ServiceKey(String.self)] = factory
let error = DependencyError.createMissingDependencyError(missingDependency: serviceKeyWithName,
serviceDict: serviceDict)
let expected = """
Missing dependency for String with name: name
Container has a total of 2 dependencies.
Similar dependencies:
- String with name: <no name>
- String with name: other_name
"""
XCTAssertEqual(expected, error.description)
}

func test_description_nilDependecy_noServiceKeyName() {
let expected = "Nil dependency for String with name: <no name>"
let error = DependencyError.nilDependency(serviceKey)
Expand Down
3 changes: 2 additions & 1 deletion Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ class DependencyDefinitionTests: XCTestCase {
}

func test_singleton_get_recoversFromThrow() {
let expectedError = DependencyError.missingDependency(ServiceKey(String.self))
let expectedError = DependencyError.createMissingDependencyError(missingDependency: ServiceKey(String.self),
serviceDict: ServiceDictionary())
var callCount = 0
let definition = SingletonDefinition(name: nil) { _ -> Int in
callCount += 1
Expand Down
1 change: 1 addition & 0 deletions Tests/WhoopDIKitTests/Service/ServiceDictionaryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ final class ServiceDictionaryTests: XCTestCase {
XCTAssertEqual("string2", dictC[String.self])
XCTAssertEqual("int2", dictC[Int.self])
XCTAssertEqual("bool", dictC[Bool.self])
XCTAssertEqual(dictC.count, 3)
}
}
10 changes: 10 additions & 0 deletions Tests/WhoopDIKitTests/Service/ServiceKeyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import XCTest
@testable import WhoopDIKit

final class ServiceKeyTests: XCTestCase {
func test_description() {
let key = ServiceKey(String.self, name: "name")
XCTAssertEqual("String with name: name", key.description)
}

func test_description_noName() {
let key = ServiceKey(String.self)
XCTAssertEqual("String with name: <no name>", key.description)
}

func test_hash_nilName() {
let key = ServiceKey(String.self)
let expected = ObjectIdentifier(String.self).hashValue
Expand Down
4 changes: 3 additions & 1 deletion Tests/WhoopDIKitTests/WhoopDITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,9 @@ class WhoopDITests {
var failed = false
validator.validate { error in
let expectedKey = ServiceKey(Dependency.self, name: "A_Factory")
let expectedError = DependencyError.missingDependency(expectedKey)
let expectedError = DependencyError.missingDependency(missingDependency: expectedKey,
similarDependencies: Set(),
dependencyCount: 1)
#expect(expectedError == error as! DependencyError)
failed = true
}
Expand Down

0 comments on commit 5189cc0

Please sign in to comment.