Skip to content

Commit

Permalink
Merge pull request #6 from Tavernari/default-injectable-value
Browse files Browse the repository at this point in the history
Enhanced Dependency Injection Functionality and Documentation
  • Loading branch information
Tavernari authored Jan 16, 2024
2 parents e55c3b3 + aa641d5 commit b62996d
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 48 deletions.
7 changes: 6 additions & 1 deletion .github/workflows/swift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -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
Expand Down
79 changes: 64 additions & 15 deletions Sources/DIContainer/Container.swift
Original file line number Diff line number Diff line change
@@ -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<Value> {

/// 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<Value>
private let container: Resolvable
public init(_ identifier: InjectIdentifier<Value>? = 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<Value>? = 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<Value> {

/// Returns the standard container used for resolving dependencies.
public static func container() -> Injectable { Container.standard }

private let identifier: InjectIdentifier<Value>
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<Value>? = 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)
}
83 changes: 65 additions & 18 deletions Sources/DIContainer/DIContainer.swift
Original file line number Diff line number Diff line change
@@ -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<Value>(_ identifier: InjectIdentifier<Value>) 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<Value>(_ identifier: InjectIdentifier<Value>, _ resolve: (Resolvable) throws -> Value)

/// Removes a dependency associated with an identifier.
///
/// - Parameter identifier: The identifier for the dependency to be removed.
func remove<Value>(_ identifier: InjectIdentifier<Value>)
}

/// 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<Value>(_ identifier: InjectIdentifier<Value>, _ 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<Value>(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<Value>(_ identifier: InjectIdentifier<Value>) {

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<Value>(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<Value>(_ identifier: InjectIdentifier<Value>) 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<Value>(type: Value.Type? = nil, key: String? = nil) throws -> Value {

try self.resolve(.by(type: type, key: key))
}
}
42 changes: 29 additions & 13 deletions Sources/DIContainer/InjectIdentifier.swift
Original file line number Diff line number Diff line change
@@ -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<Value> {

/// 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)
}
}
19 changes: 19 additions & 0 deletions Tests/DIContainerTests/DIContainerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

0 comments on commit b62996d

Please sign in to comment.