Swiftpack.co - alchemy-swift/fusion as Swift Package

Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
See all packages published by alchemy-swift.
alchemy-swift/fusion v0.3.0
Dependency Injection via Swift property wrappers
⭐️ 5
🕓 2 weeks ago
.package(url: "https://github.com/alchemy-swift/fusion.git", from: "v0.3.0")

Fusion: Services & Dependency Injection

Alchemy uses a helper library called Fusion for managing dependencies and injecting them. "Dependency Injection" is a phrase that refers to "injecting" concrete implementations of abstract service types typically through initializers or properties. Fusion can also be added to non-Alchemy targets, such as iOS or macOS apps.

Why Use Dependency Injection?

DI helps keep your code modular, testable and maintainable. It lets you define services in one place so that you may easily swap them for other implementations down the road or during tests.

Registering & Resolving Services

"Services" (a fancy word for an abstract interface, often a protocol) are registered and resolved from Containers. By default there is a global container, Container.default, that you can use to register & resolve services from.

For example, consider an abstract type, protocol Database, that is implemented by a concrete type, class PostgresDatabase: Database. You could register the PostgresDatabase type to Database via

Container.default.register(Database.self) { _ in
    PostgresDatabase(...)
}

Whenever you want to access the database; you can access it through Container.resolve.

let database = Container.default.resolve(Database.self)

This makes it easy to swap out the Database for another implementation, all you'd need to do is change the register closure.

Container.default.register(Database.self) { _ in
    MySQLDatabase(...)
}

Resolving with @Inject

You may also resolve a service with the @Inject property wrapper. The instance of the service will be resolved via the global container (Container.default) the first time this property is accessed.

@Inject var database: Database

Cross service dependencies

Sometimes, services rely on other services to function. You may resolve other services from the Container parameter in the register closure.

Container.register(Logger.self) { ... }

Container.register(Database.self) { container in
    let logger = container.resolve(Logger.self)
    return PostgresDatabase(..., logger)
}

Optional Resolving

By default, .resolve will fatalError if you try to resolve a service that isn't registered. This helps ensure that your program won't make it out of testing with you forgetting to register any services.

That being said, there may be special cases where you want to optionally resolve a service; returning nil if it isn't registered. For this, you may use Container.resolveOptional.

let optionalDatabase: Database? = Container.resolveOptional(Database.self)

Note: Optional resolving is not available when injecting via @Inject.

Service Types

Singleton Services

By default, services registered are "transient" meaning that their register closure is called each time it's resolved.

Sometimes, you'll want only a single instance of this service being passed around (a singleton). In this case, you can use .register(singleton:) to register your service.

Container.default.register(singleton: Database.self) { _ in
    PostgresDatabase(...)
}

A singleton instance is resolved once, then cached in it's Container to be injected on future calls to resolve.

Identified Singletons / Multitons

Sometimes you might want multiple instances of a singleton, each tied to a specific identifier (multiton / identified singleton). You can do this by passing an identifier when registering the singleton.

Perhaps you are working with two databases, one main one and one for writing logs to. You might register them like so,

enum DatabaseType: String {
    case main
    case logs
}

Container.register(singleton: Database, DatabaseType.main) { _ in
    PostgresDatabase(mainConfiguration)
}

Container.register(singleton: Database, DatabaseType.logs) { _ in
    PostgresDatabase(logConfiguration)
}

These can now be resolved by passing an identifier to the resolve function or the @Inject property wrapper.

// Via `.resolve`
let mainDB = Container.resolve(Database.self, identifier: DatabaseType.main)

// Via `@Inject`
@Inject(DatabaseType.main)
var mainDB: Database

Advanced Container Usage

In many cases, only using Container.default will be enough for what you're trying to do. There are some cases however, where you'd like to further modularize your code with custom containers.

Creating a Custom Container

You easily create your own containers.

let myContainer = Container()
myContainer.register(String.self) { 
    "Hello from my container!" 
}

let string = myContainer.resolve(String.self)
print(string) // "Hello from my container!"

Note: All closures and cached singletons are tied to the lifecycle of their container. When your custom container is deallocated, so will all it's closures and cached singletons.

Creating a Child Container

You can give a container a parent container. This means that if the child container doesn't have a service registered to it, resolving it will attempt to register the service from the parent container.

Container.register(Int.self) {
    0
}

let childContainer = Container(parent: .global)
childContainer.register(String.self) {
    "foo"
}

// "foo"
let string = childContainer.resolve(String.self)

// 0; inherited from parent
let int = childContainer.resolve(Int.self)

// fatalError; parents do not have access to their children's services
let int = Container.default.resolve(String.self)

Accessing Custom Containers from @Inject

By default, @Inject resolves services from the global container. If you'd like to inject from a custom container, you must conform the enclosing type to Containerized, which requires a var container: Container { get }.

class MyEnclosingType: Containerized {
    let container: Container

    @Inject var string: String
    @Inject var int: Int

    init(container: Container) {
        self.container = container
    }
}

let container = Container()
container.register(String.self) { "Howdy" }
container.register(Int.self) { 42 }

let myType = MyEnclosingType(container: container)
print(myType.string) // "Howdy"
print(myType.int) // 42

Services Automatically Registered to Container.default

There are a few types to be aware of that Alchemy automatically injects into the global container during setup. These can be accessed via @Inject or Container.resolve anywhere in your app.

  • Router: the router that will handle all incoming requests.
  • EventLoopGroup: the group of EventLoops to which your application runs requests on.

Services

Alchemy contains a Services type providing convenient static variables for injecting commonly used services from the global container.

let scheduler = Services.scheduler
let eventLoopGroup = Services.eventLoopGroup

You may also add custom static properties to it at your own convenience:

extension Services {
    static var someType: SomeType {
        Container.default.resolve(SomeType.self)
    }
}

Note: Many QueryBuilder & Rune ORM APIs default to running queries on Services.db. Be sure to register a singleton global database in your Application.setup to use them.

Other Note: You can mock many of these common services in tests by calling Services.mock() in your test case setUp().

// In Application.setup

Container.register(singleton: Database.self) { _ in
    PostgresDatabase(...)
}

Using Fusion in non-server targets

When you import Alchemy, you automatically import Fusion. If you'd like to use Fusion in a non-server target, you can add Fusion as a dependency through SPM and import it via import Fusion.

GitHub

link
Stars: 5
Last commit: 2 weeks ago
jonrohan Something's broken? Yell at me @ptrpavlik. Praise and feedback (and money) is also welcome.

Release Notes

v0.3.0
2 weeks ago

Refactors the internals to provide a cleaner interface.

Binding is simplified; it creates values with either a factory closure with a Container parameter or a value @autoclosure for providing a specific value.

  • bind(behavior:type:id:factory: (Container) -> T)
  • bind(behavior:type:id:value: @autoclosure () -> T)

Resolving is also simplified with three options.

  • resolve(_:id:) -> T? resolves a service, returning nil if it isn't registered
  • resolveAssert(_:id:) -> T resolves a service, failing an assertion if it isn't registered
  • resolveThrowing(_:id:) throws -> T resolves a service, throwing a FusionError.notRegistered if it isn't registered.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics