Skip to content

Commit

Permalink
Compatibility with beta 3 plus allow optional injections (#2)
Browse files Browse the repository at this point in the history
Compatibility with beta 3 plus allow optional injections
  • Loading branch information
petercv authored Jul 3, 2019
1 parent 244a145 commit 59b7beb
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 96 deletions.
2 changes: 1 addition & 1 deletion InjectPropertyWrapper.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

Pod::Spec.new do |spec|
spec.name = "InjectPropertyWrapper"
spec.version = "0.2.0"
spec.version = "0.3.0"
spec.author = { "Peter Verhage" => "[email protected]" }
spec.homepage = "https://github.com/egeniq/InjectPropertyWrapper"
spec.license = { :type => "MIT", :file => "LICENSE" }
Expand Down
34 changes: 0 additions & 34 deletions Package.resolved

This file was deleted.

50 changes: 32 additions & 18 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,41 @@
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription
import Foundation

// As we only want certain dependencies to be loaded for testing purposes, and there is no
// way to let Swift package manager know to only load the packages for testing purposes,
// we manually set testing to enabled or not to load the test dependencies and target.
let enableTests = ProcessInfo.processInfo.environment["ENABLE_TESTS"] == "1" ||
ProcessInfo.processInfo.environment["ENABLE_TESTS"] == "true" ||
ProcessInfo.processInfo.environment["ENABLE_TESTS"] == "yes"

var dependencies: [Package.Dependency] = []
var targets: [Target] = [
.target(name: "InjectPropertyWrapper", dependencies: []),
]

if enableTests {
dependencies.append(
.package(url: "https://github.com/Swinject/Swinject.git", from: "2.6.2")
)

targets.append(
.testTarget(
name: "InjectPropertyWrapperTests",
dependencies: [
"InjectPropertyWrapper",
"Swinject"
]
)
)
}

let package = Package(
name: "InjectPropertyWrapper",
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "InjectPropertyWrapper",
targets: ["InjectPropertyWrapper"]),
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/Swinject/Swinject.git", from: "2.6.2"),
.library(name: "InjectPropertyWrapper", targets: ["InjectPropertyWrapper"]),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "InjectPropertyWrapper",
dependencies: []),
.testTarget(
name: "InjectPropertyWrapperTests",
dependencies: ["InjectPropertyWrapper", "Swinject"]),
]
dependencies: dependencies,
targets: targets
)
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,22 @@
# InjectPropertyWrapper

Provides a generic Swift @Inject property wrapper that can be used to inject objects / services from
Provides a generic Swift `@Inject` property wrapper that can be used to inject objects / services from
a dependency injection framework of your choice.

## Basic Usage

First, you need to implement the [Sources/InjectPropertyWrapper/Resolver](Resolver) protocol for
First, you need to implement the [`Resolver`](Sources/InjectPropertyWrapper/Resolver.swift) protocol for
the Dependency Injection (DI) framework you are using.

