diff --git a/Docs/Modules.md b/Docs/Modules.md index 67cd9be..faef7a4 100644 --- a/Docs/Modules.md +++ b/Docs/Modules.md @@ -2,7 +2,7 @@ * Swinject provides the concept of an [Assembly](https://github.com/Swinject/Swinject/blob/master/Documentation/Assembler.md) which is responsible for registering a subset of the app services so that when all Assemblies are registered together via an Assembler all services are available. -`ModuleAssembly` extend the Assembly concept and define relationships between modules. +`ModuleAssembly` extends the Assembly concept to define relationships between modules. When services are assembled using `ModuleAssembler` it must be able to resolve the entire tree of modules as for each service we do not know which other services it may require. By enforcing that all child modules are also registered we can guarantee that all expected services have been registered. ## AutoInitModuleAssembly @@ -20,5 +20,3 @@ Overrides do not need to be in the same codebase as the original. The usual expe ## DefaultModuleAssemblyOverride Using `replaces` provides the power to swap DI module implementations but requires explicit setup. A base assembly that implements `DefaultModuleAssemblyOverride` can automatically choose the override when default overrides are enabled in the `ModuleAssembler`. This defaults to true for unit testing and false for normal app runs. - - diff --git a/Sources/Knit/Module/ModuleAssembler.swift b/Sources/Knit/Module/ModuleAssembler.swift index 2980376..6266036 100644 --- a/Sources/Knit/Module/ModuleAssembler.swift +++ b/Sources/Knit/Module/ModuleAssembler.swift @@ -33,7 +33,7 @@ public final class ModuleAssembler { If the closure throws an error for any of the assemblies then a fatal error will occur. - postAssemble: Hook after all assemblies are registered to make changes to the container. */ - public convenience init( + @MainActor public convenience init( parent: ModuleAssembler? = nil, _ modules: [any ModuleAssembly], overrideBehavior: OverrideBehavior = .defaultOverridesWhenTesting, @@ -66,7 +66,7 @@ public final class ModuleAssembler { } // Internal required init that throws rather than fatal errors - required init( + @MainActor required init( parent: ModuleAssembler? = nil, _modules modules: [any ModuleAssembly], overrideBehavior: OverrideBehavior = .defaultOverridesWhenTesting, @@ -93,8 +93,9 @@ public final class ModuleAssembler { let dependencyTree = builder.dependencyTree self._container.register(DependencyTree.self) { _ in dependencyTree } - let assembler = Assembler(container: self._container) - assembler.apply(assemblies: builder.assemblies) + for assembly in builder.assemblies { + assembly.assemble(container: self._container) + } postAssemble?(_container) if overrideBehavior.useAbstractPlaceholders { diff --git a/Sources/Knit/Module/ModuleAssembly.swift b/Sources/Knit/Module/ModuleAssembly.swift index 0c5fc62..ad128b7 100644 --- a/Sources/Knit/Module/ModuleAssembly.swift +++ b/Sources/Knit/Module/ModuleAssembly.swift @@ -5,7 +5,7 @@ import Foundation import Swinject -public protocol ModuleAssembly: Assembly { +public protocol ModuleAssembly { associatedtype TargetResolver @@ -13,6 +13,8 @@ public protocol ModuleAssembly: Assembly { static var dependencies: [any ModuleAssembly.Type] { get } + @MainActor func assemble(container: Container) + /// A ModuleAssembly can replace any number of other module assemblies. /// If this assembly replaces another it is expected to provide all registrations from the replaced assemblies. /// A common case is a fake assembly that registers fake services matching those from the original module. diff --git a/Sources/Knit/Module/ScopedModuleAssembler.swift b/Sources/Knit/Module/ScopedModuleAssembler.swift index 2b85616..4044048 100644 --- a/Sources/Knit/Module/ScopedModuleAssembler.swift +++ b/Sources/Knit/Module/ScopedModuleAssembler.swift @@ -19,6 +19,7 @@ public final class ScopedModuleAssembler { return internalAssembler._container } + @MainActor public convenience init( parent: ModuleAssembler? = nil, _ modules: [any ModuleAssembly], @@ -46,6 +47,7 @@ public final class ScopedModuleAssembler { } // Internal required init that throws rather than fatal errors + @MainActor required init( parent: ModuleAssembler? = nil, _modules modules: [any ModuleAssembly], diff --git a/Tests/KnitTests/ModuleAssemblerTests.swift b/Tests/KnitTests/ModuleAssemblerTests.swift index 2fffad2..a98d9c7 100644 --- a/Tests/KnitTests/ModuleAssemblerTests.swift +++ b/Tests/KnitTests/ModuleAssemblerTests.swift @@ -7,11 +7,13 @@ import XCTest final class ModuleAssemblerTests: XCTestCase { + @MainActor func test_auto_assembler() { let resolver = ModuleAssembler([Assembly1()]).resolver XCTAssertNotNil(resolver.resolve(Service1.self)) } + @MainActor func test_non_auto_assembler() { let resolver = ModuleAssembler([ Assembly3(), @@ -21,6 +23,7 @@ final class ModuleAssemblerTests: XCTestCase { XCTAssertNotNil(resolver.resolve(Service3.self)) } + @MainActor func test_registered_modules() { let assembler = ModuleAssembler([Assembly1()]) XCTAssertTrue(assembler.registeredModules.contains(where: {$0 == Assembly1.self})) @@ -28,6 +31,7 @@ final class ModuleAssemblerTests: XCTestCase { XCTAssertFalse(assembler.registeredModules.contains(where: {$0 == Assembly3.self})) } + @MainActor func test_parent_assembler() { // Put some modules in the parent and some in the child. let parent = ModuleAssembler([Assembly1()]) @@ -42,6 +46,7 @@ final class ModuleAssemblerTests: XCTestCase { XCTAssertNil(parent.resolver.resolve(Service3.self)) } + @MainActor func test_abstractAssemblyValidation() { XCTAssertThrowsError( try ModuleAssembler( @@ -60,6 +65,7 @@ final class ModuleAssemblerTests: XCTestCase { ) } + @MainActor func test_abstractAssemblyPlaceholders() throws { // No error is thrown as the graph is using abstract placeholders _ = try ModuleAssembler( diff --git a/Tests/KnitTests/ModuleAssemblyOverrideTests.swift b/Tests/KnitTests/ModuleAssemblyOverrideTests.swift index 9c3adb4..ddf43b4 100644 --- a/Tests/KnitTests/ModuleAssemblyOverrideTests.swift +++ b/Tests/KnitTests/ModuleAssemblyOverrideTests.swift @@ -35,16 +35,19 @@ final class ModuleAssemblyOverrideTests: XCTestCase { ) } + @MainActor func test_serviceRegisteredWithoutFakes() { let resolver = ModuleAssembler([Assembly2()]).resolver XCTAssertTrue(resolver.resolve(Service2Protocol.self) is Service2) } + @MainActor func test_servicesRegisteredWithFakes() { let resolver = ModuleAssembler([Assembly2(), Assembly2Fake()]).resolver XCTAssertTrue(resolver.resolve(Service2Protocol.self) is Service2Fake) } + @MainActor func test_assemblerWithDefaultOverrides() { let assembler = ModuleAssembler([Assembly2()], overrideBehavior: .useDefaultOverrides) XCTAssertTrue(assembler.registeredModules.contains(where: {$0 == Assembly1Fake.self})) @@ -53,6 +56,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase { XCTAssertTrue(assembler.isRegistered(Assembly1.self)) } + @MainActor func test_noDefaultOverrideForInputModules() { let assembler = ModuleAssembler([Assembly1()], overrideBehavior: .useDefaultOverrides) XCTAssertTrue(assembler.isRegistered(Assembly1.self)) @@ -60,18 +64,21 @@ final class ModuleAssemblyOverrideTests: XCTestCase { XCTAssertFalse(assembler.isRegistered(Assembly1Fake.self)) } + @MainActor func test_explicitInputOverride() { let assembler = ModuleAssembler([Assembly1(), Assembly1Fake()], overrideBehavior: .useDefaultOverrides) XCTAssertTrue(assembler.isRegistered(Assembly1.self)) XCTAssertTrue(assembler.isRegistered(Assembly1Fake.self)) } + @MainActor func test_assemblerWithoutDefaultOverrides() { let assembler = ModuleAssembler([Assembly2()], overrideBehavior: .disableDefaultOverrides) XCTAssertTrue(assembler.isRegistered(Assembly1.self)) XCTAssertFalse(assembler.isRegistered(Assembly1Fake.self)) } + @MainActor func test_assemblerWithFakes() { let assembler = ModuleAssembler([Assembly2Fake()]) XCTAssertFalse(assembler.registeredModules.contains(where: {$0 == Assembly2.self})) @@ -79,6 +86,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase { XCTAssertTrue(assembler.isRegistered(Assembly2Fake.self)) } + @MainActor func test_parentFakes() { let parent = ModuleAssembler([Assembly1Fake()]) let child = ModuleAssembler(parent: parent, [Assembly2()]) @@ -86,6 +94,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase { XCTAssertTrue(child.isRegistered(Assembly1Fake.self)) } + @MainActor func test_autoFake() { let assembler = ModuleAssembler([Assembly5()]) XCTAssertTrue(assembler.isRegistered(Assembly4.self)) @@ -93,6 +102,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase { XCTAssertTrue(assembler.isRegistered(Assembly5.self)) } + @MainActor func test_overrideDefaultOverride() { let assembler = ModuleAssembler( [Assembly4(), Assembly4Fake2()], @@ -111,6 +121,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase { XCTAssertTrue(builder.assemblies.first is NonAutoOverride) } + @MainActor func test_parentNonAutoOverride() { let parent = ModuleAssembler([NonAutoOverride()]) let child = ModuleAssembler(parent: parent, [Assembly1()], overrideBehavior: .disableDefaultOverrides) @@ -125,6 +136,7 @@ final class ModuleAssemblyOverrideTests: XCTestCase { ) } + @MainActor func test_multipleOverrides() { let assembler = ModuleAssembler( [MultipleDependencyAssembly(), MultipleOverrideAssembly()], diff --git a/Tests/KnitTests/ModuleCycleTests.swift b/Tests/KnitTests/ModuleCycleTests.swift index bd26c23..fb85da7 100644 --- a/Tests/KnitTests/ModuleCycleTests.swift +++ b/Tests/KnitTests/ModuleCycleTests.swift @@ -7,6 +7,7 @@ import XCTest final class ModuleCycleTests: XCTestCase { + @MainActor func test_cycleResolution() { let assembler = ModuleAssembler([Assembly1()]) XCTAssertTrue(assembler.isRegistered(Assembly1.self)) @@ -25,6 +26,7 @@ final class ModuleCycleTests: XCTestCase { ) } + @MainActor func test_sourceCycle() { let assembler = ModuleAssembler([Assembly5()]) XCTAssertEqual( diff --git a/Tests/KnitTests/ScopedModuleAssemblerTests.swift b/Tests/KnitTests/ScopedModuleAssemblerTests.swift index de856db..58a8cf8 100644 --- a/Tests/KnitTests/ScopedModuleAssemblerTests.swift +++ b/Tests/KnitTests/ScopedModuleAssemblerTests.swift @@ -8,12 +8,14 @@ import XCTest final class ScopedModuleAssemblerTests: XCTestCase { + @MainActor func testScoping() throws { // Allows modules at the same level to be registered let assembler = try ScopedModuleAssembler(_modules: [Assembly1()]) XCTAssertEqual(assembler.internalAssembler.registeredModules.count, 1) } + @MainActor func testParentExcluded() throws { let parent = try ScopedModuleAssembler(_modules: [Assembly1()]) let assembler = try ScopedModuleAssembler( @@ -23,6 +25,7 @@ final class ScopedModuleAssemblerTests: XCTestCase { XCTAssertEqual(assembler.internalAssembler.registeredModules.count, 1) } + @MainActor func testPostAssemble() throws { let assembler = try ScopedModuleAssembler(_modules: [Assembly1()]) { container in container.register(String.self) { _ in "string" } @@ -30,6 +33,7 @@ final class ScopedModuleAssemblerTests: XCTestCase { XCTAssertEqual(assembler.resolver.resolve(String.self), "string") } + @MainActor func testOutOfScopeAssemblyThrows() { XCTAssertThrowsError( try ScopedModuleAssembler( @@ -49,6 +53,7 @@ final class ScopedModuleAssemblerTests: XCTestCase { ) } + @MainActor func testIncorrectInputScope() throws { let parent = try ScopedModuleAssembler(_modules: [Assembly1()]) // Even though Assembly1 is already registered, because it was explicitly provided the validation should fail diff --git a/Tests/KnitTests/SynchronizationTests.swift b/Tests/KnitTests/SynchronizationTests.swift index da3a93e..cb0c3b3 100644 --- a/Tests/KnitTests/SynchronizationTests.swift +++ b/Tests/KnitTests/SynchronizationTests.swift @@ -8,6 +8,7 @@ import XCTest final class SynchronizationTests: XCTestCase { + @MainActor func testMultiThreadResolving() async throws { // Use a parent/child relationship to test synchronization between containers let parent = ModuleAssembler([Assembly1()]) @@ -28,6 +29,7 @@ final class SynchronizationTests: XCTestCase { XCTAssertEqual(result.0.service1.id, result.1.service1.id) } + @MainActor func testMultiThreadingScopedAssembler() async throws { let assembler = ScopedModuleAssembler([Assembly2()])