├── .spi.yml ├── Sources ├── Macaroni.swift ├── ContainerLookupPolicies │ ├── SingletonContainerLookupPolicy.swift │ ├── ContainerableLookupPolicy.swift │ └── ContainerLookupPolicy+Resolve.swift ├── ContainerLookupPolicy.swift ├── RegistrationAlternative.swift ├── Logging │ └── MacaroniLogger.swift ├── PropertyWrappers │ ├── InjectedWeakly.swift │ └── Injected.swift └── Container.swift ├── Package.swift ├── Tests └── MacaroniTests │ ├── Tests │ ├── SimpleContainerTests.swift │ ├── InjectedLazyTests.swift │ ├── InjectedAsParameterTests.swift │ ├── ParentContainerTests.swift │ ├── SimpleContainerWithParameterTests.swift │ ├── InjectedMultithreadedProblemsTests.swift │ ├── InjectedWeaklyTests.swift │ ├── ContainerSelectorTests.swift │ ├── ContainerResolveSpeedTests.swift │ ├── InjectedEagerTests.swift │ ├── ContainerFindPolicyTests.swift │ ├── InjectedTests.swift │ └── AlternativeTests.swift │ └── WaitForDeathTrap.swift ├── LICENSE ├── UPDATES.md ├── .gitignore └── README.md /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 4.0.0 2 | builder: 3 | configs: 4 | - documentation_targets: [ Macaroni ] 5 | -------------------------------------------------------------------------------- /Sources/Macaroni.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Macaroni 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 20 August 2023. 6 | // Copyright © 2023 Alex Babaev. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum Macaroni { 12 | /// By default logging messages are being printed in the console. 13 | public nonisolated(unsafe) static var logger: MacaroniLogger = SimpleMacaroniLogger() 14 | 15 | public static func set(lookupPolicy: ContainerLookupPolicy) { 16 | Container.lookupPolicy = lookupPolicy 17 | } 18 | 19 | public static func set(logger: MacaroniLogger) { 20 | self.logger = logger 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.8 2 | 3 | // 4 | // Package.swift 5 | // Macaroni 6 | // 7 | // Created by Alex Babaev on 20 March 2021. 8 | // Copyright © 2021 Alex Babaev. All rights reserved. 9 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 10 | // 11 | 12 | import PackageDescription 13 | 14 | let package = Package( 15 | name: "Macaroni", 16 | platforms: [ .iOS(.v12), .macOS(.v10_14) ], 17 | products: [ 18 | .library(name: "Macaroni", targets: [ "Macaroni" ]), 19 | ], 20 | targets: [ 21 | .target(name: "Macaroni", dependencies: [], path: "Sources"), 22 | 23 | .testTarget(name: "MacaroniTests", dependencies: ["Macaroni"]), 24 | ], 25 | swiftLanguageVersions: [.v5] 26 | ) 27 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/SimpleContainerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleContainerTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class SimpleContainerTests: BaseTestCase { 14 | private class TestInjectedType {} 15 | 16 | private var container: Container! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | 21 | container = Container() 22 | container.register { TestInjectedType() } 23 | } 24 | 25 | func testSimpleRegistration() throws { 26 | let value: TestInjectedType? = try container.resolve() 27 | XCTAssertNotNil(value, "Could not resolve value") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/ContainerLookupPolicies/SingletonContainerLookupPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SingletonContainerLookupPolicy 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 08 September 2022. 6 | // Copyright © 2022 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | public extension ContainerLookupPolicy where Self == SingletonContainer { 11 | static func singleton(_ container: Container) -> ContainerLookupPolicy { 12 | SingletonContainer(container) 13 | } 14 | } 15 | 16 | public class SingletonContainer: ContainerLookupPolicy { 17 | private let container: Container 18 | 19 | public init(_ container: Container) { 20 | self.container = container 21 | } 22 | 23 | public func container( 24 | for instance: EnclosingType, 25 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 26 | ) -> Container? { 27 | container 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Alex Babaev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Sources/ContainerLookupPolicies/ContainerableLookupPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerableLookupPolicy 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 08 September 2022. 6 | // Copyright © 2022 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | public protocol Containerable { 11 | var container: Container! { get } 12 | } 13 | 14 | public extension ContainerLookupPolicy where Self == EnclosingTypeContainer { 15 | static func enclosingType(default: Container? = nil) -> ContainerLookupPolicy { 16 | EnclosingTypeContainer(default: `default`) 17 | } 18 | } 19 | 20 | public class EnclosingTypeContainer: ContainerLookupPolicy { 21 | private let defaultContainer: Container? 22 | 23 | public init(default: Container? = nil) { 24 | defaultContainer = `default` 25 | } 26 | 27 | public func container( 28 | for instance: EnclosingType, 29 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 30 | ) -> Container? { 31 | (instance as? Containerable)?.container 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/WaitForDeathTrap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WaitForDeathTrap 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 7 September 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class BaseTestCase: XCTestCase { 14 | class TestMacaroniLogger: MacaroniLogger { 15 | let deathHandler: () -> Void 16 | 17 | init(deathHandler: @escaping () -> Void) { 18 | self.deathHandler = deathHandler 19 | } 20 | 21 | func log(_ message: String, level: MacaroniLoggingLevel, file: StaticString, function: String, line: UInt) { 22 | } 23 | 24 | func die(_ message: String, file: StaticString, function: String, line: UInt) -> Never { 25 | deathHandler() 26 | while true { Thread.sleep(forTimeInterval: 1000) /* hang here */ } 27 | } 28 | } 29 | 30 | func waitForDeathTrap(description: String, testCase: @escaping () -> Void) { 31 | let expectation = self.expectation(description: description) 32 | Macaroni.logger = TestMacaroniLogger { 33 | Macaroni.logger = SimpleMacaroniLogger() 34 | expectation.fulfill() 35 | } 36 | 37 | DispatchQueue.global(qos: .userInitiated).async(execute: testCase) 38 | waitForExpectations(timeout: 1) { _ in 39 | // wait 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/InjectedLazyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class InjectedLazyTests: BaseTestCase { 14 | let container = Container() 15 | 16 | override func setUp() { 17 | class LazyContainer { 18 | private static var counter: Int = 0 19 | lazy var value: String = { 20 | Self.counter += 1 21 | print("Created value for injection, counter: \(Self.counter)") 22 | return "SomeValue \(Self.counter)" 23 | }() 24 | init() {} 25 | } 26 | 27 | container.cleanup() 28 | let lazyContainer = LazyContainer() 29 | container.register { () -> String in lazyContainer.value } 30 | Container.lookupPolicy = .singleton(container) 31 | addTeardownBlock { Container.lookupPolicy = nil } 32 | print("Created container") 33 | } 34 | 35 | func testLazyInjection() { 36 | let value1: String = try! container.resolve() 37 | print("First resolve: \(value1)") 38 | let value2: String = try! container.resolve() 39 | print("Second resolve: \(value2)") 40 | 41 | XCTAssertEqual(value1, value2) 42 | XCTAssertEqual(value1, "SomeValue 1") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/ContainerLookupPolicies/ContainerLookupPolicy+Resolve.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerLookupPolicy 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 08 September 2022. 6 | // Copyright © 2022 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | extension ContainerLookupPolicy { 11 | func resolve( 12 | for instance: EnclosingType, option: String? = nil, 13 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 14 | ) -> Value { 15 | guard let container = container(for: instance, file: file, function: function, line: line) else { 16 | let enclosingType = String(reflecting: instance.self) 17 | Macaroni.logger.die(message: "Can't find container in \"\(enclosingType)\" object", file: file, function: function, line: line) 18 | } 19 | 20 | let value: Value 21 | do { 22 | value = try container.resolve(parameter: instance, alternative: option) 23 | } catch { 24 | do { 25 | value = try container.resolve(alternative: option) 26 | } catch { 27 | let valueType = String(reflecting: Value.self) 28 | let enclosingType = String(reflecting: instance.self) 29 | Macaroni.logger.die(message: "Can't find resolver for \"\(valueType)\" type in \"\(enclosingType)\" object") 30 | } 31 | } 32 | return value 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /UPDATES.md: -------------------------------------------------------------------------------- 1 | # New Features and Migrations 2 | 3 | This document contains only short feature and update descriptions. Please refer to [README.md](README.md) for details. 4 | 5 | ## Version 4. Some renames. No new functionality. 6 | 7 | Not a lot changed in this version, but I had to change the version number because API changed. 8 | - `@Injected(container: container)` now should be written as: `@Injected(.resolvingOnInit(from: container))` 9 | - If you've used `@Injected(captureContainerLookupOnInit: false)` for lazily initialize both container and property itself 10 | now you can use it like this: `@Injected(.lazily)` 11 | - `Container.resolvable` is being renamed to `Container.isResolvable` 12 | 13 | ## Version 3 14 | 15 | In this version main updates are: 16 | - change in `Container.policy`. It is more configurable now, because it is not an enum, but a protocol `ContainerFindable`. 17 | Old behavior is recreated in `.singleton(...)`, `.enclosingType`. Custom policy removed, please 18 | create your own implementation for that. 19 | - `@Injected` has two options now (`captureContainerLookupOnInit` parameter): 20 | - old one, when container is being searched lazily, on first access. Container.policy is being accessed for that 21 | and container is being looked up by it. 22 | - new one, when container lookup policy is being captured when injected property is created. Container is still 23 | being looked up lazily on first property access, but container lookup is being captured strongly during 24 | initialization. 25 | -------------------------------------------------------------------------------- /Sources/ContainerLookupPolicy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerFindable 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 30 May 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | public extension Container { 11 | /// This property is being used to find out container search policy. 12 | /// Please set it up before any @Injected (and others) property wrappers are used. 13 | static nonisolated(unsafe) var lookupPolicy: ContainerLookupPolicy! 14 | } 15 | 16 | /// ContainerLookupPolicy is a protocol that can control, how container for an injection is being looked up, if property wrapper is used. 17 | /// For now the only parameter that you can use for that is an instance of a reference type, where injection is happening. 18 | public protocol ContainerLookupPolicy: AnyObject { 19 | /// Implement this to be able to look up for the container. For examples see [ContainerFindable.Implementations.swift] 20 | /// (ContainerFindable.Implementations.swift) 21 | /// - Parameters: 22 | /// - instance: instance of a reference type injection will happen. 23 | /// - file: call originating file, used for logging 24 | /// - function: call originating function, used for logging 25 | /// - line: call originating line, used for logging 26 | /// - Returns: Container that will be used for the injection (if any). 27 | func container(for instance: EnclosingType, file: StaticString, function: String, line: UInt) -> Container? 28 | } 29 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/InjectedAsParameterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | #if swift(>=5.5) 14 | 15 | private let testStringValue: String = "Yes Service!" 16 | 17 | private protocol MyService: AnyObject { 18 | var testValue: String { get } 19 | } 20 | 21 | private class MyServiceImplementation: MyService { 22 | var testValue: String = testStringValue 23 | } 24 | 25 | class InjectedAsParameterTests: BaseTestCase { 26 | private var container = Container() 27 | 28 | override func setUp() { 29 | container = Container() 30 | container.register { () -> String in testStringValue } 31 | Container.lookupPolicy = .singleton(container) 32 | } 33 | 34 | override func tearDown() { 35 | super.tearDown() 36 | 37 | Container.lookupPolicy = nil 38 | } 39 | 40 | func testSimpleInjected() { 41 | func test(@Injected value: String) -> String { 42 | value 43 | } 44 | 45 | let result = test($value: .object(self)) 46 | XCTAssertTrue(result == testStringValue) 47 | } 48 | 49 | func testSimpleNonInjected() { 50 | func test(@Injected value: String) -> String { 51 | value 52 | } 53 | 54 | let result = test(value: "not-injected") 55 | XCTAssertTrue(result == "not-injected") 56 | } 57 | } 58 | 59 | #endif 60 | -------------------------------------------------------------------------------- /Sources/RegistrationAlternative.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Alternative 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 13 August 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import Foundation 11 | 12 | /// If you need to add several objects of same type to the container, it will not be able to distinguish them. 13 | /// To be able to do that, you can use `RegistrationAlternative`. 14 | /// 15 | /// First, create a property of this type. The easiest way is to create it right inside `RegistrationAlternative` extension: 16 | /// ``` 17 | /// extension RegistrationAlternative { 18 | /// static let first: RegistrationAlternative = .init() // not recommended, logs will be hard to read. 19 | /// static let second: RegistrationAlternative = "second" 20 | /// } 21 | /// ``` 22 | /// 23 | /// Then use this property during registration and during injection: 24 | /// ``` 25 | /// container.register(alternative: .firstAlternative) { ... -> ... } 26 | /// let ...: ... = container.resolve(alternative: .firstAlternative) 27 | /// 28 | /// // or 29 | /// 30 | /// @Injected(alternative: .second) 31 | /// var instance: ... 32 | /// ``` 33 | public struct RegistrationAlternative: ExpressibleByStringLiteral, Sendable { 34 | /// Name of the registration alternative. Is shown in logs. 35 | var name: String 36 | 37 | public init(_ value: StringLiteralType = UUID().uuidString) { 38 | name = value 39 | } 40 | 41 | public init(stringLiteral value: StringLiteralType = UUID().uuidString) { 42 | name = value 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/ParentContainerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ParentContainerTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class ParentContainerTests: BaseTestCase { 14 | private class TestInjectedType {} 15 | 16 | private class TestParametrizedInjectedType { 17 | var property: String 18 | 19 | init(property: String) { 20 | self.property = property 21 | } 22 | } 23 | 24 | private var controlValue: Int! 25 | private var childContainer: Container! 26 | 27 | override func setUp() { 28 | super.setUp() 29 | 30 | controlValue = Int.random(in: Int.min ... Int.max) 31 | let parentContainer = Container() 32 | parentContainer.register { TestInjectedType() } 33 | parentContainer.register { parameter in TestParametrizedInjectedType(property: "\(parameter)") } 34 | childContainer = Container(parent: parentContainer) 35 | } 36 | 37 | func testSimpleRegistration() throws { 38 | let value: TestInjectedType? = try childContainer.resolve() 39 | XCTAssertNotNil(value, "Could not resolve value") 40 | } 41 | 42 | func testParametrizedRegistration() throws { 43 | let testPropertyValue: String = "SomePropertyValue" 44 | let value: TestParametrizedInjectedType? = try childContainer.resolve(parameter: testPropertyValue) 45 | XCTAssertTrue(value?.property == testPropertyValue) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/SimpleContainerWithParameterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SimpleContainerWithParameterTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class SimpleContainerWithParameterTests: BaseTestCase { 14 | private class TestInjectedType { 15 | var control: Int 16 | 17 | init(control: Int) { 18 | self.control = control 19 | } 20 | } 21 | 22 | private var container: Container! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | 27 | container = Container() 28 | container.register { control -> TestInjectedType in 29 | guard let control = control as? Int else { fatalError("Ouch") } 30 | return TestInjectedType(control: control) 31 | } 32 | } 33 | 34 | func testRegistrationWithParameterGetWithoutParameter() { 35 | let value: TestInjectedType? 36 | do { 37 | value = try container.resolve() 38 | XCTAssertNil(value, "Value is wrongly resolved without parameter") 39 | } catch { 40 | XCTAssertNotNil(error, "Value is wrongly resolved without parameter") 41 | } 42 | } 43 | 44 | func testRegistrationWithParameter() throws { 45 | let controlValue = Int.random(in: Int.min ... Int.max) 46 | let value: TestInjectedType? = try container.resolve(parameter: controlValue) 47 | 48 | XCTAssertNotNil(value, "Could not resolve value") 49 | XCTAssertEqual(value?.control, controlValue, "Resolved value is wrong") 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/InjectedMultithreadedProblemsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedMultithreadedProblemsTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 18 December 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import Foundation 11 | import XCTest 12 | import Macaroni 13 | 14 | class InjectedMultithreadedProblemsTests: XCTestCase { 15 | private class InjectInto { 16 | @Injected 17 | var string: String 18 | 19 | func useString() -> String { 20 | string 21 | } 22 | } 23 | 24 | private class InjectIntoWeakly { 25 | @InjectedWeakly 26 | var string: String? 27 | 28 | func useString() -> String? { 29 | string 30 | } 31 | } 32 | 33 | func testWhenContainerDeinitedWhenInjecting() { 34 | let testString = "Injected String" 35 | 36 | var container: Container? = Container() 37 | Container.lookupPolicy = .singleton(container!) 38 | container?.register { () -> String in testString } 39 | 40 | let injectInto = InjectInto() 41 | container = nil 42 | Container.lookupPolicy = nil 43 | 44 | XCTAssertEqual(injectInto.useString(), testString) 45 | } 46 | 47 | func testWhenContainerDeinitedWhenInjectingWeakly() { 48 | let testString = "Injected String" 49 | 50 | var container: Container? = Container() 51 | Container.lookupPolicy = .singleton(container!) 52 | container?.register { () -> String? in testString } 53 | 54 | let injectInto = InjectIntoWeakly() 55 | container = nil 56 | Container.lookupPolicy = nil 57 | 58 | XCTAssertEqual(injectInto.useString(), testString) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # AppCode 2 | .idea 3 | 4 | # Xcode 5 | # 6 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 7 | .build/ 8 | .swiftpm/ 9 | 10 | ## User settings 11 | xcuserdata/ 12 | 13 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 14 | *.xcscmblueprint 15 | *.xccheckout 16 | 17 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 18 | build/ 19 | DerivedData/ 20 | *.moved-aside 21 | *.pbxuser 22 | !default.pbxuser 23 | *.mode1v3 24 | !default.mode1v3 25 | *.mode2v3 26 | !default.mode2v3 27 | *.perspectivev3 28 | !default.perspectivev3 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | 33 | ## App packaging 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | 42 | # Swift Package Manager 43 | # 44 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 45 | # Packages/ 46 | # Package.pins 47 | # Package.resolved 48 | # *.xcodeproj 49 | # 50 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 51 | # hence it is not needed unless you have added a package configuration file to your project 52 | # .swiftpm 53 | 54 | .build/ 55 | 56 | # fastlane 57 | # 58 | # It is recommended to not store the screenshots in the git repo. 59 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 60 | # For more information about the recommended setup visit: 61 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 62 | 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots/**/*.png 66 | fastlane/test_output 67 | 68 | # Code Injection 69 | # 70 | # After new code Injection tools there's a generated folder /iOSInjectionProject 71 | # https://github.com/johnno1962/injectionforxcode 72 | 73 | iOSInjectionProject/ 74 | 75 | # Common 76 | 77 | .DS_Store 78 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/InjectedWeaklyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedWeaklyTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | private let testStringValue: String = "Yes Service!" 14 | 15 | private protocol MyService: AnyObject {} 16 | private class MyServiceImplementation: MyService {} 17 | 18 | private class MyController { 19 | @InjectedWeakly 20 | var myService: MyService? 21 | } 22 | 23 | class InjectedWeaklyTests: BaseTestCase { 24 | static let container = Container() 25 | 26 | override class func setUp() { 27 | Container.lookupPolicy = .singleton(container) 28 | } 29 | 30 | override class func tearDown() { 31 | super.tearDown() 32 | Container.lookupPolicy = nil 33 | } 34 | 35 | override func setUp() { 36 | Self.container.cleanup() 37 | } 38 | 39 | func testWeaklyInjected() { 40 | let register: () -> MyService = { 41 | let service: MyService = MyServiceImplementation() 42 | Self.container.register { [weak service] in service } 43 | return service 44 | } 45 | 46 | var service: MyService? = register() 47 | print("(just need to silence warning) \(String(describing: service))") 48 | let testObject = MyController() 49 | XCTAssertNotNil(testObject.myService) 50 | service = nil 51 | XCTAssertNil(testObject.myService) 52 | } 53 | 54 | func testWeaklyNilInjected() { 55 | Self.container.register { () -> MyService? in nil } 56 | 57 | let testObject = MyController() 58 | XCTAssertNil(testObject.myService) 59 | } 60 | 61 | func testSingleInjected() { 62 | Self.container.register { () -> MyService? in MyServiceImplementation() } 63 | let testObject = MyController() 64 | 65 | let myService1 = testObject.myService 66 | let myService2 = testObject.myService 67 | 68 | XCTAssertTrue(myService1 === myService2) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/ContainerSelectorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerSelectorTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class ContainerSelectorTests: BaseTestCase { 14 | private class TestInjectedType {} 15 | 16 | class MyController1 { 17 | @Injected 18 | var string: String 19 | } 20 | 21 | class MyController2 { 22 | @Injected 23 | var string: String 24 | } 25 | 26 | class CustomContainer: ContainerLookupPolicy { 27 | private let container1: Container 28 | private let container2: Container 29 | private let defaultContainer: Container 30 | 31 | init(container1: Container, container2: Container, defaultContainer: Container) { 32 | self.container1 = container1 33 | self.container2 = container2 34 | self.defaultContainer = defaultContainer 35 | } 36 | 37 | func container( 38 | for instance: EnclosingType, file: StaticString = #fileID, function: String = #function, line: UInt = #line 39 | ) -> Container? { 40 | switch instance { 41 | case is MyController1: return container1 42 | case is MyController2: return container2 43 | default: return defaultContainer 44 | } 45 | } 46 | } 47 | 48 | func testDefaultScope() throws { 49 | let checkStringScope1 = "String for scope 1" 50 | let checkStringScope2 = "String for scope 2" 51 | 52 | let defaultContainer = Container() 53 | let container1 = Container() 54 | let container2 = Container() 55 | container1.register { () -> String in checkStringScope1 } 56 | container2.register { () -> String in checkStringScope2 } 57 | Container.lookupPolicy = CustomContainer(container1: container1, container2: container2, defaultContainer: defaultContainer) 58 | addTeardownBlock { Container.lookupPolicy = nil } 59 | 60 | let myController1 = MyController1() 61 | let myController2 = MyController2() 62 | 63 | XCTAssertEqual(myController1.string, checkStringScope1) 64 | XCTAssertEqual(myController2.string, checkStringScope2) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/ContainerResolveSpeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerResolveSpeedTest 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 November 2022. 6 | // Copyright © 2022 Alex Babaev. All rights reserved. 7 | // 8 | 9 | import Macaroni 10 | import XCTest 11 | 12 | class ContainerResolveSpeedTests: XCTestCase { 13 | private var container: Container = .init() 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | container.register { () -> String in "Some String" } 19 | container.register { () -> Int in 239 } 20 | container.register { () -> Double in 239.239 } 21 | } 22 | 23 | override func tearDown() { 24 | super.tearDown() 25 | 26 | container.cleanup() 27 | } 28 | 29 | func testResolvingSpeed() throws { 30 | if #available(iOS 16, macOS 13, *) { 31 | let clock = ContinuousClock() 32 | let elapsed = clock.measure { 33 | for _ in 0 ... 100000 { 34 | let _: String? = try? container.resolve() 35 | let _: Int? = try? container.resolve() 36 | let _: Double? = try? container.resolve() 37 | } 38 | } 39 | XCTAssertTrue(elapsed < .seconds(1), "Resolving is to slow for some reason (limit is 0.5 seconds for 300 000 resolves") 40 | } else { 41 | measure { 42 | for _ in 0 ... 10000 { 43 | let _: String? = try? container.resolve() 44 | let _: Int? = try? container.resolve() 45 | let _: Double? = try? container.resolve() 46 | } 47 | } 48 | } 49 | } 50 | 51 | func testResolvingSpeedLocked() throws { 52 | container.lock() 53 | 54 | if #available(iOS 16, macOS 13, *) { 55 | let clock = ContinuousClock() 56 | let elapsed = clock.measure { 57 | for _ in 0 ... 100000 { 58 | let _: String? = try? container.resolve() 59 | let _: Int? = try? container.resolve() 60 | let _: Double? = try? container.resolve() 61 | } 62 | } 63 | XCTAssertTrue(elapsed < .seconds(0.5), "Resolving is to slow for some reason (limit is 0.5 seconds for 300 000 resolves") 64 | } else { 65 | measure { 66 | for _ in 0 ... 10000 { 67 | let _: String? = try? container.resolve() 68 | let _: Int? = try? container.resolve() 69 | let _: Double? = try? container.resolve() 70 | } 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/InjectedEagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedEagerTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | private let testStringValue: String = "Yes Service!" 14 | 15 | private struct ToInject { 16 | var value: String 17 | } 18 | 19 | private protocol MyService { 20 | var testValue: String { get } 21 | } 22 | 23 | private class MyServiceImplementation: MyService { 24 | var testValue: String = testStringValue 25 | } 26 | 27 | private enum MyContainerHolder { 28 | static var container: Container = { 29 | let container = Container() 30 | container.register { () -> Int? in nil } 31 | container.register { () -> ToInject in .init(value: testStringValue) } 32 | container.register { () -> MyService in MyServiceImplementation() } 33 | container.register { (_) -> String in testStringValue } 34 | return container 35 | }() 36 | } 37 | 38 | private class MyController { 39 | @Injected(.resolvingOnInit(from: MyContainerHolder.container)) 40 | var myService: MyService 41 | } 42 | 43 | private class MyControllerWrongInjectedType { 44 | @Injected(.resolvingOnInit(from: MyContainerHolder.container)) 45 | var myService: MyServiceImplementation 46 | } 47 | 48 | private class MyControllerNilInjected { 49 | @Injected(.resolvingOnInit(from: MyContainerHolder.container)) 50 | var myValue: Int 51 | } 52 | 53 | private class MyControllerParametrizedInjected { 54 | @Injected(.resolvingOnInit(from: MyContainerHolder.container)) 55 | var myValue: String 56 | } 57 | 58 | private class MyControllerInjectedWithWrapped { 59 | @Injected 60 | var property: ToInject = try! MyContainerHolder.container.resolve() 61 | } 62 | 63 | class InjectedEagerTests: BaseTestCase { 64 | func testSimpleInjected() { 65 | let testObject = MyController() 66 | XCTAssertEqual(testObject.myService.testValue, testStringValue) 67 | } 68 | 69 | func testWrongTypeInjected() { 70 | waitForDeathTrap(description: "Wrong type injected") { 71 | _ = MyControllerWrongInjectedType() 72 | } 73 | } 74 | 75 | func testNilInjected() { 76 | waitForDeathTrap(description: "Nil injected") { 77 | _ = MyControllerNilInjected() 78 | } 79 | } 80 | 81 | func testParametrizedInjected() { 82 | waitForDeathTrap(description: "Parametrized injected") { 83 | _ = MyControllerParametrizedInjected() 84 | } 85 | } 86 | 87 | func testInjectedWithWrapped() { 88 | let testObject = MyControllerInjectedWithWrapped() 89 | XCTAssertEqual(testObject.property.value, testStringValue) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/Logging/MacaroniLogger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Macaroni 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 20 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | /// Logging levels that are used by Macaroni. 11 | public enum MacaroniLoggingLevel: Equatable { 12 | case debug 13 | case error 14 | } 15 | 16 | /// Simple logging protocol. You can implement it to send Macaroni logs to your logging system. 17 | public protocol MacaroniLogger { 18 | func log(_ message: String, level: MacaroniLoggingLevel, file: StaticString, function: String, line: UInt) 19 | func die(_ message: String, file: StaticString, function: String, line: UInt) -> Never 20 | } 21 | 22 | /// Default Macaroni logging methods. 23 | extension MacaroniLogger { 24 | @inlinable 25 | func log(message: @autoclosure () -> String, level: MacaroniLoggingLevel, file: StaticString = #fileID, function: String = #function, line: UInt = #line) { 26 | log(message(), level: level, file: file, function: function, line: line) 27 | } 28 | 29 | @inlinable 30 | func debug(message: @autoclosure () -> String, file: StaticString = #fileID, function: String = #function, line: UInt = #line) { 31 | log(message(), level: .debug, file: file, function: function, line: line) 32 | } 33 | 34 | @inlinable 35 | func die(message: @autoclosure () -> String, file: StaticString = #fileID, function: String = #function, line: UInt = #line) -> Never { 36 | log(message(), level: .error, file: file, function: function, line: line) 37 | die(message(), file: file, function: function, line: line) 38 | } 39 | } 40 | 41 | /// Default Macaroni logger, that shows everything in the console. 42 | public final class SimpleMacaroniLogger: MacaroniLogger { 43 | public init() { 44 | } 45 | 46 | public func log(_ message: String, level: MacaroniLoggingLevel, file: StaticString, function: String, line: UInt) { 47 | let levelString: String 48 | switch level { 49 | case .debug: levelString = "👣" 50 | case .error: levelString = "👿" 51 | } 52 | print("\(levelString) \(file):\(line) \(message)") 53 | } 54 | 55 | public func die(_ message: String, file: StaticString, function: String, line: UInt) -> Never { 56 | fatalError("Fatal error occurred during dependency resolving: \(message)", file: file, line: line) 57 | } 58 | } 59 | 60 | /// Macaroni logger that discards all logging messages. 61 | public final class DisabledMacaroniLogger: MacaroniLogger { 62 | public init() { 63 | } 64 | 65 | public func log(_ message: String, level: MacaroniLoggingLevel, file: StaticString, function: String, line: UInt) {} 66 | 67 | public func die(_ message: String, file: StaticString, function: String, line: UInt) -> Never { 68 | fatalError("Fatal error occurred during dependency resolving: \(message)", file: file, line: line) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/ContainerFindPolicyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContainerFindPolicyTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 7 September 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | class ContainerFindPolicyTests: BaseTestCase { 14 | class ToInjectClass { 15 | @Injected 16 | var property: String 17 | } 18 | 19 | struct ToInjectStruct { 20 | @Injected 21 | var property: String 22 | } 23 | 24 | class ToInjectClassContainerable: Containerable { 25 | private(set) var container: Container! 26 | 27 | @Injected 28 | var property: String 29 | 30 | init(container: Container) { 31 | self.container = container 32 | } 33 | } 34 | 35 | override func tearDown() { 36 | super.tearDown() 37 | 38 | Container.lookupPolicy = nil 39 | } 40 | 41 | private let testString = "Injected String" 42 | 43 | func testNoResolvePolicyInClass() { 44 | let container = Container() 45 | container.register { [self] () -> String in testString } 46 | Container.lookupPolicy = nil 47 | 48 | waitForDeathTrap(description: "No Resolve Policy (class)") { 49 | let instance = ToInjectClass() 50 | _ = instance.property 51 | } 52 | } 53 | 54 | func testNoResolvePolicyInStruct() { 55 | let container = Container() 56 | container.register { [self] () -> String in testString } 57 | Container.lookupPolicy = nil 58 | 59 | waitForDeathTrap(description: "No Resolve Policy (struct)") { 60 | let instance = ToInjectStruct() 61 | _ = instance.property 62 | } 63 | } 64 | 65 | func testSingletonPolicyInClass() { 66 | let container = Container() 67 | container.register { [self] () -> String in testString } 68 | Container.lookupPolicy = .singleton(container) 69 | 70 | let instance = ToInjectClass() 71 | XCTAssertTrue(instance.property == testString) 72 | } 73 | 74 | func testSingletonPolicyInStruct() { 75 | let container = Container() 76 | container.register { [self] () -> String in testString } 77 | Container.lookupPolicy = .singleton(container) 78 | 79 | let instance = ToInjectStruct() 80 | waitForDeathTrap(description: "No Resolve Policy (struct)") { 81 | _ = instance.property 82 | } 83 | } 84 | 85 | func testFromEnclosedObjectPolicyFail() { 86 | let container = Container() 87 | container.register { [self] () -> String in testString } 88 | Container.lookupPolicy = .enclosingType() 89 | 90 | let instance = ToInjectClass() 91 | waitForDeathTrap(description: "From enclosed object policy fail") { 92 | _ = instance.property 93 | } 94 | } 95 | 96 | func testFromEnclosedObjectPolicyWithContainer() { 97 | let container = Container() 98 | container.register { [self] () -> String in testString } 99 | Container.lookupPolicy = .enclosingType() 100 | 101 | let instance = ToInjectClassContainerable(container: container) 102 | XCTAssertTrue(instance.property == testString) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/InjectedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 27 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | private let testStringValue: String = "Yes Service!" 14 | private let testIntValue: Int = 239 15 | 16 | private protocol MyService: AnyObject { 17 | var testValue: String { get } 18 | } 19 | 20 | private class MyServiceImplementation: MyService { 21 | var testValue: String = testStringValue 22 | } 23 | 24 | private class MyController { 25 | @Injected 26 | var myService: MyService 27 | @Injected 28 | var myStringInitializingWithParameter: String 29 | } 30 | 31 | private class MyControllerWrongInjection { 32 | @Injected 33 | var myProperty: Int 34 | } 35 | 36 | private class MyControllerWithOptionals { 37 | @Injected 38 | var myOptionalService: MyService? 39 | @Injected 40 | var myOptionalString: String? 41 | } 42 | 43 | private class MyControllerWithForcedOptionals { 44 | @Injected 45 | var myForceUnwrappedOptionalService: MyService! 46 | } 47 | 48 | private struct MyStruct { 49 | @Injected 50 | var myProperty: String 51 | } 52 | 53 | private let globalContainer = Container() 54 | 55 | private struct MyStructEager { 56 | @Injected(.resolvingOnInit(from: globalContainer)) 57 | var myProperty: String 58 | } 59 | 60 | class InjectedTests: BaseTestCase { 61 | override class func setUp() { 62 | super.setUp() 63 | globalContainer.register { () -> String in testStringValue } 64 | } 65 | 66 | override class func tearDown() { 67 | super.tearDown() 68 | globalContainer.cleanup() 69 | } 70 | 71 | override func setUp() { 72 | let container = Container() 73 | container.register { (_) -> String in testStringValue } 74 | container.register { () -> MyService in MyServiceImplementation() } 75 | Container.lookupPolicy = .singleton(container) 76 | addTeardownBlock { Container.lookupPolicy = nil } 77 | } 78 | 79 | func testSimpleInjected() { 80 | let testObject = MyController() 81 | 82 | XCTAssertEqual(testObject.myService.testValue, testStringValue) 83 | XCTAssertEqual(testObject.myStringInitializingWithParameter, testStringValue) 84 | } 85 | 86 | func testInjectionFail() { 87 | let testObject = MyControllerWrongInjection() 88 | 89 | waitForDeathTrap(description: "No value to inject") { 90 | _ = testObject.myProperty 91 | } 92 | } 93 | 94 | func testOptionalInjected() { 95 | let testObject = MyControllerWithOptionals() 96 | 97 | XCTAssertEqual(testObject.myOptionalService?.testValue, testStringValue) 98 | XCTAssertEqual(testObject.myOptionalString, testStringValue) 99 | } 100 | 101 | func testForcedOptionalInjected() { 102 | let testObject = MyControllerWithForcedOptionals() 103 | 104 | XCTAssertEqual(testObject.myForceUnwrappedOptionalService.testValue, testStringValue) 105 | } 106 | 107 | func testSingleInjected() { 108 | let testObject = MyController() 109 | 110 | let myService1 = testObject.myService 111 | let myService2 = testObject.myService 112 | 113 | XCTAssertTrue(myService1 === myService2) 114 | } 115 | 116 | func testStructInjection() { 117 | let testStruct = MyStruct() 118 | waitForDeathTrap(description: "No value to inject") { 119 | XCTAssertEqual(testStruct.myProperty, testStringValue) 120 | } 121 | } 122 | 123 | func testStructEagerInjection() { 124 | let testStruct = MyStructEager() 125 | XCTAssertEqual(testStruct.myProperty, testStringValue) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/PropertyWrappers/InjectedWeakly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InjectedWeakly 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 29 May 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | /// This wrapper can be used only with classes. You must capture injected object weakly (with [weak object]) in registration closure. 11 | @propertyWrapper 12 | public struct InjectedWeakly { 13 | public var wrappedValue: ValueType? { 14 | get { Macaroni.logger.die(message: "Injecting only works for class enclosing types") } 15 | // We need setter here so that KeyPaths in subscript were writable. 16 | set { Macaroni.logger.die(message: "Injecting only works for class enclosing types") } 17 | } 18 | 19 | // We need to strongly handle policy to be able to resolve lazily. 20 | private var findPolicyCapture: Injected.ContainerCapturePolicy = .onFirstUsage 21 | private var alternative: RegistrationAlternative? 22 | 23 | public init( 24 | _ initialization: Injected.InitializationKind = .capturingContainerOnInit(), 25 | alternative: RegistrationAlternative? = nil, 26 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 27 | ) { 28 | self.alternative = alternative 29 | switch initialization { 30 | case .resolvingOnInit(let container): 31 | guard let container = container ?? Container.lookupPolicy.container(for: Self.self, file: file, function: function, line: line) else { 32 | Macaroni.logger.die(message: "Can't find container for InjectedWeakly immediateResolve", file: file, function: function, line: line) 33 | } 34 | 35 | do { 36 | let storage: ValueType = try container.resolve() 37 | self.storage = storage as AnyObject 38 | Macaroni.logger.debug(message: "Injecting (eager from container): \(String(describing: ValueType.self))\(alternative.map { "/\($0.name)" } ?? "")", file: file, function: function, line: line) 39 | } catch { 40 | if container.isResolvable(ValueType.self) { 41 | Macaroni.logger.die(message: "Parametrized resolvers are not supported for greedy injection (\"\(String(describing: ValueType.self))\").", file: file, function: function, line: line) 42 | } else { 43 | Macaroni.logger.die(message: "Dependency \"\(String(describing: ValueType.self))\" does not have a resolver", file: file, function: function, line: line) 44 | } 45 | } 46 | case .capturingContainerOnInit(let container): 47 | findPolicyCapture = .onInitialization(container.map(SingletonContainer.init) ?? Container.lookupPolicy) 48 | case .lazily: 49 | findPolicyCapture = .onFirstUsage 50 | } 51 | } 52 | 53 | private weak var storage: AnyObject? 54 | private var isResolved: Bool = false 55 | 56 | public static subscript( 57 | _enclosingInstance instance: EnclosingType, 58 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 59 | storage storageKeyPath: ReferenceWritableKeyPath 60 | ) -> ValueType? { 61 | get { 62 | let enclosingValue = instance[keyPath: storageKeyPath] 63 | if enclosingValue.isResolved, let value = enclosingValue.storage as? ValueType { 64 | return value 65 | } else { 66 | let alternative = instance[keyPath: storageKeyPath].alternative 67 | guard let findPolicy = instance[keyPath: storageKeyPath].findPolicyCapture.policy else { 68 | Macaroni.logger.die(message: "Container selection policy (Macaroni.Container.policy) is not set") 69 | } 70 | 71 | if let value: ValueType? = findPolicy.resolve(for: instance, option: alternative?.name) { 72 | instance[keyPath: storageKeyPath].isResolved = true 73 | instance[keyPath: storageKeyPath].storage = value as AnyObject 74 | return value 75 | } else { 76 | instance[keyPath: storageKeyPath].isResolved = true 77 | instance[keyPath: storageKeyPath].storage = nil 78 | return nil 79 | } 80 | } 81 | } 82 | set { /* compiler needs this. We do not. */ } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/MacaroniTests/Tests/AlternativeTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlternativeTests 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 13 August 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import XCTest 11 | import Macaroni 12 | 13 | extension RegistrationAlternative { 14 | static let one: RegistrationAlternative = "one" 15 | static let two: RegistrationAlternative = .init("two") 16 | } 17 | 18 | class AlternativeTests: BaseTestCase { 19 | private var container: Container! 20 | 21 | func testSimpleAlternatives() throws { 22 | let objectDefault = TestInjectedClassDefault(property: nil) 23 | let objectOne = TestInjectedClassOne(property: nil) 24 | let objectTwo = TestInjectedClassTwo(property: nil) 25 | 26 | container = Container() 27 | container.register { () -> TestInjectedProtocol in objectDefault } 28 | container.register(alternative: .one) { () -> TestInjectedProtocol in objectOne } 29 | container.register(alternative: .two) { () -> TestInjectedProtocol in objectTwo } 30 | 31 | let valueDefault: TestInjectedProtocol? = try container.resolve() 32 | let valueOne: TestInjectedProtocol? = try container.resolve(alternative: .one) 33 | let valueTwo: TestInjectedProtocol? = try container.resolve(alternative: .two) 34 | 35 | XCTAssertTrue(objectDefault === valueDefault) 36 | XCTAssertTrue(objectOne === valueOne) 37 | XCTAssertTrue(objectTwo === valueTwo) 38 | } 39 | 40 | func testAlternativesInjection() throws { 41 | class Test { 42 | @Injected 43 | var valueDefault: TestInjectedProtocol 44 | @Injected(alternative: .one) 45 | var valueOne: TestInjectedProtocol 46 | } 47 | 48 | let objectDefault = TestInjectedClassDefault(property: nil) 49 | let objectOne = TestInjectedClassOne(property: nil) 50 | 51 | container = Container() 52 | Container.lookupPolicy = .singleton(container) 53 | addTeardownBlock { Container.lookupPolicy = nil } 54 | container.register { () -> TestInjectedProtocol in objectDefault } 55 | container.register(alternative: .one) { () -> TestInjectedProtocol in objectOne } 56 | 57 | let value = Test() 58 | 59 | XCTAssertTrue(objectDefault === value.valueDefault) 60 | XCTAssertTrue(objectOne === value.valueOne) 61 | } 62 | 63 | func testOnlyAlternative() throws { 64 | let objectOne = TestInjectedClassOne(property: nil) 65 | 66 | container = Container() 67 | container.register(alternative: .one) { () -> TestInjectedProtocol in objectOne } 68 | 69 | let valueDefault: TestInjectedProtocol? 70 | let valueOne: TestInjectedProtocol? = try container.resolve(alternative: .one) 71 | let valueTwo: TestInjectedProtocol? 72 | do { 73 | valueDefault = try container.resolve() 74 | } catch MacaroniError.noResolver { 75 | valueDefault = nil 76 | } 77 | do { 78 | valueTwo = try container.resolve(alternative: .two) 79 | } catch MacaroniError.noResolver { 80 | valueTwo = nil 81 | } 82 | 83 | XCTAssertTrue(valueDefault == nil) 84 | XCTAssertTrue(objectOne === valueOne) 85 | XCTAssertTrue(valueTwo == nil) 86 | } 87 | 88 | func testAlternativeWithParameter() throws { 89 | container = Container() 90 | container.register(alternative: .one) { 91 | parameter -> TestInjectedProtocol in TestInjectedClassOne(property: "\(parameter)") 92 | } 93 | 94 | let valueOne: TestInjectedProtocol? 95 | do { 96 | valueOne = try container.resolve(alternative: .one) 97 | } catch MacaroniError.noResolver { 98 | valueOne = nil 99 | } 100 | let valueOneWithParameter: TestInjectedProtocol? = try container.resolve(parameter: "Parameter", alternative: .one) 101 | 102 | XCTAssertTrue(valueOne == nil) 103 | XCTAssertTrue(valueOneWithParameter != nil) 104 | XCTAssertTrue(valueOneWithParameter?.property == "Parameter") 105 | } 106 | } 107 | 108 | private protocol TestInjectedProtocol: AnyObject { 109 | var property: String? { get set } 110 | } 111 | 112 | private class TestInjectedClassDefault: TestInjectedProtocol { 113 | var property: String? 114 | 115 | init(property: String?) { 116 | self.property = property 117 | } 118 | } 119 | 120 | private class TestInjectedClassOne: TestInjectedProtocol { 121 | var property: String? 122 | 123 | init(property: String?) { 124 | self.property = property 125 | } 126 | } 127 | 128 | private class TestInjectedClassTwo: TestInjectedProtocol { 129 | var property: String? 130 | 131 | init(property: String?) { 132 | self.property = property 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Macaroni 2 | It's a Swift Dependency Injection Framework that is called “Macaroni”. 3 | Cut [Spaghetti Code](https://en.wikipedia.org/wiki/Spaghetti_code) into pieces! :–) 4 | 5 | #### Main reason to exist 6 | 7 | When I start my projects, I need some kind of DI. 8 | It's obvious that [property wrappers](https://github.com/apple/swift-evolution/blob/master/proposals/0258-property-wrappers.md) 9 | can be used for DI framework. Here it is. 10 | 11 | Macaroni uses a hack from this article https://www.swiftbysundell.com/articles/accessing-a-swift-property-wrappers-enclosing-instance/ 12 | to be able to access `self` of the enclosing object. There is a limitation because of that: `@Injected` can be used _only in reference types_, 13 | because properties are being lazy initialized when accessed first time, thus changing the container 14 | (which is problematic to do with value types). 15 | 16 | #### Migration 17 | 18 | Please look at [UPDATE.md](UPDATES.md) to find out about migrations. 19 | 20 | ## Installation 21 | 22 | Please use [Swift Package Manager](https://swift.org/package-manager/). 23 | Repository address: `git@github.com:bealex/Macaroni.git` or `https://github.com/bealex/Macaroni.git`. 24 | Name of the package is `Macaroni`. 25 | 26 | ### Current version 27 | 28 | Current version is v4.x 29 | 30 | ## 30-second tutorial 31 | 32 | ```swift 33 | // Create the container. 34 | let container = Container() 35 | // Set it as a singleton for the simplest service-locator style resolution. 36 | Container.lookupPolicy = .singleton(container) 37 | 38 | // Add service implementations into the container. 39 | let myService = MyServiceImplementation() 40 | container.register { () -> MyService in myService } 41 | 42 | // Use it in code. 43 | let myService: MyService = container.resolve() 44 | 45 | // Or use it with property wrapper. 46 | class MyClass { 47 | @Injected 48 | var service: MyService 49 | } 50 | ``` 51 | 52 | ## Example 53 | 54 | First let's import Macaroni and prepare our protocol and implementation that we want to inject. 55 | 56 | ```swift 57 | import Macaroni 58 | 59 | protocol MyService {} 60 | class MyServiceImplementation: MyService {} 61 | ``` 62 | 63 | Macaroni should know where container is placed, to get objects for injection. You can think of _container_ as a box 64 | that holds all the objects. The knowledge of where container is placed is defined by `Container.lookupPolicy`. 65 | Let's use simple [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) policy, that uses 66 | a `singleton` object to hold all the objects that can be injected. 67 | 68 | ```swift 69 | let container = Container() 70 | Container.lookupPolicy = .singleton(container) 71 | ``` 72 | 73 | To register something inside a container, we register a _resolver_ there. Resolver is a closure that 74 | returns instance of a specific type. It can return same instance all the time, can create it each time it is accessed. You choose. 75 | For now let's register the resolver, that returns same instance every time it is used. 76 | 77 | ```swift 78 | let myService = MyServiceImplementation() 79 | container.register { myService } 80 | ``` 81 | 82 | And then we can inject this value like this: 83 | 84 | ```swift 85 | class MyClass { 86 | @Injected 87 | var myService: MyServiceImplementation 88 | } 89 | ``` 90 | 91 | Usually we need to be able to use it with the protocol like this: `var myService: MyService`, not with the implementation 92 | type (`var myService: MyServiceImplementation`). For that we need to tell `Container`, that if it is being asked of `MyService`, 93 | it should inject this specific object. It can be done using one of two options: 94 | 95 | ```swift 96 | // 1. 97 | // Now myService is of type `MyService` and registration will be 98 | // typed as `() -> MyService` instead of `() -> MyServiceImplementation` 99 | let myService: MyService /* <- Magic happens here */ = MyServiceImplementation() 100 | container.register { myService } 101 | 102 | // 2. 103 | // or like this (I prefer this option): 104 | let myService = MyServiceImplementation() 105 | container.register { () -> MyService /* <- Magic happens here */ in myService } 106 | ``` 107 | 108 | > Please note that injection is happening lazily, not during `MyController` initialization but when `myService` is first accessed. 109 | 110 | In the code above, implementation is being created right away. If you want to lazily create objects that 111 | should be injected, you can use a wrapper like this: 112 | 113 | ```swift 114 | class LazilyInitialized { 115 | lazy var value: Type = { resolver() }() 116 | 117 | private let resolver: () -> Type 118 | 119 | init(resolver: @escaping () -> Type) { 120 | self.resolver = resolver 121 | } 122 | } 123 | 124 | let willBeInstantiatedOnFirstAccess = LazilyInitialized { MyServiceImplementation() } 125 | container.register { () -> MyService in willBeInstantiatedOnFirstAccess.value } 126 | ``` 127 | 128 | ## Locking the container 129 | 130 | After you finished registering all the dependencies into the container, you can `lock` it. In the lock state the container can't register 131 | dependencies, but at the same time, dependency access is faster (by not going via the queue). 132 | 133 | ## `Injected` options 134 | 135 | #### Class property injection 136 | 137 | ```swift 138 | // 1. 139 | // Lazy injection from the container that is captured on initialization, determined by `Container.policy`: 140 | @Injected 141 | var property: Type 142 | 143 | // 2. 144 | // Lazy injection from the container that is captured on initialization (you specify it): 145 | @Injected(.capturingContainerOnInit(from: container)) 146 | var property: Type 147 | 148 | // 3. 149 | // Lazy capturing of the container and resolving: 150 | @Injected(.lazily) 151 | var property: Type 152 | 153 | // 4. 154 | // Eager resolving, during the initialization, from the container from `Container.policy`: 155 | @Injected(.resolvingOnInit()) 156 | var property: Type 157 | 158 | // 5. 159 | // Eager resolving, during the initialization, from the specified container: 160 | @Injected(.resolvingOnInit(from: container)) 161 | var property: Type 162 | ``` 163 | 164 | > Please note that parametrized injection works only when object is being resolved lazily. 165 | > Eager injection can only resolve objects by type (and alternative if it is provided). 166 | > 167 | > Also lazy injection can't be used in `structs`, because it needs to modify object after the resolve. 168 | 169 | #### Resolving several objects with the same type 170 | 171 | ```swift 172 | // - create alternative identifier. Strings must be different for different types. 173 | extension RegistrationAlternative { 174 | static let another: RegistrationAlternative = "another" 175 | } 176 | // - registration 177 | container.register(alternative: .another) { () -> MyService in anotherInstance } 178 | // - injection 179 | @Injected(alternative: .another) 180 | var myServiceAlternative: MyService 181 | ``` 182 | 183 | #### Function parameter injection 184 | 185 | Starting from Swift 5.5 we can use property wrappers for function parameters too. Here is the function declaration: 186 | 187 | ```swift 188 | func foo(@Injected service: MyService) { /* Use service here */ } 189 | ``` 190 | 191 | And its call using default instance: 192 | 193 | ```swift 194 | foo($service: container.resolved()) 195 | ``` 196 | 197 | Or alternative instance 198 | 199 | ```swift 200 | foo($service: container.resolved(alternative: .another)) 201 | ``` 202 | 203 | #### Using information about enclosing object (parametrized injection) 204 | 205 | If you need to use object that contains the injected property, you can get from inside registration closure like this: 206 | 207 | ```swift 208 | container.register { enclosing -> String in String(describing: enclosing) } 209 | ``` 210 | 211 | > This resolver will be available for lazy injections only. 212 | 213 | ## Weak injection 214 | 215 | When using property wrappers, you can't use `weak` (or `lazy` or `unowned`). If you need that, you can use `@InjecteadWeakly`. 216 | 217 | ```swift 218 | @InjectedWeakly 219 | var myService: MyService? 220 | ``` 221 | 222 | ## Container lookup Policies 223 | 224 | There are three policies of container selection for properties of specific enclosing object: 225 | - service locator style. It is called `singleton`, and can be set up like this: `Container.lookupPolicy = .singleton(myContainer)`. 226 | - enclosing object based. This policy implies, that every enclosing type implements `Containerable` 227 | protocol that defines `Container` for the object. You can set it up with `.enclosingType(default:)`. 228 | - custom. If you want to control container finding yourself and no other option suits you, you can implement `ContainerLookupPolicy` yourself. 229 | 230 | ## Per Module Injection 231 | 232 | If your application uses several modules and each module needs its own `Container`, you can use this option: 233 | 234 | Write this somewhere in the common module: 235 | 236 | ```swift 237 | protocol ModuleDI: Containerable {} 238 | Container.lookupPolicy = EnclosingTypeContainer() 239 | ``` 240 | 241 | And this in each module: 242 | 243 | ```swift 244 | private var moduleContainer: Container! 245 | extension ModuleDI { 246 | var container: Container! { moduleContainer } // now each module does have its own container 247 | } 248 | 249 | class MyClass: ModuleDI { 250 | @Inject var service: MyService // will be injected from the `moduleContainer` 251 | } 252 | ``` 253 | 254 | ## Multithreading support 255 | 256 | Macaroni does not do anything about multithreading. Please handle it yourself if needed. 257 | 258 | ### Logging 259 | 260 | By default, Macaroni will print simple events (container creation, resolver registering, injections) to the console. If you don't need that 261 | (or need to alter logs in some way), please set `Macaroni.Logger` to your implementation of `MacaroniLogger`: 262 | 263 | ```swift 264 | class MyMacaroniLogger: MacaroniLogger { 265 | func log(/* Parameters */) { /* Logging code */ } 266 | func die() -> Never { /* Log and crash */ } 267 | } 268 | 269 | Macaroni.logger = MyMacaroniLogger() 270 | ``` 271 | 272 | Use this code to disable logging completely: 273 | 274 | ```swift 275 | Macaroni.logger = DisabledMacaroniLogger() 276 | ``` 277 | 278 | ## License 279 | 280 | License: MIT, https://github.com/bealex/Macaroni/blob/main/LICENSE 281 | -------------------------------------------------------------------------------- /Sources/PropertyWrappers/Injected.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Injected 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 20 March 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | private extension Container { 11 | static let alwaysFailRootContainer: Container = Container(name: "_alwaysFailContainersDeriveFromThis") 12 | } 13 | 14 | /// This property wrapper helps to use objects that were previously registered in the `Container`. It does two things: 15 | /// - searches for the container. Usually it is using `Container.lookupPolicy` for that. 16 | /// - asks the found container to resolve object based on the type `ValueType`. 17 | /// 18 | /// Both actions can happen while enclosing object is being initialized or when the property is being accessed for the first time. 19 | /// Initialization parameter determines when exactly. Only three options are available: 20 | /// - `@Injected(.capturingContainerOnInit(Container?))` is the default option. It tries to capture container 21 | /// during the initialization, but resolves property value later, when it is being accessed for the first time. 22 | /// - `@Injected(.lazily)` means that both container lookup and property resolve are happening when property 23 | /// is being accessed for the first time 24 | /// - `@Injected(.resolvingOnInit(from: Container?))` tries to do everything right during the initialization. 25 | /// 26 | /// You can also use `@Injected` in functions: `func test(@Injected value: String)`, 27 | /// using it like this: `test($value: container.resolved())`. 28 | @propertyWrapper 29 | public struct Injected { 30 | enum ContainerCapturePolicy { 31 | case onInitialization(ContainerLookupPolicy) 32 | case onFirstUsage 33 | 34 | var policy: ContainerLookupPolicy? { 35 | switch self { 36 | case .onInitialization(let policy): return policy 37 | case .onFirstUsage: return Container.lookupPolicy 38 | } 39 | } 40 | } 41 | 42 | public enum ResolveFrom { 43 | case object(Any, alternative: String? = nil) 44 | case container(Container, alternative: String? = nil) 45 | case alreadyResolved(Any) 46 | } 47 | 48 | public enum InitializationKind { 49 | /// Will find container and resolve the value on first access. Useful for "late initialization". 50 | case lazily 51 | /// Will capture container when initializing, resolve value on first access. If container is specified, it is used for resolve. 52 | case capturingContainerOnInit(Container? = nil) 53 | /// Will capture container when initializing and resolve the value on initializing. If container is specified, it is used for resolve. 54 | case resolvingOnInit(from: Container? = nil) 55 | } 56 | 57 | public var wrappedValue: ValueType { 58 | get { 59 | if let value = storage { 60 | return value 61 | } else { 62 | Macaroni.logger.die(message: "Injected value is nil") 63 | } 64 | } 65 | set { /* compiler needs this. We do not. */ } 66 | } 67 | public private(set) var projectedValue: ResolveFrom 68 | 69 | // This works only for lazy initialization. 70 | private var alternative: RegistrationAlternative? 71 | private var storage: ValueType? 72 | 73 | // We need to strongly handle policy to be able to resolve lazily. 74 | private var capturePolicy: ContainerCapturePolicy = .onFirstUsage 75 | 76 | // Is used for class property injection. Lazy initialization if container is not present, eager otherwise. 77 | public init( 78 | _ initialization: InitializationKind = .capturingContainerOnInit(), 79 | alternative: RegistrationAlternative? = nil, 80 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 81 | ) { 82 | self.alternative = alternative 83 | switch initialization { 84 | case .resolvingOnInit(let container): 85 | projectedValue = .container(Container.alwaysFailRootContainer) 86 | guard let container = container ?? Container.lookupPolicy?.container(for: Self.self, file: file, function: function, line: line) else { 87 | Macaroni.logger.die(message: "Can't find container for Injected immediateResolve", file: file, function: function, line: line) 88 | } 89 | 90 | projectedValue = .container(container, alternative: alternative?.name) 91 | resolveRightNowIfPossible(file: file, function: function, line: line) 92 | case .capturingContainerOnInit(let container): 93 | if let container = container { 94 | projectedValue = .container(Container.alwaysFailRootContainer) 95 | capturePolicy = .onInitialization(.singleton(container)) 96 | } else if let policy = Container.lookupPolicy { 97 | projectedValue = .container(Container.alwaysFailRootContainer) 98 | capturePolicy = .onInitialization(policy) 99 | } else { 100 | Macaroni.logger.die(message: "Container.lookupPolicy is not initialized", file: file, function: function, line: line) 101 | } 102 | case .lazily: 103 | projectedValue = .container(Container.alwaysFailRootContainer) 104 | capturePolicy = .onFirstUsage 105 | Macaroni.logger.debug(message: "Injecting (lazy): \(String(reflecting: ValueType.self))\(alternative.map { "/\($0.name)" } ?? "")", file: file, function: function, line: line) 106 | } 107 | } 108 | 109 | public init(wrappedValue: ValueType, file: StaticString = #fileID, function: String = #function, line: UInt = #line) { 110 | storage = wrappedValue 111 | projectedValue = .container(Container.alwaysFailRootContainer) 112 | Macaroni.logger.debug(message: "Injecting (eager, value): \(String(reflecting: ValueType.self))", file: file, function: function, line: line) 113 | } 114 | 115 | // Is used for function parameter injection. 116 | public init(projectedValue: ResolveFrom, file: StaticString = #fileID, function: String = #function, line: UInt = #line) { 117 | self.projectedValue = projectedValue 118 | resolveRightNowIfPossible(file: file, function: function, line: line) 119 | Macaroni.logger.debug(message: "Injecting (eager, projected): \(String(reflecting: ValueType.self))", file: file, function: function, line: line) 120 | } 121 | 122 | private mutating func resolveRightNowIfPossible(file: StaticString = #fileID, function: String = #function, line: UInt = #line) { 123 | switch projectedValue { 124 | case .alreadyResolved(let value): 125 | if let value = value as? ValueType { 126 | storage = value 127 | Macaroni.logger.debug( 128 | message: "Injecting (eager from container): \(String(reflecting: ValueType.self))\(alternative.map { "/\($0.name)" } ?? "")", 129 | file: file, function: function, line: line 130 | ) 131 | } else { 132 | Macaroni.logger.die( 133 | message: "Injected value is not of type \"\(String(reflecting: ValueType.self))\": (\(value))", 134 | file: file, function: function, line: line 135 | ) 136 | } 137 | case .object(let enclosedObject, let alternative): 138 | let resolved: ValueType = Injected.resolve(for: enclosedObject, alternative: alternative, findPolicy: Container.lookupPolicy) 139 | storage = resolved 140 | case .container(let container, let alternative): 141 | do { 142 | let resolved: ValueType = try container.resolve(alternative: alternative) 143 | storage = resolved 144 | } catch { 145 | Macaroni.logger.die( 146 | message: "Can't find resolver for \"\(String(reflecting: ValueType.self))\" in container (\(container.name))", 147 | file: file, function: function, line: line 148 | ) 149 | } 150 | } 151 | } 152 | 153 | /// Is called when injected into a class property and being accessed. 154 | public static subscript( 155 | _enclosingInstance instance: EnclosingType, 156 | wrapped wrappedKeyPath: ReferenceWritableKeyPath, 157 | storage storageKeyPath: ReferenceWritableKeyPath 158 | ) -> ValueType { 159 | get { 160 | let enclosingValue = instance[keyPath: storageKeyPath] 161 | if let value = enclosingValue.storage { 162 | return value 163 | } else { 164 | let alternative = instance[keyPath: storageKeyPath].alternative?.name 165 | let findPolicy = instance[keyPath: storageKeyPath].capturePolicy.policy 166 | let value = resolve(for: instance, alternative: alternative, findPolicy: findPolicy) 167 | instance[keyPath: storageKeyPath].storage = value 168 | return value 169 | } 170 | } 171 | set { /* compiler needs this. We do not. */ } 172 | } 173 | 174 | private static func resolve( 175 | for enclosingInstance: Any, alternative: String? = nil, findPolicy: ContainerLookupPolicy?, 176 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 177 | ) -> ValueType { 178 | Macaroni.logger.debug( 179 | message: "Resolving [\(String(reflecting: ValueType.self))\(alternative.map { " / \($0)" } ?? "")] in the \(String(reflecting: type(of: enclosingInstance)))", 180 | file: file, function: function, line: line 181 | ) 182 | guard let findPolicy else { 183 | Macaroni.logger.die( 184 | message: "Can't find container for [\(String(reflecting: ValueType.self))\(alternative.map { " / \($0)" } ?? "")] to \(String(reflecting: type(of: enclosingInstance)))", 185 | file: file, function: function, line: line 186 | ) 187 | } 188 | 189 | return findPolicy.resolve(for: enclosingInstance, option: alternative) 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /Sources/Container.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DependencyInjection 3 | // Macaroni 4 | // 5 | // Created by Alex Babaev on 30 May 2021. 6 | // Copyright © 2021 Alex Babaev. All rights reserved. 7 | // License: MIT License, https://github.com/bealex/Macaroni/blob/main/LICENSE 8 | // 9 | 10 | import Foundation 11 | 12 | public enum MacaroniError: Error { 13 | /// No resolvers was found for the type. 14 | case noResolver 15 | } 16 | 17 | /// Dependency injection container, that can resolve registered objects. Registration is done on type-by-type basis, 18 | /// so that only one object can be resolved based on its type. 19 | /// 20 | /// If you need to resolve several objects for one type, you need to use `alternatives`. 21 | /// 22 | /// Usually if you register `Type`, you can resolve `Type?` and `Type!` as well. 23 | /// 24 | /// Containers can have a hierarchy. If type is not found in current container, its resolving is delegated to the parent. 25 | /// 26 | /// Containers have two resolver types. One does not know about anything but the type it is resolving. Another knows 27 | /// the type of type that contains property that is being resolved. 28 | /// 29 | /// There is a `@Injected` property wrapper that helps to inject objects into classes (mostly). 30 | public final class Container { 31 | let name: String 32 | let parent: Container? 33 | 34 | private static var counter: Int = 1 35 | private let queue: DispatchQueue 36 | 37 | /// you can lock container in case it will not be updated anymore. 38 | /// This should speed up container access, but remove ability to add new resolvers. 39 | private var isLocked: Bool = false 40 | 41 | public init( 42 | parent: Container? = nil, 43 | name: String? = nil, 44 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 45 | ) { 46 | self.parent = parent 47 | self.name = name ?? "UnnamedContainer.\(Container.counter)" 48 | queue = DispatchQueue(label: "container.\(self.name)", attributes: [ .concurrent ]) 49 | 50 | Container.counter += 1 51 | Macaroni.logger.debug( 52 | message: "\(self.name)\(self.parent == nil ? "" : " (parent: \(parent?.name ?? "???"))") created", 53 | file: file, function: function, line: line 54 | ) 55 | } 56 | 57 | public func lock() { 58 | queue.sync { 59 | isLocked = true 60 | } 61 | } 62 | 63 | public func unlock() { 64 | queue.sync { 65 | isLocked = false 66 | } 67 | } 68 | 69 | /// Resolvers that can create object by type. 70 | private var typeResolvers: [ObjectIdentifier: [String: () -> Any]] = [:] 71 | /// Resolvers that can create object, based on type and some arbitrary parameter. 72 | /// What is this parameter, depends on the usage. 73 | private var typeParametrizedResolvers: [ObjectIdentifier: [String: (_ parameter: Any) -> Any]] = [:] 74 | 75 | private func keys(_ type: D.Type, alternative: String?) -> (ObjectIdentifier, String?) { 76 | (ObjectIdentifier(type), alternative) 77 | 78 | // if let alternative { 79 | // "\(String(reflecting: type))\(alternative)" 80 | // } else { 81 | // String(reflecting: type) 82 | // } 83 | } 84 | 85 | private let defaultAlternativeKey: String = "__default" 86 | 87 | private func resolver(_ objectId: ObjectIdentifier, alternative: String?) -> (() -> Any)? { 88 | if let alternative { 89 | return typeResolvers[objectId]?[alternative] 90 | } else { 91 | return typeResolvers[objectId]?[defaultAlternativeKey] 92 | } 93 | } 94 | 95 | private func parametrizedResolver(_ objectId: ObjectIdentifier, alternative: String?) -> ((_ parameter: Any) -> Any)? { 96 | if let alternative { 97 | return typeParametrizedResolvers[objectId]?[alternative] 98 | } else { 99 | return typeParametrizedResolvers[objectId]?[defaultAlternativeKey] 100 | } 101 | } 102 | 103 | /// Returns true, if type is resolvable with the container or its parent. 104 | public func isResolvable(_ type: D.Type, alternative: String? = nil) -> Bool { 105 | queue.sync { 106 | let objectId = ObjectIdentifier(type) 107 | return parametrizedResolver(ObjectIdentifier(type), alternative: alternative) != nil || 108 | resolver(ObjectIdentifier(type), alternative: alternative) != nil || (parent?.isResolvable(type) ?? false) 109 | } 110 | } 111 | 112 | /// Registers resolving closure for type `D`. 113 | public func register( 114 | alternative: String? = nil, 115 | file: StaticString = #fileID, function: String = #function, line: UInt = #line, 116 | _ resolver: @escaping () -> D 117 | ) { 118 | guard !isLocked else { return assertionFailure("Container is locked") } 119 | 120 | let alternativeKey = alternative ?? defaultAlternativeKey 121 | queue.async(flags: .barrier) { [self] in 122 | let nonOptionalObjectId = ObjectIdentifier(D.self) 123 | let optionalObjectId = ObjectIdentifier(Optional.self) 124 | typeResolvers[nonOptionalObjectId, default: [:]][alternativeKey] = resolver 125 | 126 | if self.resolver(optionalObjectId, alternative: alternativeKey) == nil && parametrizedResolver(optionalObjectId, alternative: alternativeKey) == nil { 127 | typeResolvers[optionalObjectId, default: [:]][alternativeKey] = resolver 128 | Macaroni.logger.debug( 129 | message: "\(name) is registering resolver for \(String(describing: D.self)) and its Optional\(alternative.map { " / \($0)" } ?? "")", 130 | file: file, function: function, line: line 131 | ) 132 | } else { 133 | Macaroni.logger.debug( 134 | message: "\(name) is registering resolver for \(String(describing: D.self))\(alternative.map { " / \($0)" } ?? "")", 135 | file: file, function: function, line: line 136 | ) 137 | } 138 | } 139 | } 140 | 141 | /// Registers resolving closure with parameter for type `D`. `@Injected` annotation sends enclosing object as a parameter. 142 | public func register( 143 | alternative: String? = nil, 144 | file: StaticString = #fileID, function: String = #function, line: UInt = #line, 145 | _ resolver: @escaping (_ parameter: Any) -> D 146 | ) { 147 | guard !isLocked else { return assertionFailure("Container is locked") } 148 | 149 | let alternativeKey = alternative ?? defaultAlternativeKey 150 | queue.async(flags: .barrier) { [self] in 151 | let nonOptionalObjectId = ObjectIdentifier(D.self) 152 | let optionalObjectId = ObjectIdentifier(Optional.self) 153 | typeParametrizedResolvers[nonOptionalObjectId, default: [:]][alternativeKey] = resolver 154 | 155 | if self.resolver(optionalObjectId, alternative: alternativeKey) == nil && parametrizedResolver(optionalObjectId, alternative: alternativeKey) == nil { 156 | typeParametrizedResolvers[optionalObjectId, default: [:]][alternativeKey] = resolver 157 | Macaroni.logger.debug( 158 | message: "\(name) is registering parametrized resolver for \(String(describing: D.self)) and its Optional\(alternative.map { " / \($0)" } ?? "")", 159 | file: file, function: function, line: line 160 | ) 161 | } else { 162 | Macaroni.logger.debug( 163 | message: "\(name) is registering parametrized resolver for \(String(describing: D.self))\(alternative.map { " / \($0)" } ?? "")", 164 | file: file, function: function, line: line 165 | ) 166 | } 167 | } 168 | } 169 | 170 | /// Returns instance of type `D`, if it is registered. 171 | public func resolve( 172 | alternative: String? = nil, 173 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 174 | ) throws -> D { 175 | let objectId = ObjectIdentifier(D.self) 176 | let resolver = isLocked 177 | ? resolver(objectId, alternative: alternative) 178 | : queue.sync { self.resolver(objectId, alternative: alternative) } 179 | if let resolver { 180 | return resolver() as! D 181 | } else if let parent = self.parent { 182 | return try parent.resolve(alternative: alternative) 183 | } else { 184 | throw MacaroniError.noResolver 185 | } 186 | } 187 | 188 | /// Returns instance of type `D`, if it is registered. Sends `parameter` to the resolver. 189 | /// For example, parameter can be a class name that encloses value that needs to be injected. 190 | public func resolve( 191 | parameter: Any, 192 | alternative: String? = nil, 193 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 194 | ) throws -> D { 195 | let objectId = ObjectIdentifier(D.self) 196 | let resolver = isLocked 197 | ? parametrizedResolver(objectId, alternative: alternative) 198 | : queue.sync { self.parametrizedResolver(objectId, alternative: alternative) } 199 | if let resolver { 200 | return resolver(parameter) as! D 201 | } else if let parent = self.parent { 202 | return try parent.resolve(parameter: parameter, alternative: alternative, file: file, function: function, line: line) 203 | } else { 204 | throw MacaroniError.noResolver 205 | } 206 | } 207 | 208 | /// Removes all resolvers. 209 | public func cleanup(file: StaticString = #fileID, function: String = #function, line: UInt = #line) { 210 | queue.async(flags: .barrier) { [self] in 211 | typeResolvers = [:] 212 | typeParametrizedResolvers = [:] 213 | Macaroni.logger.debug(message: "\(name) cleared", file: file, function: function, line: line) 214 | } 215 | } 216 | } 217 | 218 | public extension Container { 219 | func register( 220 | file: StaticString = #fileID, function: String = #function, line: UInt = #line, 221 | _ resolver: @escaping () -> D 222 | ) { 223 | register(alternative: Optional.none, file: file, function: function, line: line, resolver) 224 | } 225 | 226 | func register( 227 | alternative: RegistrationAlternative? = nil, 228 | file: StaticString = #fileID, function: String = #function, line: UInt = #line, 229 | _ resolver: @escaping () -> D 230 | ) { 231 | register(alternative: alternative?.name, file: file, function: function, line: line, resolver) 232 | } 233 | 234 | func register( 235 | file: StaticString = #fileID, function: String = #function, line: UInt = #line, 236 | _ resolver: @escaping (_ parameter: Any) -> D 237 | ) { 238 | register(alternative: Optional.none, file: file, function: function, line: line, resolver) 239 | } 240 | 241 | func register( 242 | alternative: RegistrationAlternative? = nil, 243 | file: StaticString = #fileID, function: String = #function, line: UInt = #line, 244 | _ resolver: @escaping (_ parameter: Any) -> D 245 | ) { 246 | register(alternative: alternative?.name, file: file, function: function, line: line, resolver) 247 | } 248 | 249 | func resolve( 250 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 251 | ) throws -> D? { 252 | try resolve(alternative: Optional.none, file: file, function: function, line: line) 253 | } 254 | 255 | func resolve( 256 | alternative: RegistrationAlternative? = nil, 257 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 258 | ) throws -> D? { 259 | try resolve(alternative: alternative?.name, file: file, function: function, line: line) 260 | } 261 | 262 | func resolve( 263 | parameter: Any, 264 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 265 | ) throws -> D? { 266 | try resolve(parameter: parameter, alternative: Optional.none, file: file, function: function, line: line) 267 | } 268 | 269 | func resolve( 270 | parameter: Any, alternative: RegistrationAlternative? = nil, 271 | file: StaticString = #fileID, function: String = #function, line: UInt = #line 272 | ) throws -> D? { 273 | try resolve(parameter: parameter, alternative: alternative?.name, file: file, function: function, line: line) 274 | } 275 | } 276 | --------------------------------------------------------------------------------