Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions Sources/Container/Async/AsyncContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Sources/Container/Sync/Container.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Sources/Models/Async/AsyncRegistration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
)
}

Expand Down
26 changes: 19 additions & 7 deletions Sources/Models/RegistrationIdentifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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<Dependency, each Argument>(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<Dependency>(type: Dependency.Type) {
typeIdentifier = ObjectIdentifier(type)
typeDescription = String(reflecting: type)
argumentIdentifiers = []
argumentTypeDescriptions = []
}
}

Expand All @@ -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))
}
2 changes: 1 addition & 1 deletion Sources/Models/Sync/Registration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))"
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions Sources/Protocols/Resolution/Sync/DependencyResolving.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions Tests/Common/Dependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
38 changes: 38 additions & 0 deletions Tests/Container/Async/AsyncArgumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand Down Expand Up @@ -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")
}
Expand Down
2 changes: 2 additions & 0 deletions Tests/Container/Async/AsyncBaseTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
Loading
Loading