diff --git a/CHANGELOG.md b/CHANGELOG.md index cd08f43..bf0dc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,17 @@ __Sections__ - `Removed` for deprecated features removed in this release. - `Fixed` for any bug fixes. +## [2.0.0] + +### Added + +- Support for Swift parameter packs +- Tests converted to new Swift Testing framework + +### Fixed + +- Resolving of shared instances + ## [1.0.5] ### Added diff --git a/README.md b/README.md index e0ec71c..a2a93bd 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,20 @@ container.register { container, number in } ``` +Argument matching is based on the compile-time type of each argument. That means `ConcreteType` and `any SomeProtocol` are different registrations even if the concrete value conforms to the protocol: +```swift +let container = Container() +container.register { _, dependency: any DIProtocol in + DependencyWithProtocolParameter(subDependency: dependency) +} + +let concrete = StructureDependency(property1: "42") +let existential: any DIProtocol = concrete + +let service: DependencyWithProtocolParameter = container.resolve(arguments: existential) // works +// container.resolve(arguments: concrete) throws because StructureDependency != any DIProtocol +``` + ### Autoregistration Let's have look at an example from above: diff --git a/Sources/Container/Async/AsyncContainer.swift b/Sources/Container/Async/AsyncContainer.swift index 211ec7a..12a920f 100644 --- a/Sources/Container/Async/AsyncContainer.swift +++ b/Sources/Container/Async/AsyncContainer.swift @@ -63,6 +63,8 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same container, /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container /// should always return a new instance for dependencies with arguments. + /// Argument matching is based on compile-time types, so registering with `ConcreteType` and resolving with `any Protocol` + /// (or the other way around) creates different registrations. /// /// - Parameters: /// - type: Type of the dependency to register @@ -95,6 +97,7 @@ public actor AsyncContainer: AsyncDependencyResolving, AsyncDependencyRegisterin /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If a dependency of the given type with the given arguments wasn't registered before this method call /// the method throws ``ResolutionError.dependencyNotRegistered`` + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - type: Type of the dependency that should be resolved diff --git a/Sources/Container/Sync/Container.swift b/Sources/Container/Sync/Container.swift index f359f1c..05e90af 100644 --- a/Sources/Container/Sync/Container.swift +++ b/Sources/Container/Sync/Container.swift @@ -56,6 +56,8 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same container, /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container /// should always return a new instance for dependencies with arguments. + /// Argument matching is based on compile-time types, so registering with `ConcreteType` and resolving with `any Protocol` + /// (or the other way around) creates different registrations. /// /// - Parameters: /// - type: Type of the dependency to register @@ -88,6 +90,7 @@ open class Container: DependencyAutoregistering, DependencyResolving, Dependency /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If a dependency of the given type with the given arguments wasn't registered before this method call /// the method throws ``ResolutionError.dependencyNotRegistered`` + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - type: Type of the dependency that should be resolved diff --git a/Sources/Models/Async/AsyncRegistration.swift b/Sources/Models/Async/AsyncRegistration.swift index b7eedf5..8746a4d 100644 --- a/Sources/Models/Async/AsyncRegistration.swift +++ b/Sources/Models/Async/AsyncRegistration.swift @@ -33,7 +33,7 @@ struct AsyncRegistration: Sendable { asyncRegistrationFactory = { resolver, arg in guard let arguments = arg as? (repeat each Argument) else { throw ResolutionError.unmatchingArgumentType( - message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))" + message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(runtimeTypeDescription(of: arg))" ) } diff --git a/Sources/Models/RegistrationIdentifier.swift b/Sources/Models/RegistrationIdentifier.swift index 0a0f08d..a2d152d 100644 --- a/Sources/Models/RegistrationIdentifier.swift +++ b/Sources/Models/RegistrationIdentifier.swift @@ -16,6 +16,8 @@ enum RegistrationIdentifierConstant { struct RegistrationIdentifier: Sendable { let typeIdentifier: ObjectIdentifier let argumentIdentifiers: [ObjectIdentifier] + let typeDescription: String + let argumentTypeDescriptions: [String] /// Number of argument types (used to enforce maximum at resolve time) var argumentCount: Int { argumentIdentifiers.count } @@ -28,17 +30,23 @@ struct RegistrationIdentifier: Sendable { /// - argumentTypes: Variadic argument types using parameter packs. Only 1-3 arguments are supported. Entering more arguments will cause error in runtime. init(type: Dependency.Type, argumentTypes _: repeat (each Argument).Type) { typeIdentifier = ObjectIdentifier(type) + typeDescription = String(reflecting: type) var identifiers: [ObjectIdentifier] = [] + var descriptions: [String] = [] repeat identifiers.append(ObjectIdentifier((each Argument).self)) + repeat descriptions.append(String(reflecting: (each Argument).self)) argumentIdentifiers = identifiers + argumentTypeDescriptions = descriptions } /// Convenience initializer for dependencies without arguments init(type: Dependency.Type) { typeIdentifier = ObjectIdentifier(type) + typeDescription = String(reflecting: type) argumentIdentifiers = [] + argumentTypeDescriptions = [] } } @@ -48,14 +56,18 @@ extension RegistrationIdentifier: Hashable {} // MARK: Debug information extension RegistrationIdentifier: CustomStringConvertible { var description: String { - let argumentsDescription: String = if argumentIdentifiers.isEmpty { - "nil" + if argumentTypeDescriptions.isEmpty { + typeDescription } else { - argumentIdentifiers.map(\.debugDescription).joined(separator: ", ") + "\(typeDescription)(\(argumentTypeDescriptions.joined(separator: ", ")))" } - return """ - Type: \(typeIdentifier.debugDescription) - Arguments: \(argumentsDescription) - """ } } + +func runtimeTypeDescription(of value: Any?) -> String { + guard let value else { + return "nil" + } + + return String(reflecting: Swift.type(of: value)) +} diff --git a/Sources/Models/Sync/Registration.swift b/Sources/Models/Sync/Registration.swift index 112a06b..a5d7d85 100644 --- a/Sources/Models/Sync/Registration.swift +++ b/Sources/Models/Sync/Registration.swift @@ -31,7 +31,7 @@ struct Registration { self.factory = { resolver, arg in guard let arguments = arg as? (repeat each Argument) else { throw ResolutionError.unmatchingArgumentType( - message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(Swift.type(of: arg))" + message: "Registration of type \(registrationIdentifier.description) doesn't accept arguments of type \(runtimeTypeDescription(of: arg))" ) } diff --git a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift index f1e1247..f22270f 100644 --- a/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift +++ b/Sources/Protocols/Registration/Async/AsyncDependencyRegistering.swift @@ -29,6 +29,7 @@ public protocol AsyncDependencyRegistering { /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container /// should always return a new instance for dependencies with arguments. + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are different registrations. /// /// - Parameters: /// - type: Type of the dependency to register @@ -53,6 +54,7 @@ public extension AsyncDependencyRegistering { /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container /// should always return a new instance for dependencies with arguments. + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are different registrations. /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved diff --git a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift index d920326..73cf101 100644 --- a/Sources/Protocols/Registration/Sync/DependencyRegistering.swift +++ b/Sources/Protocols/Registration/Sync/DependencyRegistering.swift @@ -29,6 +29,7 @@ public protocol DependencyRegistering { /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container /// should always return a new instance for dependencies with arguments. + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are different registrations. /// /// - Parameters: /// - type: Type of the dependency to register @@ -53,6 +54,7 @@ public extension DependencyRegistering { /// The arguments are typically parameters in an initializer of the dependency that are not registered in the same resolver (i.e. container), /// therefore, they need to be passed in `resolve` call. This registration method doesn't have any scope parameter for a reason - the container /// should always return a new instance for dependencies with arguments. + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are different registrations. /// /// - Parameters: /// - factory: Closure that is called when the dependency is being resolved diff --git a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift index a10a102..71f4dea 100644 --- a/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift +++ b/Sources/Protocols/Resolution/Async/AsyncDependencyResolving.swift @@ -22,6 +22,7 @@ public protocol AsyncDependencyResolving { /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If the container doesn't contain any registration for a dependency with the given type /// or if arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - type: Type of the dependency that should be resolved @@ -52,6 +53,7 @@ public extension AsyncDependencyResolving { /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If the container doesn't contain any registration for a dependency with the given type /// or if arguments of different types than expected are passed, a runtime error occurs + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - type: Type of the dependency that should be resolved @@ -66,6 +68,7 @@ public extension AsyncDependencyResolving { /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If the container doesn't contain any registration for a dependency with the given type /// or if arguments of different types than expected are passed, a runtime error occurs + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) diff --git a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift index a3af0a5..f94a836 100644 --- a/Sources/Protocols/Resolution/Sync/DependencyResolving.swift +++ b/Sources/Protocols/Resolution/Sync/DependencyResolving.swift @@ -22,6 +22,7 @@ public protocol DependencyResolving { /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If the container doesn't contain any registration for a dependency with the given type /// or if arguments of different types than expected are passed, ``ResolutionError`` is thrown + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - type: Type of the dependency that should be resolved @@ -52,6 +53,7 @@ public extension DependencyResolving { /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If the container doesn't contain any registration for a dependency with the given type /// or if arguments of different types than expected are passed, a runtime error occurs + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - type: Type of the dependency that should be resolved @@ -66,6 +68,7 @@ public extension DependencyResolving { /// Uses Swift parameter packs to support 1-3 arguments with a single method signature. /// If the container doesn't contain any registration for a dependency with the given type /// or if arguments of different types than expected are passed, a runtime error occurs + /// Argument matching is based on compile-time types, so `ConcreteType` and `any Protocol` are treated as different argument lists. /// /// - Parameters: /// - arguments: Arguments that will be passed as input parameters to the factory method (1-3 arguments supported) diff --git a/Tests/Common/Dependencies.swift b/Tests/Common/Dependencies.swift index f760fcf..269feec 100644 --- a/Tests/Common/Dependencies.swift +++ b/Tests/Common/Dependencies.swift @@ -25,6 +25,14 @@ final class DependencyWithValueTypeParameter: Sendable { } } +final class DependencyWithProtocolParameter: Sendable { + let subDependency: any DIProtocol + + init(subDependency: any DIProtocol) { + self.subDependency = subDependency + } +} + final class DependencyWithParameter: Sendable { let subDependency: SimpleDependency diff --git a/Tests/Container/Async/AsyncArgumentTests.swift b/Tests/Container/Async/AsyncArgumentTests.swift index 759a340..5bfead1 100644 --- a/Tests/Container/Async/AsyncArgumentTests.swift +++ b/Tests/Container/Async/AsyncArgumentTests.swift @@ -65,6 +65,8 @@ struct AsyncContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(resolutionError.localizedDescription.contains("SimpleDependency")) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } @@ -94,6 +96,10 @@ struct AsyncContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(resolutionError.localizedDescription.contains("DependencyWithValueTypeParameter")) + #expect(resolutionError.localizedDescription.contains("StructureDependency")) + #expect(resolutionError.localizedDescription.contains("Swift.Int")) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } @@ -160,6 +166,7 @@ struct AsyncContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } @@ -231,6 +238,37 @@ struct AsyncContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) + default: + Issue.record("Incorrect resolution error") + } + } + } + + @Test("Argument matching uses compile-time type") + func argumentMatchingUsesCompileTimeType() async throws { + // Given + let subject = AsyncContainer() + await subject.register { (_: any AsyncDependencyResolving, dependency: any DIProtocol) -> DependencyWithProtocolParameter in + DependencyWithProtocolParameter(subDependency: dependency) + } + let concrete = StructureDependency(property1: "48") + + // When + do { + _ = try await subject.tryResolve(type: DependencyWithProtocolParameter.self, arguments: concrete) + Issue.record("Expected to throw error") + } catch { + // Then + guard let resolutionError = error as? ResolutionError else { + Issue.record("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + #expect(resolutionError.localizedDescription.contains("DIProtocol")) + #expect(resolutionError.localizedDescription.contains("StructureDependency")) default: Issue.record("Incorrect resolution error") } diff --git a/Tests/Container/Async/AsyncBaseTests.swift b/Tests/Container/Async/AsyncBaseTests.swift index ffd3db2..f57074d 100644 --- a/Tests/Container/Async/AsyncBaseTests.swift +++ b/Tests/Container/Async/AsyncBaseTests.swift @@ -77,6 +77,8 @@ struct AsyncBaseTests { switch resolutionError { case .dependencyNotRegistered: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(resolutionError.localizedDescription.contains("SimpleDependency")) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } diff --git a/Tests/Container/Sync/ArgumentTests.swift b/Tests/Container/Sync/ArgumentTests.swift index 9f9313b..ec00ce5 100644 --- a/Tests/Container/Sync/ArgumentTests.swift +++ b/Tests/Container/Sync/ArgumentTests.swift @@ -65,6 +65,8 @@ struct ContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(resolutionError.localizedDescription.contains("SimpleDependency")) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } @@ -94,6 +96,10 @@ struct ContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(resolutionError.localizedDescription.contains("DependencyWithValueTypeParameter")) + #expect(resolutionError.localizedDescription.contains("StructureDependency")) + #expect(resolutionError.localizedDescription.contains("Swift.Int")) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } @@ -160,6 +166,7 @@ struct ContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") } @@ -231,6 +238,37 @@ struct ContainerArgumentTests { switch resolutionError { case .unmatchingArgumentType: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) + default: + Issue.record("Incorrect resolution error") + } + } + } + + @Test("Argument matching uses compile-time type") + func argumentMatchingUsesCompileTimeType() throws { + // Given + let subject = Container() + subject.register { (_: DependencyResolving, dependency: any DIProtocol) -> DependencyWithProtocolParameter in + DependencyWithProtocolParameter(subDependency: dependency) + } + let concrete = StructureDependency(property1: "48") + + // When + do { + _ = try subject.tryResolve(type: DependencyWithProtocolParameter.self, arguments: concrete) + Issue.record("Expected to throw error") + } catch { + // Then + guard let resolutionError = error as? ResolutionError else { + Issue.record("Incorrect error type") + return + } + + switch resolutionError { + case .unmatchingArgumentType: + #expect(resolutionError.localizedDescription.contains("DIProtocol")) + #expect(resolutionError.localizedDescription.contains("StructureDependency")) default: Issue.record("Incorrect resolution error") } diff --git a/Tests/Container/Sync/BaseTests.swift b/Tests/Container/Sync/BaseTests.swift index 7b00628..6d98f8e 100644 --- a/Tests/Container/Sync/BaseTests.swift +++ b/Tests/Container/Sync/BaseTests.swift @@ -119,6 +119,8 @@ struct BaseTests { switch resolutionError { case .dependencyNotRegistered: #expect(!resolutionError.localizedDescription.isEmpty) + #expect(resolutionError.localizedDescription.contains("SimpleDependency")) + #expect(!resolutionError.localizedDescription.contains("ObjectIdentifier")) default: Issue.record("Incorrect resolution error") }