diff --git a/README.md b/README.md index 87bb4dd..7d011cb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/Sources/WhoopDIKit/Container/Container.swift b/Sources/WhoopDIKit/Container/Container.swift index f096a60..a4b9e7f 100644 --- a/Sources/WhoopDIKit/Container/Container.swift +++ b/Sources/WhoopDIKit/Container/Container.swift @@ -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) } } diff --git a/Sources/WhoopDIKit/DependencyError.swift b/Sources/WhoopDIKit/DependencyError.swift index 0208729..0feb82f 100644 --- a/Sources/WhoopDIKit/DependencyError.swift +++ b/Sources/WhoopDIKit/DependencyError.swift @@ -1,16 +1,42 @@ enum DependencyError: Error, CustomStringConvertible, Equatable { case badParams(ServiceKey) - case missingDependency(ServiceKey) + case missingDependency(missingDependency: ServiceKey, similarDependencies: Set, 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 ?? "")" - case .missingDependency(let serviceKey): - return "Missing dependency for \(serviceKey.type) with name: \(serviceKey.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 ?? "")" + return "Nil dependency for \(serviceKey)" } } + + private func missingDeendencyDescription(_ missingDependency: ServiceKey, + _ similarDependencies: Set, + _ 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 + ) -> 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) + } } diff --git a/Sources/WhoopDIKit/Service/ServiceDictionary.swift b/Sources/WhoopDIKit/Service/ServiceDictionary.swift index 6e7f864..e891e8f 100644 --- a/Sources/WhoopDIKit/Service/ServiceDictionary.swift +++ b/Sources/WhoopDIKit/Service/ServiceDictionary.swift @@ -34,6 +34,10 @@ public final class ServiceDictionary { public func removeAll() { valuesByType.removeAll() } + + public var count: Int { + valuesByType.count + } } public extension ServiceDictionary { diff --git a/Sources/WhoopDIKit/Service/ServiceKey.swift b/Sources/WhoopDIKit/Service/ServiceKey.swift index 07ed707..b241892 100644 --- a/Sources/WhoopDIKit/Service/ServiceKey.swift +++ b/Sources/WhoopDIKit/Service/ServiceKey.swift @@ -1,6 +1,6 @@ /// 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? @@ -8,6 +8,10 @@ public struct ServiceKey: Sendable { self.type = type self.name = name } + + public var description: String { + "\(type) with name: \(name ?? "")" + } } extension ServiceKey: Equatable, Hashable { diff --git a/Tests/WhoopDIKitTests/DependencyErrorTests.swift b/Tests/WhoopDIKitTests/DependencyErrorTests.swift index bedfd89..af40d9e 100644 --- a/Tests/WhoopDIKitTests/DependencyErrorTests.swift +++ b/Tests/WhoopDIKitTests/DependencyErrorTests.swift @@ -2,9 +2,10 @@ import XCTest @testable import WhoopDIKit class DependencyErrorTests: XCTestCase { + private let emptyDict = ServiceDictionary() 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: " let error = DependencyError.badParams(serviceKey) @@ -18,17 +19,41 @@ class DependencyErrorTests: XCTestCase { } func test_description_missingDependency_noServiceKeyName() { - let expected = "Missing dependency for String with name: " - let error = DependencyError.missingDependency(serviceKey) + let error = DependencyError.createMissingDependencyError(missingDependency: serviceKey, serviceDict: emptyDict) + let expected = """ + Missing dependency for String with 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 = ServiceDictionary() + 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: + - String with name: other_name + """ + XCTAssertEqual(expected, error.description) + } + func test_description_nilDependecy_noServiceKeyName() { let expected = "Nil dependency for String with name: " let error = DependencyError.nilDependency(serviceKey) diff --git a/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift b/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift index 95f8f18..aa6aa95 100644 --- a/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift +++ b/Tests/WhoopDIKitTests/Module/DependencyDefinitionTests.swift @@ -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 diff --git a/Tests/WhoopDIKitTests/Service/ServiceDictionaryTests.swift b/Tests/WhoopDIKitTests/Service/ServiceDictionaryTests.swift index 7ff790a..256f948 100644 --- a/Tests/WhoopDIKitTests/Service/ServiceDictionaryTests.swift +++ b/Tests/WhoopDIKitTests/Service/ServiceDictionaryTests.swift @@ -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) } } diff --git a/Tests/WhoopDIKitTests/Service/ServiceKeyTests.swift b/Tests/WhoopDIKitTests/Service/ServiceKeyTests.swift index d4d8b6d..cffb209 100644 --- a/Tests/WhoopDIKitTests/Service/ServiceKeyTests.swift +++ b/Tests/WhoopDIKitTests/Service/ServiceKeyTests.swift @@ -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: ", key.description) + } + func test_hash_nilName() { let key = ServiceKey(String.self) let expected = ObjectIdentifier(String.self).hashValue diff --git a/Tests/WhoopDIKitTests/WhoopDITests.swift b/Tests/WhoopDIKitTests/WhoopDITests.swift index cc95460..5124d74 100644 --- a/Tests/WhoopDIKitTests/WhoopDITests.swift +++ b/Tests/WhoopDIKitTests/WhoopDITests.swift @@ -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 }