diff --git a/Sources/Knit/Module/Container+AbstractRegistration.swift b/Sources/Knit/Module/Container+AbstractRegistration.swift index 0a22e0d..a8238e7 100644 --- a/Sources/Knit/Module/Container+AbstractRegistration.swift +++ b/Sources/Knit/Module/Container+AbstractRegistration.swift @@ -18,6 +18,20 @@ extension Container { abstractRegistrations().abstractRegistrations.append(registration) } + /// Register that a service is expected to exist but no implementation is currently available + /// The concrete implementation must be registered or the dependency graph is considered invalid + /// - NOTE: We don't currently support abstract registrations with arguments + /// As this is an `Optional` Service type this allows special handling of the abstract registration for test environments: + /// If during testing and no concrete registration is available, then `nil` will be resolved automatically. + public func registerAbstract( + _ serviceType: Optional.Type, + name: String? = nil, + file: String = #fileID + ) { + let registration = OptionalAbstractRegistration(name: name, file: file) + abstractRegistrations().abstractRegistrations.append(registration) + } + // Must be called before using `registerAbstract` func registerAbstractContainer() -> AbstractRegistrationContainer { let registrations = AbstractRegistrationContainer() @@ -91,6 +105,29 @@ fileprivate struct RealAbstractRegistration: AbstractRegistration { } } +/// An abstract registration for an optional service +fileprivate struct OptionalAbstractRegistration: AbstractRegistration { + let name: String? + // Source file used for debugging. Not included in hash calculation or equality + let file: String + + var serviceType: ServiceType.Type { ServiceType.self } + + var key: RegistrationKey { + return .init(typeIdentifier: ObjectIdentifier(ServiceType.self), name: name) + } + + func registerPlaceholder( + container: Container, + errorFormatter: ModuleAssemblerErrorFormatter, + dependencyTree: DependencyTree + ) { + container.register(Optional.self, name: name) { _ in + return nil + } + } +} + // MARK: - Inner types extension Container { diff --git a/Tests/KnitTests/AbstractRegistrationTests.swift b/Tests/KnitTests/AbstractRegistrationTests.swift index bfa59d8..4b09a2f 100644 --- a/Tests/KnitTests/AbstractRegistrationTests.swift +++ b/Tests/KnitTests/AbstractRegistrationTests.swift @@ -73,6 +73,16 @@ final class AbstractRegistrationTests: XCTestCase { ) } + @MainActor + func testOptionalAbstractRegistrations() { + let assembler = ModuleAssembler([Assembly3()]) + let string = assembler.resolver.resolve(String?.self) ?? nil + XCTAssertNil(string) + + let int = assembler.resolver.resolve(Optional.self) ?? nil + XCTAssertNil(int) + } + } private struct Assembly1: AutoInitModuleAssembly { @@ -86,3 +96,11 @@ private struct Assembly2: AutoInitModuleAssembly { container.registerAbstract(String.self) } } + +private struct Assembly3: AutoInitModuleAssembly { + static var dependencies: [any ModuleAssembly.Type] { [] } + func assemble(container: Container) { + container.registerAbstract(Optional.self) + container.registerAbstract(Int?.self) + } +}