Skip to content

Commit

Permalink
Enable a dependency root to receive a renamed @Instantiated property (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed authored Jan 25, 2024
1 parent 62637b4 commit aa60c3a
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 17 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
41 changes: 27 additions & 14 deletions Sources/SafeDICore/Generators/DependencyTreeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public final class DependencyTreeGenerator {
private lazy var possibleRootInstantiableTypes: Set<TypeDescription> = Set(
typeDescriptionToFulfillingInstantiableMap
.values
.filter(\.dependencies.areAllInstantiated)
.filter(\.dependencies.couldRepresentRoot)
.map(\.concreteInstantiableType)
)

Expand Down Expand Up @@ -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)
)
}
}
}

Expand Down Expand Up @@ -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
}
}

Expand Down
101 changes: 101 additions & 0 deletions Tests/SafeDIToolTests/SafeDIToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand Down

0 comments on commit aa60c3a

Please sign in to comment.