diff --git a/.github/workflows/swift.yml b/.github/workflows/swift.yml index 393f978..f53456e 100644 --- a/.github/workflows/swift.yml +++ b/.github/workflows/swift.yml @@ -8,9 +8,14 @@ on: jobs: test: - runs-on: macos-11 + runs-on: macos-latest steps: + - name: Install Swift + uses: slashmo/install-swift@v0.4.0 + with: + version: "5.9" + - name: Get Sources uses: actions/checkout@v2 diff --git a/Package.swift b/Package.swift index ead3fa7..5500227 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.3 +// swift-tools-version:5.9 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription diff --git a/Sources/DIContainer/Container.swift b/Sources/DIContainer/Container.swift index 1ab5972..15c5f6b 100644 --- a/Sources/DIContainer/Container.swift +++ b/Sources/DIContainer/Container.swift @@ -1,51 +1,100 @@ -// -// Container.swift -// -// -// Created by Victor C Tavernari on 07/08/21. -// - -import Foundation - +/// `Container`: A singleton class to manage dependency injections. +/// +/// This class provides a shared instance to manage dependencies across the application. +/// It allows for registering and retrieving dependencies via a dictionary. public class Container: Injectable { + /// The shared instance of `Container`. + /// + /// Use this static property to access the same instance of `Container` throughout the application. public static var standard = Container() + /// A dictionary holding the dependencies. + /// + /// The dependencies are stored as key-value pairs where the key + /// is any hashable object and the value is the dependency. public var dependencies: [AnyHashable: Any] = [:] + /// Creates a new instance of `Container`. + /// + /// This initializer is public and required as per the `Injectable` protocol. required public init() {} } +/// A property wrapper for injecting dependencies. +/// +/// This struct wraps a property and injects a dependency into it. +/// If the dependency cannot be resolved and no default value is provided, +/// it will crash the application. @propertyWrapper public struct Injected { + /// Error types related to dependency injection. + enum Error: Swift.Error { + case couldNotResolveAndDefaultIsNil + } + + /// Returns the standard container used for resolving dependencies. public static func container() -> Injectable { Container.standard } private let identifier: InjectIdentifier private let container: Resolvable - public init(_ identifier: InjectIdentifier? = nil, container: Resolvable? = nil) { + private let `default`: Value? + + /// Creates a new `Injected` instance. + /// + /// - Parameters: + /// - identifier: The identifier used to resolve the dependency. Defaults to the type of `Value`. + /// - container: The container used for resolving the dependency. Defaults to `Container.standard`. + /// - default: An optional default value to use if the dependency cannot be resolved. + public init(_ identifier: InjectIdentifier? = nil, container: Resolvable? = nil, `default`: Value? = nil) { self.identifier = identifier ?? .by(type: Value.self) self.container = container ?? Self.container() + self.default = `default` } + /// The resolved value of the dependency. + /// + /// This property lazily resolves the dependency. + /// If the dependency cannot be resolved, it will use the provided default value. + /// If both fail, the application will crash. public lazy var wrappedValue: Value = { - do { - - return try container.resolve(identifier) - - } catch { fatalError( error.localizedDescription ) } + if let value = try? container.resolve(identifier) { + return value + } + + if let `default` { + return `default` + } + + fatalError("Could not resolve with \(identifier) and default is nil") }() } +/// A property wrapper for safely injecting dependencies. +/// +/// This struct wraps a property and injects an optional dependency into it. +/// If the dependency cannot be resolved, the property will be nil. @propertyWrapper public struct InjectedSafe { + /// Returns the standard container used for resolving dependencies. public static func container() -> Injectable { Container.standard } private let identifier: InjectIdentifier private let container: Resolvable + + /// Creates a new `InjectedSafe` instance. + /// + /// - Parameters: + /// - identifier: The identifier used to resolve the dependency. Defaults to the type of `Value`. + /// - container: The container used for resolving the dependency. Defaults to `Container.standard`. public init(_ identifier: InjectIdentifier? = nil, container: Resolvable? = nil) { self.identifier = identifier ?? .by(type: Value.self) self.container = container ?? Self.container() } + /// The optionally resolved value of the dependency. + /// + /// This property lazily tries to resolve the dependency. + /// If the dependency cannot be resolved, the property will be nil. public lazy var wrappedValue: Value? = try? container.resolve(identifier) } diff --git a/Sources/DIContainer/DIContainer.swift b/Sources/DIContainer/DIContainer.swift index 1ffa8ab..16be6c0 100644 --- a/Sources/DIContainer/DIContainer.swift +++ b/Sources/DIContainer/DIContainer.swift @@ -1,92 +1,139 @@ import Foundation +/// A protocol defining the ability to resolve dependencies. public protocol Resolvable { + /// Resolves a dependency based on an identifier. + /// + /// - Parameter identifier: The identifier for the dependency to be resolved. + /// - Throws: An error if the dependency cannot be resolved. + /// - Returns: The resolved dependency of the given type `Value`. func resolve(_ identifier: InjectIdentifier) throws -> Value } +/// An enumeration representing errors +/// that can occur during the resolution of dependencies. public enum ResolvableError: Error { + /// Indicates that a dependency could not be found. + /// + /// - Parameters: + /// - type: The type of the dependency that was not found. + /// - key: An optional key associated with the dependency. case dependencyNotFound(Any.Type?, String?) } +/// Extension to make `ResolvableError` conform +/// to `LocalizedError`, providing a localized description of the error. extension ResolvableError: LocalizedError { + /// A localized description of the error. public var errorDescription: String? { switch self { case let .dependencyNotFound(type, key): - var message = "Could not find dependency for " - if let type = type { message += "type: \(type) " } else if let key = key { message += "key: \(key)" } - return message } } } +/// A protocol representing an object that can inject dependencies. +/// +/// Conforming types can register, remove, and resolve dependencies. public protocol Injectable: Resolvable, AnyObject { + /// Initializes a new instance. init() + /// A dictionary to hold dependencies. var dependencies: [AnyHashable: Any] { get set } + /// Registers a dependency with an identifier. + /// + /// - Parameters: + /// - identifier: The identifier for the dependency. + /// - resolve: A closure that resolves the dependency. func register(_ identifier: InjectIdentifier, _ resolve: (Resolvable) throws -> Value) + /// Removes a dependency associated with an identifier. + /// + /// - Parameter identifier: The identifier for the dependency to be removed. func remove(_ identifier: InjectIdentifier) } +/// Default implementations for the `Injectable` protocol. public extension Injectable { - + + /// Registers a dependency. + /// + /// - Parameters: + /// - identifier: The identifier for the dependency. + /// - resolve: A closure that resolves the dependency. func register(_ identifier: InjectIdentifier, _ resolve: (Resolvable) throws -> Value) { - do { - - self.dependencies[identifier] = try resolve( self ) - + self.dependencies[identifier] = try resolve(self) } catch { - assertionFailure(error.localizedDescription) } } + /// Convenience method to register a dependency using type and optional key. + /// + /// - Parameters: + /// - type: The type of the dependency. + /// - key: An optional key for the dependency. + /// - resolve: A closure that resolves the dependency. func register(type: Value.Type? = nil, key: String? = nil, _ resolve: (Resolvable) throws -> Value) { - self.register(.by(type: type, key: key), resolve) } - + + /// Removes a dependency associated with an identifier. + /// + /// - Parameter identifier: The identifier for the dependency to be removed. func remove(_ identifier: InjectIdentifier) { - self.dependencies.removeValue(forKey: identifier) } + /// Convenience method to remove a dependency using type and optional key. + /// + /// - Parameters: + /// - type: The type of the dependency. + /// - key: An optional key for the dependency. func remove(type: Value.Type? = nil, key: String? = nil) { - let identifier = InjectIdentifier.by(type: type, key: key) self.dependencies.removeValue(forKey: identifier) } + /// Removes all dependencies from the container. func removeAllDependencies() { - self.dependencies.removeAll() } + /// Resolves a dependency based on an identifier. + /// + /// - Parameter identifier: The identifier for the dependency to be resolved. + /// - Throws: `ResolvableError.dependencyNotFound` if the dependency cannot be found. + /// - Returns: The resolved dependency of the given type `Value`. func resolve(_ identifier: InjectIdentifier) throws -> Value { - guard let dependency = dependencies[identifier] as? Value else { - throw ResolvableError.dependencyNotFound(identifier.type, identifier.key) } - return dependency } + /// Convenience method to resolve a dependency using type and optional key. + /// + /// - Parameters: + /// - type: The type of the dependency. + /// - key: An optional key for the dependency. + /// - Throws: `ResolvableError.dependencyNotFound` if the dependency cannot be found. + /// - Returns: The resolved dependency of the given type `Value`. func resolve(type: Value.Type? = nil, key: String? = nil) throws -> Value { - try self.resolve(.by(type: type, key: key)) } } diff --git a/Sources/DIContainer/InjectIdentifier.swift b/Sources/DIContainer/InjectIdentifier.swift index c6fe6a7..850e6c2 100644 --- a/Sources/DIContainer/InjectIdentifier.swift +++ b/Sources/DIContainer/InjectIdentifier.swift @@ -1,43 +1,59 @@ -// -// InjectIdentifier.swift -// -// -// Created by Victor C Tavernari on 07/08/21. -// - import Foundation +/// A structure used to uniquely identify dependencies for injection. public struct InjectIdentifier { + /// The type of the value to be injected. private(set) var type: Value.Type? = nil + + /// An optional key to further distinguish dependencies of the same type. private(set) var key: String? = nil + /// Private initializer to create an `InjectIdentifier` instance. + /// + /// - Parameters: + /// - type: The type of the value to be injected. + /// - key: An optional key to further distinguish dependencies. private init(type: Value.Type? = nil, key: String? = nil) { - self.type = type self.key = key } } +/// Extension to make `InjectIdentifier` conform to `Hashable`. extension InjectIdentifier: Hashable { - public static func == (lhs: InjectIdentifier, rhs: InjectIdentifier) -> Bool { lhs.hashValue == rhs.hashValue } + /// Determines equality between two `InjectIdentifier` instances. + /// + /// - Parameters: + /// - lhs: The left-hand side `InjectIdentifier` instance. + /// - rhs: The right-hand side `InjectIdentifier` instance. + /// - Returns: A Boolean value indicating whether the two instances are equal. + public static func == (lhs: InjectIdentifier, rhs: InjectIdentifier) -> Bool { + lhs.hashValue == rhs.hashValue + } + /// Hashes the essential components of this value by feeding them into the given hasher. + /// + /// - Parameter hasher: The hasher to use when combining the components of this instance. public func hash(into hasher: inout Hasher) { - hasher.combine(self.key) - if let type = self.type { - hasher.combine(ObjectIdentifier(type)) } } } +/// Public extension to provide a convenient way to create an `InjectIdentifier`. public extension InjectIdentifier { + /// Creates an `InjectIdentifier` instance. + /// + /// - Parameters: + /// - type: The type of the value to be injected. + /// - key: An optional key to further distinguish dependencies. + /// - Returns: An `InjectIdentifier` instance. static func by(type: Value.Type? = nil, key: String? = nil ) -> InjectIdentifier { - return .init(type: type, key: key) } } diff --git a/Tests/DIContainerTests/DIContainerTests.swift b/Tests/DIContainerTests/DIContainerTests.swift index 09b7a51..b180319 100644 --- a/Tests/DIContainerTests/DIContainerTests.swift +++ b/Tests/DIContainerTests/DIContainerTests.swift @@ -179,4 +179,23 @@ final class SlightDIContainerTests: XCTestCase { XCTAssertEqual(wrapperTest.text, expectedResult) XCTAssertEqual(wrapperTest.textSafe!, expectedResult) } + + func testWrapperInjectWithDefaultValueByStructType() { + + let expectedResult = "default_value" + + struct WrapperTest { + + @Injected(default: "default_value") + var text: String + + @InjectedSafe + var textSafe: String? + } + + var wrapperTest = WrapperTest() + + XCTAssertEqual(wrapperTest.text, expectedResult) + XCTAssertNil(wrapperTest.textSafe) + } }