Skip to content

Commit

Permalink
Enable throwing error when ForwardingInstantiator's generics are wrong
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed committed Dec 19, 2023
1 parent c8b3b9f commit ab87ccb
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,19 @@ actor ScopeGenerator {
let isConstant: Bool
let propertyDeclaration: String
let leadingConcreteTypeName: String
let closureArguments = if let forwardedProperty { " \(forwardedProperty.label) in" } else { "" }
let closureArguments: String
if let forwardedProperty {
guard property.generics.first == forwardedProperty.typeDescription else {
throw GenerationError.forwardingInstantiatorGenericDoesNotMatch(
property: property,
expectedType: forwardedProperty.typeDescription,
instantiable: instantiable
)
}
closureArguments = " \(forwardedProperty.label) in"
} else {
closureArguments = ""
}
switch property.propertyType {
case .instantiator, .forwardingInstantiator:
isConstant = false
Expand Down Expand Up @@ -162,7 +174,7 @@ actor ScopeGenerator {
private func generateProperties(leadingMemberWhitespace: String) async throws -> [String] {
var generatedProperties = [String]()
while
let childGenerator = try nextSatisfiableProperty(),
let childGenerator = nextSatisfiableProperty(),
let childProperty = childGenerator.property
{
resolvedProperties.insert(childProperty)
Expand All @@ -174,7 +186,7 @@ actor ScopeGenerator {
return generatedProperties
}

private func nextSatisfiableProperty() throws -> ScopeGenerator? {
private func nextSatisfiableProperty() -> ScopeGenerator? {
let remainingProperties = propertiesToGenerate.filter {
if let property = $0.property {
!resolvedProperties.contains(property)
Expand Down Expand Up @@ -211,4 +223,17 @@ actor ScopeGenerator {
|| receivedProperties.contains(property)
|| forwardedProperty == property
}

// MARK: GenerationError

private enum GenerationError: Error, CustomStringConvertible {
case forwardingInstantiatorGenericDoesNotMatch(property: Property, expectedType: TypeDescription, instantiable: Instantiable)

var description: String {
switch self {
case let .forwardingInstantiatorGenericDoesNotMatch(property, expectedType, instantiable):
"Property `\(property.asSource)` on \(instantiable.concreteInstantiableType.asSource) incorrectly configured. Property should instead be of type `\(Dependency.forwardingInstantiatorType)<\(expectedType.asSource), \(property.typeDescription.asInstantiatedType.asSource)>`. First generic argument must match type of @\(Dependency.Source.forwarded.rawValue) property."
}
}
}
}
21 changes: 21 additions & 0 deletions Sources/SafeDICore/Models/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
}
}

var generics: [TypeDescription] {
switch typeDescription {
case let .simple(_, generics),
let .nested(_, _, generics):
return generics
case .any,
.array,
.attributed,
.closure,
.composition,
.dictionary,
.implicitlyUnwrappedOptional,
.metatype,
.optional,
.some,
.tuple,
.unknown:
return []
}
}

// MARK: PropertyType

enum PropertyType {
Expand Down
97 changes: 97 additions & 0 deletions Tests/SafeDIToolTests/SafeDIToolTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2363,6 +2363,103 @@ final class SafeDIToolTests: XCTestCase {
}
}

func test_run_onCodeWithIncorrectInstantiableFirstGeneric_throwsError() async throws {
await assertThrowsError(
"""
Property `loggedInViewControllerBuilder: ForwardingInstantiator<String, UIViewController>` on LoggedInViewController incorrectly configured. Property should instead be of type `ForwardingInstantiator<User, UIViewController>`. First generic argument must match type of @Forwarded property.
"""
) {
try await SafeDITool.run(
swiftFileContent: [
"""
public struct User {}
""",
"""
public protocol AuthService {
func login(username: String, password: String) async -> User
}
@Instantiable(fulfillingAdditionalTypes: [AuthService.self])
public final class DefaultAuthService: AuthService {
public init(networkService: NetworkService) {
self.networkService = networkService
}
public func login(username: String, password: String) async -> User {
User()
}
@Received
let networkService: NetworkService
}
""",
"""
public protocol NetworkService {}
@Instantiable(fulfillingAdditionalTypes: [NetworkService.self])
public final class DefaultNetworkService: NetworkService {
public init() {}
}
""",
"""
import UIKit
@Instantiable
public final class RootViewController: UIViewController {
public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ForwardingInstantiator<String, UIViewController>) {
self.authService = authService
self.networkService = networkService
self.loggedInViewControllerBuilder = loggedInViewControllerBuilder
derivedValue = false
super.init(nibName: nil, bundle: nil)
}
@Instantiated
let networkService: NetworkService
@Instantiated
let authService: AuthService
@Instantiated(fulfilledByType: "LoggedInViewController")
let loggedInViewControllerBuilder: ForwardingInstantiator<String, UIViewController>
private let derivedValue: Bool
func login(username: String, password: String) {
Task { @MainActor in
let loggedInViewController = loggedInViewControllerBuilder.instantiate(username)
pushViewController(loggedInViewController)
}
}
}
""",
"""
import UIKit
@Instantiable
public final class LoggedInViewController: UIViewController {
public init(user: User, networkService: NetworkService) {
self.user = user
self.networkService = networkService
}
@Forwarded
private let user: User
@Received
let networkService: NetworkService
}
""",
],
dependentImportStatements: [],
dependentInstantiables: [],
buildDependencyTreeOutput: true
)
}
}


private func assertThrowsError<ReturnType>(
_ errorDescription: String,
line: UInt = #line,
Expand Down

0 comments on commit ab87ccb

Please sign in to comment.