├── .github └── workflows │ ├── swift.yml │ └── swiftlinux.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE.md ├── Package.swift ├── README.md ├── SECURITY.md ├── Sources └── MicroInjection │ └── MicroInjection.swift └── Tests ├── LinuxMain.swift └── MicroInjectionTests ├── MicroInjectionNonCompileTests.swift.swift ├── MicroInjectionTests.swift └── XCTestManifests.swift /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.github/workflows/swiftlinux.yml: -------------------------------------------------------------------------------- 1 | name: SwiftLinux 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | container: swift:5.3-focal 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # Licenses 2 | 3 | ## Personal and non-commercial 4 | 5 | Everyone may use and modify freely for their own non-commercial software, personal experimentation and learning. 6 | 7 | ## Commercial licensing and distribution licensing 8 | 9 | The following individuals and organisations are granted a non-expiring license to use, modify and distribute this software with their products and use in their internal developments. If you or organisation would like to be licensed make a PR to modify this file to add the propsed additional licensee. The PR is likely to be accepted (and rapidly) in almost all cases and there is no financial cost. The contribution of the PR and recognition will in most cases be accepted as consideration. 10 | 11 | Human Friendly Ltd. 12 | Pulselive Innovations Ltd. 13 | 14 | If you would like alternative license conditions you can make the proposal in the PR and explain in the description or make contact to directly by email. joseph @ human-friendly.com. 15 | 16 | If you wish to use in a signficant existing open source project and need the license to be compatible I would definitely consider adopting a relevant license. Again please get in touch. 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "MicroInjection", 8 | products: [ 9 | // Products define the executables and libraries a package produces, and make them visible to other packages. 10 | .library( 11 | name: "MicroInjection", 12 | targets: ["MicroInjection"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 21 | .target( 22 | name: "MicroInjection", 23 | dependencies: []), 24 | .testTarget( 25 | name: "MicroInjectionTests", 26 | dependencies: ["MicroInjection"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroInjection 2 | 3 | A tiny (40 lines or ~100 lines including comments and whitespace) dependency injection framework inspired by the SwiftUI environment. 4 | 5 | Read the [blog post about it](https://blog.human-friendly.com/how-does-the-swiftui-environment-work-and-can-it-be-used-outside-swiftui-for-dependency-injection). 6 | 7 | ![Swift](https://github.com/josephlord/MicroInjection/workflows/Swift/badge.svg?branch=main) 8 | 9 | This hasn't been tested in any real project yet and it does use a Swift compiler feature that isn't officially supported (see the blog post for details). The property wrapper could be left out if needed it just wouldn't be quite as nice. 10 | 11 | Be aware that the property wrapper uses a compiler feature that is not officially supported (it was included as a possible future extension of the Property Wrappers Swift Evolution proposal). However even in the worst case if the feature is removed it should be possible to repace all the uses of the `@Injection` property wrapper with a computed var, that could mostly be done with a search and replace but would probably just need the type adding in each location. 12 | 13 | Currently there is a small test suite covering all the happy cases. There is another test file for cases that shouldn't compile to ensure that is the case. They are commented out so as not to fail builds but could be periodically checked in case any of them do build (and then lead to undesirable behaviour). 14 | 15 | It may well be that rather than have it as an external dependency you instead just drop the file into your project as a single file library (might build quicker that way than pulling the package from git). 16 | 17 | See the [LICENSE.md](LICENSE.md) file for information on licensing. 18 | 19 | ## Help wanted 20 | 21 | If you can see a way to allow the property wrapper to be used on structs and enums that is the additional feature that I'm looking for. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | No support guaranteed but will resposively update the current version if there is a security issue (hard to imagine how it could have security isses). 6 | 7 | ## Reporting a Vulnerability 8 | 9 | Email: security.microinjection at human-friendly.com 10 | -------------------------------------------------------------------------------- /Sources/MicroInjection/MicroInjection.swift: -------------------------------------------------------------------------------- 1 | /// Create a key that conforms to this protocol 2 | /// Any type that conforms to SwiftUI's EnvironmentKey can adopt this conformance without changes and vice-versa. 3 | public protocol InjectionKey { 4 | associatedtype Value 5 | /// By implementing this both the type and the mechanism to get an instance if nothing specific has been set in the InjectionValues 6 | static var defaultValue: Value { get } 7 | } 8 | 9 | /// Like EnvironmentValues but this is to be manually passed through your application and set as the 10 | /// `injection` property on any class that you want to be able to use the `@Injection` property 11 | /// wrapper it to get the necessary values from the 12 | public struct InjectionValues { 13 | let callForUnstoredValues: ((Any) -> Any?)? 14 | 15 | /// Create new empty environment 16 | /// The normal behaviour is for any values that aren't in the stored dictionary just to return the defaultValue 17 | /// from the key type itself. Optionally you can pass a closure to handle unstored values. If it returns a value 18 | /// the value must be of the correct type for the key. 19 | /// This is mostly for testing so that you can ensure only the keys you expect to be accessed are accessed 20 | public init(callForUnstoredValues: ((Any) -> Any?)? = nil) { 21 | self.callForUnstoredValues = callForUnstoredValues 22 | } 23 | 24 | // Did consider a dictionary of closures but unnecessary layoer of complication 25 | private var dict: [String: Any] = [:] 26 | 27 | /// You can call this directly but much nicer to extend InjectionValues and add a computed property to 28 | /// get/set it for you which is anyway required to get the `@Injection`property wrapper working 29 | public subscript(key: K.Type) -> K.Value where K : InjectionKey { 30 | get { 31 | // If this force unwrap ever fails this whole design is wrong 32 | let storedValue = dict[K.dictKey].map { $0 as! K.Value } 33 | if let unstoredValueClosure = callForUnstoredValues, 34 | storedValue == nil, 35 | let closureResult = unstoredValueClosure(key) { 36 | assert(closureResult is K.Value) 37 | return (closureResult as? K.Value) ?? K.defaultValue 38 | } 39 | return storedValue ?? K.defaultValue 40 | } 41 | set { 42 | dict[K.dictKey] = newValue 43 | } 44 | } 45 | 46 | /// Remove item from the dictionary and go back to using the defaultValues for that key. 47 | /// - Parameter key: The key to remove the stored information for. The defaultValue of the Key type will be used until the key is updated again 48 | public mutating func resetToDefault(key: K.Type) where K : InjectionKey { 49 | dict[K.dictKey] = nil 50 | } 51 | } 52 | 53 | /// Conform to this and you can then add `@Injection` wrapped properties to your class 54 | public protocol Injectable : class { 55 | /// This is where the `@Injection` properties will actually look up their values. You will often want to 56 | /// inject this in the init. You can also create an empty one, expose a mutable var or even have this implemetned with a computed var 57 | /// potentially to access a shared app injection if you want. 58 | /// 59 | /// Note: InjectionValues is a value type which means if it isn't a computed var accessing a shared instance a copy will made when it is set. 60 | var injection: InjectionValues { get } 61 | } 62 | 63 | /// The `@Injection` property wrapper can be added to classes conforming to Injectable 64 | /// and takes a keypath argument into InjectionValues (you should extend InjectionValues with with properties 65 | /// that you want to be able read from the Injection 66 | @frozen @propertyWrapper public struct Injection { 67 | 68 | public let keyPath: KeyPath 69 | 70 | @inlinable public init(_ key: KeyPath) { 71 | keyPath = key 72 | } 73 | 74 | /// This is where the magic happens. Look at this to understand how the `@Injection` actually works. Do not call this directly. 75 | /// 76 | /// When the wrapped value is unavailable this (unnofficially supported Swift - note _enclosingInstance) is called instead allowing the instance containing 77 | /// the `@Injection` wrapped property to be accessible to be able to read from the injection property of the enclosing type to get the required object. 78 | /// 79 | /// Note: Not intended to be called directly. Just access the `@Injection` wrapped property as if it is normal read only property and the magic will happen 80 | /// automatically 81 | @inlinable public static subscript ( 82 | _enclosingInstance instance: OuterSelf, 83 | wrapped wrappedKeyPath: KeyPath, 84 | storage storageKeyPath: KeyPath) -> Value { 85 | get { 86 | let keypath = instance[keyPath: storageKeyPath].keyPath 87 | return instance.injection[keyPath: keypath] 88 | } 89 | } 90 | 91 | @available(*, unavailable, message: "Expected subscript to be used") 92 | @inlinable public var wrappedValue: Value { 93 | get { fatalError() } 94 | } 95 | } 96 | 97 | extension InjectionKey { 98 | fileprivate static var dictKey: String { 99 | return String(reflecting: Self.self) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import MicroInjectionTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += MicroInjectionTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/MicroInjectionTests/MicroInjectionNonCompileTests.swift.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import MicroInjection 3 | 4 | fileprivate class Foo { 5 | let text: String 6 | init(text: String) { 7 | self.text = text 8 | } 9 | } 10 | 11 | fileprivate struct AKey : InjectionKey { 12 | static var defaultValue = "a" 13 | } 14 | 15 | extension InjectionValues { 16 | fileprivate var a: String { 17 | get { self[AKey.self] } 18 | set { self[AKey.self] = newValue } 19 | } 20 | } 21 | 22 | /// The intention of these tests is to prove that invalid usage will not compile. To use this uncomment the tests 23 | /// and check that they don't compile 24 | final class MicroInjectionCompileFailTests: XCTestCase { 25 | 26 | // func testSetWrongTypeValue() { 27 | // struct TestKey : InjectionKey { 28 | // static var defaultValue = 5 29 | // } 30 | // var injection = InjectionValues() 31 | // injection[TestKey.self] = "a" 32 | // } 33 | 34 | // func testExtendInjectionSetWrongTypeValue() { 35 | // var injection = InjectionValues() 36 | // injection.a = 3 37 | // } 38 | 39 | // func testPropertyWrapperWrongType() { 40 | // class Bar : Injectable { 41 | // let injection = InjectionValues() 42 | // @Injection(\.a) var a: Int 43 | // } 44 | // } 45 | 46 | // func testPropertyWrapperWrite() { 47 | // class Bar : Injectable { 48 | // let injection = InjectionValues() 49 | // @Injection(\.a) var a 50 | // } 51 | // let b = Bar() 52 | // b.a = "Can't write" 53 | // } 54 | 55 | // func testPropertyNonInjectableType() { 56 | // class Bar { 57 | // let injection = InjectionValues() 58 | // @Injection(\.a) var a 59 | // } 60 | // } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/MicroInjectionTests/MicroInjectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import MicroInjection 3 | 4 | fileprivate class Foo { 5 | let text: String 6 | init(text: String) { 7 | self.text = text 8 | } 9 | } 10 | 11 | fileprivate struct AKey : InjectionKey { 12 | static var defaultValue = "a" 13 | } 14 | 15 | extension InjectionValues { 16 | fileprivate var a: String { 17 | get { self[AKey.self] } 18 | set { self[AKey.self] = newValue } 19 | } 20 | } 21 | 22 | final class MicroInjectionTests: XCTestCase { 23 | func testDefaultValue() { 24 | struct TestKey : InjectionKey { 25 | static var defaultValue = 5 26 | } 27 | let injection = InjectionValues() 28 | XCTAssertEqual(injection[TestKey.self], 5) 29 | } 30 | 31 | func testDefaultValueComputed() { 32 | struct TestKey : InjectionKey { 33 | static var lastValue = 0 34 | static var defaultValue: Int { 35 | let next = lastValue + 5 36 | lastValue = next 37 | return next 38 | } 39 | } 40 | let injection = InjectionValues() 41 | XCTAssertEqual(injection[TestKey.self], 5) 42 | XCTAssertEqual(injection[TestKey.self], 10) 43 | } 44 | 45 | func testSetValue() { 46 | struct TestKey : InjectionKey { 47 | static var defaultValue = 5 48 | } 49 | var injection = InjectionValues() 50 | injection[TestKey.self] = 8 51 | XCTAssertEqual(injection[TestKey.self], 8) 52 | } 53 | 54 | func testDefaultObject() { 55 | struct TestKey : InjectionKey { 56 | static var defaultValue = Foo(text: "default") 57 | } 58 | let injection = InjectionValues() 59 | XCTAssertEqual(injection[TestKey.self].text, "default") 60 | } 61 | 62 | func testSetObject() { 63 | struct TestKey : InjectionKey { 64 | static var defaultValue = Foo(text: "default") 65 | } 66 | var injection = InjectionValues() 67 | injection[TestKey.self] = Foo(text: "Updated") 68 | XCTAssertEqual(injection[TestKey.self].text, "Updated") 69 | } 70 | 71 | func testExtendInjectionDefaultValue() { 72 | let injection = InjectionValues() 73 | XCTAssertEqual(injection.a, "a") 74 | } 75 | 76 | func testExtendInjectionSetValue() { 77 | var injection = InjectionValues() 78 | injection.a = "A" 79 | XCTAssertEqual(injection.a, "A") 80 | XCTAssertEqual(injection[AKey.self], "A") 81 | } 82 | 83 | func testExtendInjectionSetValueSubscript() { 84 | var injection = InjectionValues() 85 | injection[AKey.self] = "A" 86 | XCTAssertEqual(injection.a, "A") 87 | XCTAssertEqual(injection[AKey.self], "A") 88 | } 89 | 90 | func testPropertyWrapperDefault() { 91 | class Bar : Injectable { 92 | let injection = InjectionValues() 93 | @Injection(\.a) var a 94 | } 95 | let bar = Bar() 96 | XCTAssertEqual(bar.a, "a") 97 | } 98 | 99 | func testPropertyWrapperStored() { 100 | class Bar : Injectable { 101 | init(injection: InjectionValues) { 102 | self.injection = injection 103 | } 104 | let injection: InjectionValues 105 | @Injection(\.a) var a 106 | } 107 | var injection = InjectionValues() 108 | injection.a = "A" 109 | let bar = Bar(injection: injection) 110 | XCTAssertEqual(bar.a, "A") 111 | } 112 | 113 | func testCallForUnstoredNil() { 114 | var hasCalledCount = 0 115 | let injection = InjectionValues(callForUnstoredValues: { key in 116 | XCTAssertEqual(String(reflecting: key), String(reflecting: AKey.self)) 117 | hasCalledCount += 1 118 | return nil 119 | }) 120 | XCTAssertEqual(injection.a, "a") 121 | XCTAssertEqual(hasCalledCount, 1) 122 | } 123 | 124 | func testCallForUnstoredReturn() { 125 | var hasCalledCount = 0 126 | let injection = InjectionValues(callForUnstoredValues: { key in 127 | XCTAssertEqual(String(reflecting: key), String(reflecting: AKey.self)) 128 | hasCalledCount += 1 129 | return "A" 130 | }) 131 | XCTAssertEqual(injection.a, "A") 132 | XCTAssertEqual(hasCalledCount, 1) 133 | } 134 | 135 | func testCallForUnstoredNoCallWhenStored() { 136 | var hasCalledCount = 0 137 | var injection = InjectionValues(callForUnstoredValues: { key in 138 | hasCalledCount += 1 139 | return "Z" 140 | }) 141 | injection.a = "A" 142 | XCTAssertEqual(injection.a, "A") 143 | XCTAssertEqual(hasCalledCount, 0) 144 | } 145 | 146 | func testExtendInjectionResetValue() { 147 | var injection = InjectionValues() 148 | injection.a = "A" 149 | injection.resetToDefault(key: AKey.self) 150 | XCTAssertEqual(injection.a, "a") 151 | XCTAssertEqual(injection[AKey.self], "a") 152 | } 153 | 154 | 155 | // The functionality to set an overriding closure has been removed. It could potentially 156 | // be readded in the future but I think it is an unnecessary level of complication. 157 | // Just because something can be done doesn't mean it should be done. 158 | // func testExtendInjectionSetValueClosure() { 159 | // var injection = InjectionValues() 160 | // injection.set(key: AKey.self) { "A" } 161 | // XCTAssertEqual(injection.a, "A") 162 | // XCTAssertEqual(injection[AKey.self], "A") 163 | // } 164 | // 165 | // func testPropertyWrapperComputed() { 166 | // class Bar : Injectable { 167 | // init(injection: InjectionValues) { 168 | // self.injection = injection 169 | // } 170 | // let injection: InjectionValues 171 | // @Injection(\.a) var a 172 | // } 173 | // var injection = InjectionValues() 174 | // injection.set(key: AKey.self) { "A" } 175 | // let bar = Bar(injection: injection) 176 | // XCTAssertEqual(bar.a, "A") 177 | // } 178 | 179 | // I would like to be able to make this work but don't currently know a 180 | // mechanism to support structs being Injectable. This is a nice to have if 181 | // anyone has a solution it would be great. 182 | // func injectableStruct() { 183 | // struct Baz : Injectable { 184 | // let injection = InjectionValues() 185 | // } 186 | // } 187 | 188 | static var allTests = [ 189 | ("testDefaultValue", testDefaultValue), 190 | ("testDefaultValueComputed", testDefaultValueComputed), 191 | ("testSetValue", testSetValue), 192 | ("testDefaultObject", testDefaultObject), 193 | ("testSetObject", testSetObject), 194 | ("testExtendInjectionDefaultValue", testExtendInjectionDefaultValue), 195 | ("testExtendInjectionSetValue", testExtendInjectionSetValue), 196 | ("testExtendInjectionSetValueSubscript", testExtendInjectionSetValueSubscript), 197 | ("testPropertyWrapperDefault", testPropertyWrapperDefault), 198 | ("testPropertyWrapperStored", testPropertyWrapperStored), 199 | ("testCallForUnstoredNil", testCallForUnstoredNil), 200 | ("testCallForUnstoredReturn", testCallForUnstoredReturn), 201 | ("testCallForUnstoredNoCallWhenStored", testCallForUnstoredNoCallWhenStored), 202 | ] 203 | } 204 | -------------------------------------------------------------------------------- /Tests/MicroInjectionTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(MicroInjectionTests.allTests), 7 | ] 8 | } 9 | #endif 10 | --------------------------------------------------------------------------------