diff --git a/README.md b/README.md index 40e82d7b..5a8015b1 100644 --- a/README.md +++ b/README.md @@ -247,7 +247,7 @@ public final class UpdateUserService { #### Renaming and retyping dependencies -It is possible to rename or retype a dependency that is `@Instantiated` or `@Forwarded` by an object higher up in the dependency tree with the `@Received` macro. Renamed or retyped dependencies are able to be received with their new name and type by objects instantiated further down the dependency tree. +It is possible to rename or retype a dependency that is `@Instantiated` or `@Forwarded` by an object higher up in the dependency tree with the `@Received(fulfilledByDependencyNamed:ofType:)` macro. Renamed or retyped dependencies are able to be received with their new name and type by objects instantiated further down the dependency tree. Here we have an example of a `UserManager` type that is received as a `UserVendor` further down the dependency tree. @@ -379,8 +379,9 @@ SafeDI automatically finds the root(s) of your dependency tree, and creates an e An `@Instantiable` type qualifies as the root of a dependency tree if and only if: -1. The type‘s SafeDI-injected properties are all `@Instantiated` -2. The type is not instantiated by another `@Instantiable` type +1. The type‘s SafeDI-injected properties are all `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` +2. The type‘s `@Received(fulfilledByDependencyNamed:ofType:)` properties can be fulfilled by `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` properties declared on this type +3. The type is not instantiated by another `@Instantiable` type ### Comparing SafeDI and Manual Injection: Key Differences diff --git a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift index e2e63342..45f5c1c3 100644 --- a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift +++ b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift @@ -148,7 +148,7 @@ public final class DependencyTreeGenerator { private lazy var possibleRootInstantiableTypes: Set = Set( typeDescriptionToFulfillingInstantiableMap .values - .filter(\.dependencies.areAllInstantiated) + .filter(\.dependencies.couldRepresentRoot) .map(\.concreteInstantiableType) ) @@ -296,11 +296,17 @@ public final class DependencyTreeGenerator { let parentContainsProperty = receivableProperties.contains(receivedProperty) let propertyIsCreatedAtThisScope = createdProperties.contains(receivedProperty) if !parentContainsProperty && !propertyIsCreatedAtThisScope { - unfulfillableProperties.insert(.init( - property: receivedProperty, - instantiable: scope.instantiable, - parentStack: instantiables.elements) - ) + if instantiables.elements.isEmpty { + // This property's scope is not a real root instantiable! Remove it from the list. + rootInstantiableTypes.remove(scope.instantiable.concreteInstantiableType) + } else { + // This property is in a dependency tree and is unfulfillable. Record the problem. + unfulfillableProperties.insert(.init( + property: receivedProperty, + instantiable: scope.instantiable, + parentStack: instantiables.elements) + ) + } } } @@ -357,26 +363,33 @@ extension Dependency { fileprivate var isInstantiated: Bool { switch source { case .instantiated: - return true + true case .aliased, .forwarded, .received: - return false + false } } fileprivate var isForwarded: Bool { switch source { case .forwarded: - return true + true case .aliased, .instantiated, .received: - return false + false } } } -// MARK: - Array +// MARK: - Collection -extension Array where Element == Dependency { - fileprivate var areAllInstantiated: Bool { - first(where: { !$0.isInstantiated }) == nil +extension Collection where Element == Dependency { + fileprivate var couldRepresentRoot: Bool { + first(where: { + switch $0.source { + case .instantiated, .aliased: + false + case .forwarded, .received: + true + } + }) == nil } } diff --git a/Tests/SafeDIToolTests/SafeDIToolTests.swift b/Tests/SafeDIToolTests/SafeDIToolTests.swift index 820cca6f..d32bf5ec 100644 --- a/Tests/SafeDIToolTests/SafeDIToolTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolTests.swift @@ -2295,6 +2295,107 @@ final class SafeDIToolTests: XCTestCase { ) } + func test_run_writesConvenienceExtensionOnRootOfTree_whenRootHasReceivedAliasOfInstantiable() async throws { + let output = try await executeSystemUnderTest( + swiftFileContent: [ + """ + @Instantiable + public struct Root { + @Instantiated + private let defaultUserService: DefaultUserService + + @Received(fulfilledByDependencyNamed: "defaultUserService", ofType: DefaultUserService.self) + private let userService: any UserService + } + """, + """ + import Foundation + + public protocol UserService { + var userName: String? { get set } + } + + @Instantiable(fulfillingAdditionalTypes: [UserService.self]) + public final class DefaultUserService: UserService { + public init() {} + + public var userName: String? + } + """, + ], + buildDependencyTreeOutput: true + ) + + XCTAssertEqual( + try XCTUnwrap(output.dependencyTree), + """ + // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. + // Any modifications made to this file will be overwritten on subsequent builds. + // Please refrain from editing this file directly. + + #if canImport(Foundation) + import Foundation + #endif + + extension Root { + public init() { + let defaultUserService = DefaultUserService() + let userService: any UserService = defaultUserService + self.init(defaultUserService: defaultUserService, userService: userService) + } + } + """ + ) + } + + func test_run_successfullyGeneratesOutputFileWhenNoRootFound() async throws { + let output = try await executeSystemUnderTest( + swiftFileContent: [ + """ + @Instantiable + public struct NotRoot { + @Instantiated + private let defaultUserService: DefaultUserService + + // This received property's alias is improperly configured, meaning that this type is not a root. + @Received(fulfilledByDependencyNamed: "userService", ofType: DefaultUserService.self) + private let userService: any UserService + } + """, + """ + import Foundation + + public protocol UserService { + var userName: String? { get set } + } + + @Instantiable(fulfillingAdditionalTypes: [UserService.self]) + public final class DefaultUserService: UserService { + public init() {} + + public var userName: String? + } + """, + ], + buildDependencyTreeOutput: true + ) + + XCTAssertEqual( + try XCTUnwrap(output.dependencyTree), + """ + // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. + // Any modifications made to this file will be overwritten on subsequent builds. + // Please refrain from editing this file directly. + + #if canImport(Foundation) + import Foundation + #endif + + // No root @Instantiable-decorated types found, or root types already had a `public init()` method. + """ + ) + } + func test_run_writesConvenienceExtensionOnRootOfTree_whenReceivedPropertyIsAliased() async throws { let output = try await executeSystemUnderTest( swiftFileContent: [