For example, when using [https://github.com/Swinject/Swinject](Swinject):
For example, when using [Swinject](https://github.com/Swinject/Swinject):
```swift
extension Container: InjectPropertyWrapper.Resolver {
public func resolve<T>(_ type: T.Type) -> T {
return resolve(type)!
}

public func resolve<T>(_ type: T.Type, name: String) -> T {
return resolve(type, name: name)!
}
}
```

In case of Swinject the `Container` class already contains a method with the same signature (`resolve<T>(_ type: T, name: String?)`)
as the InjectPropertyWrapper `Resolver` protocol requires.

Then you need to set the global resolver (for example in your app delegate):
```swift
let container = Container()
Expand All @@ -34,7 +30,7 @@ container.register(MovieRepository.self) { _ in IMDBMovieRepository() }
container.register(MovieRepository.self, name: "netherlands") { _ in IMDBMovieRepository("nl") }
```

Now you can use the @Inject property wrapper to inject objects/services in your own classes:
Now you can use the `@Inject` property wrapper to inject objects/services in your own classes:
```swift
class IMDBMovieRepository: MovieRepository {
@Inject private var apiClient: APIClient
Expand Down Expand Up @@ -74,6 +70,25 @@ class MovieViewModel: BindableObject {
}
```

Normally if the property wrapper is unable to resolve a dependency it will raise a non-recoverable
fatal error. If for some reason you expect an object sometimes to be unavailable in your container,
you can mark the property as optional:
```swift
class MovieViewModel: BindableObject {
...
@Inject(name: "germany") private var deMovieRepository: MovieRepository?
...
}
```

## Testing

To run the tests for this package make sure the `ENABLE_TESTS` environment variable is set to `1` or `true`. For example when using the command line:
```
ENABLE_TESTS=1 swift test
```
This allows the package to only load certain dependencies when testing.

## License

This project is licensed under the terms of the MIT license. See the [LICENSE](LICENSE) file.
28 changes: 28 additions & 0 deletions Sources/InjectPropertyWrapper/FatalError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// FatalError.swift
//
//
// Created by Peter Verhage on 03/07/2019.
//
// See https://marcosantadev.com/test-swift-fatalerror/
//

import Foundation


struct FatalErrorUtil {
private static let defaultFatalErrorClosure = { Swift.fatalError($0, file: $1, line: $2) }
static var fatalErrorClosure: (String, StaticString, UInt) -> Never = defaultFatalErrorClosure

static func replaceFatalError(closure: @escaping (String, StaticString, UInt) -> Never) {
fatalErrorClosure = closure
}

static func restoreFatalError() {
fatalErrorClosure = defaultFatalErrorClosure
}
}

func fatalError(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) -> Never {
FatalErrorUtil.fatalErrorClosure(message(), file, line)
}
30 changes: 15 additions & 15 deletions Sources/InjectPropertyWrapper/Inject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@

@propertyWrapper
public struct Inject<Value> {
public private(set) var value: Value
public private(set) var wrappedValue: Value

public init() {
guard let resolver = InjectSettings.resolver else {
self.init(name: nil, resolver: nil)
}

public init(name: String? = nil, resolver: Resolver? = nil) {
guard let resolver = resolver ?? InjectSettings.resolver else {
fatalError("Make sure InjectSettings.resolver is set!")
}

self.init(resolver: resolver)
guard let value = resolver.resolve(Value.self, name: name) else {
fatalError("Could not resolve non-optional \(Value.self)")
}

wrappedValue = value
}

public init(name: String) {
guard let resolver = InjectSettings.resolver else {
public init<Wrapped>(name: String? = nil, resolver: Resolver? = nil) where Value == Optional<Wrapped> {
guard let resolver = resolver ?? InjectSettings.resolver else {
fatalError("Make sure InjectSettings.resolver is set!")
}

self.init(name: name, resolver: resolver)
}

public init(resolver: Resolver) {
value = resolver.resolve(Value.self)
}

public init(name: String, resolver: Resolver) {
value = resolver.resolve(Value.self, name: name)

wrappedValue = resolver.resolve(Wrapped.self, name: name)
}
}
3 changes: 1 addition & 2 deletions Sources/InjectPropertyWrapper/Resolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,5 @@
//

public protocol Resolver {
func resolve<T>(_ type: T.Type) -> T
func resolve<T>(_ type: T.Type, name: String) -> T
func resolve<T>(_ type: T.Type, name: String?) -> T?
}
8 changes: 1 addition & 7 deletions Tests/InjectPropertyWrapperTests/Container+Resolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,5 @@ import Swinject
import InjectPropertyWrapper

extension Container: InjectPropertyWrapper.Resolver {
public func resolve<T>(_ type: T.Type) -> T {
return resolve(type)!
}

public func resolve<T>(_ type: T.Type, name: String) -> T {
return resolve(type, name: name)!
}
// resolve method signature is the same
}
7 changes: 6 additions & 1 deletion Tests/InjectPropertyWrapperTests/MockObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ import struct InjectPropertyWrapper.Inject
class MockObject<T> {
@Inject var value: T
@Inject(name: "named") var namedValue: T
}
}

class MockObjectOptional<T> {
@Inject var value: T?
@Inject(name: "named") var namedValue: T?
}
8 changes: 2 additions & 6 deletions Tests/InjectPropertyWrapperTests/MockResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,7 @@ class MockResolver: Resolver {
registry[key(type: type, name: name)] = value
}

func resolve<T>(_ type: T.Type) -> T {
return registry[key(type: type)] as! T
}

func resolve<T>(_ type: T.Type, name: String) -> T {
return registry[key(type: type, name: name)] as! T
func resolve<T>(_ type: T.Type, name: String?) -> T? {
return registry[key(type: type, name: name)] as? T
}
}
14 changes: 14 additions & 0 deletions Tests/InjectPropertyWrapperTests/SimpleInjectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ final class SimpleInjectTests: XCTestCase {
let intObject = MockObject<Int>()
XCTAssertEqual(intObject.value, 123)
XCTAssertEqual(intObject.namedValue, 456)

// there is no registered bool, but the optional mock object has declared its
// injected properties as optional, so no error is thrown
let boolObject = MockObjectOptional<Bool>()
XCTAssertEqual(boolObject.value, nil)
XCTAssertEqual(boolObject.namedValue, nil)

// however, the non optional mock object does require all injected properties
// to be non optional, so we expect an exception here
expectFatalError(expectedMessage: "Could not resolve non-optional Bool") {
let boolObject = MockObject<Bool>()
XCTAssertEqual(boolObject.value, nil)
XCTAssertEqual(boolObject.namedValue, nil)
}
}

static var allTests = [
Expand Down
22 changes: 21 additions & 1 deletion Tests/InjectPropertyWrapperTests/SwinjectInjectTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,27 @@ final class SwinjectInjectTests: XCTestCase {

let intObject = MockObject<Int>()
XCTAssertEqual(intObject.value, 123)
XCTAssertEqual(intObject.namedValue, 456)
XCTAssertEqual(intObject.namedValue, 456)

// disable logging, because we don't want to confuse the test output
let loggingFunction = Container.loggingFunction
Container.loggingFunction = nil

// there is no registered bool, but the optional mock object has declared its
// injected properties as optional, so no error is thrown
let boolObject = MockObjectOptional<Bool>()
XCTAssertEqual(boolObject.value, nil)
XCTAssertEqual(boolObject.namedValue, nil)

// however, the non optional mock object does require all injected properties
// to be non optional, so we expect an exception here
expectFatalError(expectedMessage: "Could not resolve non-optional Bool") {
let boolObject = MockObject<Bool>()
XCTAssertEqual(boolObject.value, nil)
XCTAssertEqual(boolObject.namedValue, nil)
}

Container.loggingFunction = loggingFunction
}

static var allTests = [
Expand Down
39 changes: 39 additions & 0 deletions Tests/InjectPropertyWrapperTests/XCTestCase+FatalError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// File.swift
//
//
// Created by Peter Verhage on 03/07/2019.
//
// See https://marcosantadev.com/test-swift-fatalerror/
//

import Foundation

import XCTest
@testable import InjectPropertyWrapper

extension XCTestCase {
func expectFatalError(expectedMessage: String, testcase: @escaping () -> Void) {
let expectation = self.expectation(description: "expectingFatalError")
var assertionMessage: String? = nil

FatalErrorUtil.replaceFatalError { message, _, _ in
assertionMessage = message
expectation.fulfill()
self.unreachable()
}

DispatchQueue.global(qos: .userInitiated).async(execute: testcase)

waitForExpectations(timeout: 2) { _ in
XCTAssertEqual(assertionMessage, expectedMessage)
FatalErrorUtil.restoreFatalError()
}
}

private func unreachable() -> Never {
repeat {
RunLoop.current.run()
} while (true)
}
}

0 comments on commit 59b7beb

Please sign in to comment.