diff --git a/CLI/Sources/KnitCodeGen/UnitTestSourceFile.swift b/CLI/Sources/KnitCodeGen/UnitTestSourceFile.swift index 3fefd63..be365d2 100644 --- a/CLI/Sources/KnitCodeGen/UnitTestSourceFile.swift +++ b/CLI/Sources/KnitCodeGen/UnitTestSourceFile.swift @@ -138,6 +138,7 @@ public enum UnitTestSourceFile { private static func makeCollectionAssert() throws -> FunctionDeclSyntax { let string: SyntaxNodeString = #""" + @MainActor func assertCollectionResolves( _ type: T.Type, count expectedCount: Int, diff --git a/CLI/Tests/KnitCodeGenTests/ConfigurationSetTests.swift b/CLI/Tests/KnitCodeGenTests/ConfigurationSetTests.swift index 7ce132c..6102ed8 100644 --- a/CLI/Tests/KnitCodeGenTests/ConfigurationSetTests.swift +++ b/CLI/Tests/KnitCodeGenTests/ConfigurationSetTests.swift @@ -140,6 +140,7 @@ final class ConfigurationSetTests: XCTestCase { line: line ) } + @MainActor func assertCollectionResolves( _ type: T.Type, count expectedCount: Int, diff --git a/Example/KnitExample/KnitExampleAssembly.swift b/Example/KnitExample/KnitExampleAssembly.swift index 3d1206a..a9debe6 100644 --- a/Example/KnitExample/KnitExampleAssembly.swift +++ b/Example/KnitExample/KnitExampleAssembly.swift @@ -36,8 +36,22 @@ final class KnitExampleAssembly: ModuleAssembly { ClosureService(closure: arg1) } + container.register( + MainActorService.self, + mainActorFactory: { _ in + MainActorService() + } + ) + container.autoregisterIntoCollection(ExampleService.self, initializer: ExampleService.init) + container.registerIntoCollection( + MainActorService.self, + factory: { _ in + MainActorService() + } + ) + #if DEBUG container.autoregister(DebugService.self, initializer: DebugService.init) #endif @@ -81,3 +95,10 @@ final class ClosureService { } struct DebugService { } + +@MainActor +final class MainActorService { + init() { } + + var title: String { "Example String" } +} diff --git a/Sources/Knit/ServiceCollection/Container+ServiceCollection.erb b/Sources/Knit/ServiceCollection/Container+ServiceCollection.erb index 87c34c5..70330e9 100644 --- a/Sources/Knit/ServiceCollection/Container+ServiceCollection.erb +++ b/Sources/Knit/ServiceCollection/Container+ServiceCollection.erb @@ -33,9 +33,17 @@ extension Container { @discardableResult public func registerIntoCollection( _ service: Service.Type, - factory: @escaping (Resolver) -> Service + factory: @escaping @MainActor (Resolver) -> Service ) -> ServiceEntry { - self.register(service, name: makeUniqueCollectionRegistrationName(), factory: factory) + self.register( + service, + name: makeUniqueCollectionRegistrationName(), + factory: { resolver in + MainActor.assumeIsolated { + return factory(resolver) + } + } + ) } /// Registers a service factory into a collection. diff --git a/Sources/Knit/ServiceCollection/Container+ServiceCollection.swift b/Sources/Knit/ServiceCollection/Container+ServiceCollection.swift index a8f78e6..29bb905 100644 --- a/Sources/Knit/ServiceCollection/Container+ServiceCollection.swift +++ b/Sources/Knit/ServiceCollection/Container+ServiceCollection.swift @@ -32,9 +32,17 @@ extension Container { @discardableResult public func registerIntoCollection( _ service: Service.Type, - factory: @escaping (Resolver) -> Service + factory: @escaping @MainActor (Resolver) -> Service ) -> ServiceEntry { - self.register(service, name: makeUniqueCollectionRegistrationName(), factory: factory) + self.register( + service, + name: makeUniqueCollectionRegistrationName(), + factory: { resolver in + MainActor.assumeIsolated { + return factory(resolver) + } + } + ) } /// Registers a service factory into a collection. diff --git a/Sources/Knit/ServiceCollection/Resolver+ServiceCollection.swift b/Sources/Knit/ServiceCollection/Resolver+ServiceCollection.swift index 628b24d..5e56fe4 100644 --- a/Sources/Knit/ServiceCollection/Resolver+ServiceCollection.swift +++ b/Sources/Knit/ServiceCollection/Resolver+ServiceCollection.swift @@ -21,6 +21,7 @@ extension Resolver { /// - Parameter serviceType: The service types to resolve. /// - Returns: A ``ServiceCollection`` containing all registered services, /// or an empty collection if no services were registered. + @MainActor public func resolveCollection(_ serviceType: Service.Type) -> ServiceCollection { resolve(ServiceCollection.self) ?? .init(parent: nil, entries: []) } diff --git a/Tests/KnitTests/ServiceCollectorTests.swift b/Tests/KnitTests/ServiceCollectorTests.swift index d24380b..900bd3e 100644 --- a/Tests/KnitTests/ServiceCollectorTests.swift +++ b/Tests/KnitTests/ServiceCollectorTests.swift @@ -53,6 +53,7 @@ final class ServiceCollectorTests: XCTestCase { // MARK: - Tests - registerIntoCollection + @MainActor func test_registerIntoCollection() { let container = Container() container.addBehavior(ServiceCollector()) @@ -78,6 +79,7 @@ final class ServiceCollectorTests: XCTestCase { ) } + @MainActor func test_registerIntoCollection_emptyWithBehavior() { let container = Container() container.addBehavior(ServiceCollector()) @@ -86,6 +88,7 @@ final class ServiceCollectorTests: XCTestCase { XCTAssertEqual(collection.entries.count, 0) } + @MainActor func test_registerIntoCollection_emptyWithoutBehavior() { let container = Container() @@ -95,6 +98,7 @@ final class ServiceCollectorTests: XCTestCase { /// ``ServiceCollector`` shouldn't preclude users from registering their own separate ``Array``. /// A conflict here would be confusing and surprising to the user. + @MainActor func test_registerIntoCollection_doesntConflictWithArray() throws { let container = Container() container.addBehavior(ServiceCollector()) @@ -116,6 +120,7 @@ final class ServiceCollectorTests: XCTestCase { XCTAssert(array.first is ServiceB) } + @MainActor func test_registerIntoCollection_doesntImplicitlyAggregateInstances() throws { let container = Container() container.addBehavior(ServiceCollector()) @@ -137,6 +142,7 @@ final class ServiceCollectorTests: XCTestCase { XCTAssert(container.resolve(ServiceProtocol.self) is ServiceB) } + @MainActor func test_registerIntoCollection_allowsDuplicates() { let container = Container() container.addBehavior(ServiceCollector()) @@ -156,6 +162,7 @@ final class ServiceCollectorTests: XCTestCase { // MARK: - Tests - autoregisterIntoCollection + @MainActor func test_autoregisterIntoCollection() { let container = Container() container.addBehavior(ServiceCollector()) @@ -172,6 +179,7 @@ final class ServiceCollectorTests: XCTestCase { } // High-arity overloads are generated by a script. Ensure they work as expected. + @MainActor func test_autoregisterIntoCollection_highArityOverloads() { let container = Container() container.addBehavior(ServiceCollector()) @@ -195,6 +203,7 @@ final class ServiceCollectorTests: XCTestCase { // MARK: - Tests - Object Scopes + @MainActor func test_registerIntoCollection_supportsTransientScopedObjects() throws { let container = Container() container.addBehavior(ServiceCollector()) @@ -214,6 +223,7 @@ final class ServiceCollectorTests: XCTestCase { XCTAssert(instance1 !== instance2) } + @MainActor func test_registerIntoCollection_supportsContainerScopedObjects() throws { let container = Container() container.addBehavior(ServiceCollector()) @@ -233,6 +243,7 @@ final class ServiceCollectorTests: XCTestCase { XCTAssert(instance1 === instance2) } + @MainActor func test_registerIntoCollection_supportsWeakScopedObjects() throws { let container = Container() container.addBehavior(ServiceCollector()) @@ -264,6 +275,7 @@ final class ServiceCollectorTests: XCTestCase { XCTAssertEqual(factoryCallCount, 2) } + @MainActor func test_parentChildContainersWithAssemblers() { let parent = ModuleAssembler([AssemblyA()]) let child = ModuleAssembler(parent: parent, [AssemblyB()]) @@ -288,6 +300,7 @@ final class ServiceCollectorTests: XCTestCase { } + @MainActor func test_childWithEmptyParent() { let parent = ModuleAssembler([AssemblyC()]) let child = ModuleAssembler(parent: parent, [AssemblyB()]) @@ -303,7 +316,8 @@ final class ServiceCollectorTests: XCTestCase { 1 ) } - + + @MainActor func test_emptyChildWithParent() { let parent = ModuleAssembler([AssemblyB()]) let child = ModuleAssembler(parent: parent, [AssemblyC()]) @@ -321,6 +335,7 @@ final class ServiceCollectorTests: XCTestCase { ) } + @MainActor func test_grandparentRelationship() { let grandParent = ModuleAssembler([AssemblyA()]) let parent = ModuleAssembler(parent: grandParent, [AssemblyC()])