Corridor A Coreader-like Dependency Injection μFramework

Overview

Corridor

A Coreader-like Dependency Injection μFramework

Build Status Language Carthage Compatible @elmkretzer

Table of Contents

Why | Examples | Usage | Installation | Credits & License |

Why

In order to write tests we must substitute parts of our code that we do not have control over such as:

  • Network
  • File system
  • Creating dates
  • Keychain

We need to substitute them in tests in order to verify assumptions.

The purpose of Corridor is to:

  • Provide a common interface for things that need to be replaced in TestCases
  • Simplify setup in TestCases without manually providing mocks etc
  • Transparently provide the current context to all your Types
  • Separate any kind of test related logic from production code

In an ideal World a Coeffect is under control.

class Controller: UIViewController {

  override func viewWillAppear(_ animated: Bool) {
    print(Date())
  }
}

The Date in the above example is out of control.

Running a test for that Controller will always result in a different Date. In that case the Date is just a placeholder for any Coeffect.

Corridor tries to solve this problem by taking the concept of a Coreader and turning it into a Swift friendly implementation via protocols and a single property.

What will it look like?

class Controller: UIViewController, HasInstanceContext {

  var resolve = `default`

  override func viewWillAppear(_ animated: Bool) {
     print(now)
  }
}

The idea behind Corridor was part of my talk at the Functional Swift Conference 2017 in Berlin.

Reader and Coreader for Dependency Injection

Usage

Implement a Protocol

Either one of the two protocols provided by Corridor: HasInstanceContext or HasStaticContext.
Or any convenience protocol that extends one of those.

Add a Property

Any type that needs access to an injected value also needs to know how to resolve it. This is done by providing a property called resolve.

By default it should be set to var resolve = `default` .

Why the backticks?
default is a swift keyword and by using the backticks the property looks more config-ish.

Setup Context

A base protocol that defines your dependencies:

protocol AppContext {

  /// The current Date
  var now: Date { get }
}

Context Implementation

An implemention of a Context. Usually we use two implementations. One for the running application, one for the test cases.

struct DefaultContext: AppContext {

  var now: Date {
    // The real current Date
    return Date()
  }
}

struct MockContext: AppContext {

  var now: Date {
    // We assume way earlier
    return Date.distantPast
  }
}

Resolver

In order to provide the default resolver you must extend the base protocol in Corridor. This will provide a static variable called default of Type Resolver to your Type in order to provide access.
This extension is done once in your app.

extension HasContext {

    typealias Context = AppContext

    static var `default`: Resolver<Self, AppContext> {
       return Resolver(context: DefaultContext())
    }
}

The visibility of any property in the context is controlled by extending either HasInstanceContext or HasStaticContext or any derived protocol.

By using protocols we can constrain access in a granular way. Additionally it allows for the injection of functions.

See example CorridorDemo.playground.

extension HasInstanceContext where Self.Context == AppContext  {

    /// Injected now property
    var now: Date {
        return resolve[\.now]
    }
}

Changing the Context

In your actual code everything resolves to the DefaultContext.
But in your Tests you need to make sure to switch to the mock context.
The simplest way is:

var myController = withContext(Controller(), MockContext())

Setting up the context in the Tests can easily be simplified by making the TestCase itself Context aware. Additionally you can build functions on top of that to make instantiation automagically have the correct Context.

extension HasContext {

    static var mock: Resolver<Self, AppContext> {
        return Resolver(context: MockContext())
    }
}

/// Extension for TestCase (e.g. subclass of XCTestCase)
/// to provide easy access to get controller with mock context
extension HasInstanceAppContext where Self: TestCase {

    func withController<V>() -> V?
        where V: UIViewController, V: ManagedByStoryboard, V: HasInstanceAppContext {
        /// A simplified function that will make sure your context is set
        return self.controller()
    }
}

Examples from Playground

See the provided Playground in the workspace.

Intro

import Foundation
import UIKit
import PlaygroundSupport
import Corridor

// 1. Protocol for Context 
// e.g. AppContext.swift
public protocol AppContext {
  var now: Date { get }
}

// 2. Context Implementation for Running App
// e.g. DefaultContext.swift
struct DefaultContext: AppContext {
  var now: Date { return Date() }
}

// 3. HasContext is Corridor base protocol
// e.g. Resolver.swift
extension HasContext {
  typealias Context = AppContext
  // provide default resolver
  static var `default`: Resolver<Self, AppContext> {
    return Resolver(context: DefaultContext())
  }
}

// 4. Add resolvable values
// e.g. Resolver.swift
extension HasInstanceContext
where Self.Context == AppContext {
  var now: Date {
    return resolve[\.now]
  }
}

// 5. Usage
final class Controller: LabelController, HasInstanceContext {
  var resolve = `default`
  override func viewWillAppear(_ animated: Bool) {
    label.text = "Now is \(now)"
  }
}

PlaygroundPage.current.liveView = Controller()

Test

import Foundation
import PlaygroundSupport
import Corridor

// 1. Context Implementation for Testing App
struct MockContext: AppContext {
  var now: Date { return Date.distantPast }
}

// 2. Usage
let test = withContext(Controller(), MockContext())

PlaygroundPage.current.liveView = test

Api

This example combines a Reader composition for chained REST calls. Defining the Api in AppContext and by implementing ContextAware it will have access to the current context. Therefore we don't need to pass additional params to the network calls, and it is ensured that all injected valus are correctly resolved.

= Reader typealias ApiFuture = ApiAware> typealias ApiBind = (Future) -> ApiFuture func bind(_ urlFrom: @escaping (I) -> String) -> ApiBind where O: Codable { return { future in ApiAware { api in future.flatMap { input in api.dispatch("\(api.now.formatted) \n -\(input)") return api.getResponse(urlFrom(input)) } } } } let apiEntrypoint: ApiFuture = ApiAware { api in Future(value: api.endpoint) } let usersEndpoint: ApiBind = bind { $0.usersEndpoint } let firstUserEndpoint: ApiBind = bind { $0.firstUserEndpoint } let addressEndpoint: ApiBind = bind { $0.addressEndpoint } let apiCall = usersEndpoint >=> firstUserEndpoint >=> addressEndpoint // 10. Controller final class Controller: LabelController, ContextAware { var resolve = `default` override func viewWillAppear(_ animated: Bool) { let address = apiEntrypoint .flatMap(apiCall) .run(api) address.onSuccess { (s: Address) in label.text = messages + "\n" + s } } } // 11. Run let app = Controller() let test = withContext(Controller(), MockContext()) PlaygroundPage.current.liveView = app ">
import Foundation
import UIKit
import PlaygroundSupport
import Corridor

// 1. Protocol for Context
protocol AppContext {
  var now: Date { get }
  var api: Api { get }
}

// 2. Context Implementation for Running App
struct DefaultContext: AppContext {
  var now: Date { return Date() }
  var api: Api { return Api(connection: ServerConnection()) }
}

// 3. Context Implementation for Test App
struct MockContext: AppContext {
  var now: Date { return Date.distantPast }
  var api: Api { return Api(connection: MockConnection()) }
}

// 4. Extend Corridor
extension HasContext {
  typealias Context = AppContext
  static var `default`: Resolver<Self, AppContext> {
    return Resolver(context: DefaultContext())
  }
}

// 5. Convenience protocol
protocol ContextAware: HasInstanceContext
where Self.Context == AppContext {}

// 6. Define API for (String) -> Future
struct Api: ContextAware {
  var resolve = `default`
  let connection: Connection
  init(connection: Connection) {
    self.connection = connection
  }
  var endpoint: Endpoint {
    return connection.endpoint
  }
  func getResponse<T: Codable>(_ s: String) -> Future {
    return connection.getResponse(s)
  }
}

// 7. Fake ReSwift Store
var dispatched: Set<String> = Set()

// 8. Extend Corridor
extension ContextAware {
  // Extract
  var now: Date {
    return resolve[\.now]
  }
  // Extend
  var api: Api {
    return resolve[\.api]
  }
  var dispatch: Dispatch {
    return { dispatched.insert($0) }
  }
  var messages: String {
    return dispatched.sorted().joined(separator: "\n")
  }
}

// 9. Define API Operations
typealias ApiAware<O> = Reader
typealias ApiFuture<O> = ApiAware>
typealias ApiBind<I, O> = (Future) -> ApiFuture

func bind<I, O>(_ urlFrom: @escaping (I) -> String) -> ApiBind
  where O: Codable {
    return { future in
      ApiAware { api in
        future.flatMap { input in
          api.dispatch("\(api.now.formatted) \n -\(input)")
          return api.getResponse(urlFrom(input))
        }
      }
    }
}

let apiEntrypoint: ApiFuture = ApiAware { api in
  Future<Endpoint>(value: api.endpoint)
}
let usersEndpoint: ApiBind = bind {
  $0.usersEndpoint
}
let firstUserEndpoint: ApiBind = bind {
  $0.firstUserEndpoint
}
let addressEndpoint: ApiBind = bind {
  $0.addressEndpoint
}

let apiCall = usersEndpoint >=> firstUserEndpoint >=> addressEndpoint

// 10. Controller
final class Controller: LabelController, ContextAware {

  var resolve = `default`

  override func viewWillAppear(_ animated: Bool) {

    let address = apiEntrypoint
      .flatMap(apiCall)
      .run(api)

    address.onSuccess { (s: Address) in
      label.text = messages + "\n" + s

    }
  }
}

// 11. Run
let app = Controller()
let test = withContext(Controller(), MockContext())
PlaygroundPage.current.liveView = app

FAQ

What if a property needs to be resolved to the context?

You can use lazy var to resolve properties directly. See Tests.

final class MyClass: HasInstanceContext {
  var resolve = `default`

  lazy var contextAwareProperty = resolve[AnotherContextAwareClass()]
}

Installation

Carthage

To integrate Corridor into your project using Carthage, add to your Cartfile:

github "symentis/Corridor"

See Carthage for further inststructions.

Requirements

Swift 4

Credits & License

Corridor is owned and maintained by Symentis GmbH.

Developed by: Elmar Kretzer Twitter

All modules are released under the MIT license. See LICENSE for details.

You might also like...
A simple way to handle dependency injection using property wrappers

Injektion Introduction A simple way to handle dependency injection using propert

Reliant - Nonintrusive Objective-C Dependency Injection

Reliant Reliant is a Dependency Injection (DI) framework for Objective-C, both for OS X and iOS. Its goal is to make its use as simple as possible, wh

Kraken - Simple Dependency Injection container for Swift. Use protocols to resolve dependencies with easy-to-use syntax!
Kraken - Simple Dependency Injection container for Swift. Use protocols to resolve dependencies with easy-to-use syntax!

Kraken Photo courtesy of www.krakenstudios.blogspot.com Introduction Kraken is a simple Dependency Injection Container. It's aimed to be as simple as

Pilgrim - Dependency injection for Swift (iOS, OSX, Linux). Strongly typed, pure Swift successor to Typhoon.

pilgrim.ph Pilgrim is a dependency injection library for Swift with the following features: Minimal runtime-only library that works with pure Swift (s

Perform - Easy dependency injection for storyboard segues
Perform - Easy dependency injection for storyboard segues

Perform Easy dependency injection for storyboard segues. import Perform // ... func tableView(_ tableView: UITableView, didSelectRowAt indexPath: NS

StoryboardBuilder - Simple dependency injection for generating views from storyboard.
StoryboardBuilder - Simple dependency injection for generating views from storyboard.

StoryboardBuilder Simple dependency injection for generating views from storyboard. Description StoryboardBuilder is framework to help simply and easi

ViperServices - Simple dependency injection container for services written for iOS in swift supporting boot order

ViperServices Introduction ViperServices is dependency injection container for iOS applications written in Swift. It is more lightweight and simple in

Needle - Compile-time safe Swift dependency injection framework
Needle - Compile-time safe Swift dependency injection framework

Needle is a dependency injection (DI) system for Swift. Unlike other DI frameworks, such as Cleanse, Swinject, Needle encourages hierarchical DI struc

Injector - A Swift package for simple dependency injection that also supports Swift UI previews

A Swift package for simple dependency injection that also supports Swift UI prev

Comments
  • FooContextAware as `default` Resolver provider.

    FooContextAware as `default` Resolver provider.

    Hi Elmar, the approach suggested in the readme, i.e:

    extension HasContext {
        typealias Context = FooContext
        static var `default`: Resolver<Self, FooContext> {
            return Resolver(context: DefaultFooContext())
        }
    }
    

    , has the limitation that this extension must either be private (1 default resolver per file) or internal (only one context protocol per target). While this is probably not a big practical problem, I wonder why only use FooContextAware as an empty convenience protocol, instead of actually providing the default resolver in that protocol. At first try, the following setup seems to work equally well for the hello world example, but I wonder if this is really wrong in some way:

    protocol FooContextAware: HasInstanceContext where Self.Context == FooContext {
    }
    extension FooContextAware {
        static var `default`: Resolver<Self, FooContext> {
            return Resolver(context: DefaultFooContext())
        }
    }
    
    question 
    opened by pteasima 3
  • README.md and Travis-CI / Fastlane tests

    README.md and Travis-CI / Fastlane tests

    I have updated the README.md, rewording a few things to read a little clearer, and improving on some formatting / linking.

    I also added badges, because badges are cool!

    In addition there is a .travis.yml file which executes fastlane scan and a Scanfile which runs the tests.

    The tests are passing on my fork with Travis-CI. Once the project is public and you have enabled the project in your Travis-CI, trigger a test and the testing badge should display as passing. 👍

    opened by madhavajay 1
Releases(1.0.1)
Owner
symentis GmbH
We build enterprise applications and love to share.
symentis GmbH
Cleanse is a dependency injection framework for Swift.

Cleanse - Swift Dependency Injection Cleanse is a dependency injection framework for Swift. It is designed from the ground-up with developer experienc

Square 1.7k Dec 16, 2022
Deli is an easy-to-use Dependency Injection Container that creates DI containers

Deli is an easy-to-use Dependency Injection Container that creates DI containers with all required registrations and corresponding factories.

Jungwon An 134 Aug 10, 2022
DIKit Dependency Injection Framework for Swift, inspired by KOIN.

DIKit Dependency Injection Framework for Swift, inspired by KOIN. Basically an implementation of service-locator pattern, living within the applicatio

null 95 Dec 22, 2022
Dip is a simple Dependency Injection Container.

Dip is a simple Dependency Injection Container. It's aimed to be as simple as possible yet p

Olivier Halligon 949 Jan 3, 2023
Tranquillity is a lightweight but powerful dependency injection library for swift.

DITranquillity Tranquillity is a lightweight but powerful dependency injection library for swift. The name "Tranquillity" laid the foundation in the b

Ivlev Alexander 393 Dec 24, 2022
Swinject is a lightweight dependency injection framework for Swift.

Swinject Swinject is a lightweight dependency injection framework for Swift. Dependency injection (DI) is a software design pattern that implements In

null 5.6k Dec 31, 2022
Typhoon Powerful dependency injection for Cocoa and CocoaTouch.

Typhoon Powerful dependency injection for Cocoa and CocoaTouch. Lightweight, yet full-featured and super-easy to use. Pilgrim is a pure Swift successo

AppsQuick.ly 2.7k Dec 14, 2022
Dependency Injection framework for Swift (iOS/macOS/Linux)

Declarative, easy-to-use and safe Dependency Injection framework for Swift (iOS/macOS/Linux) Features Dependency declaration via property wrappers or

Scribd 684 Dec 12, 2022
Swift Ultralight Dependency Injection / Service Locator framework

Swift Ultralight Dependency Injection / Service Locator framework

Michael Long 1.9k Jan 6, 2023
DIContainer Swift is an ultra-light dependency injection container made to help developers to handle dependencies easily. It works with Swift 5.1 or above.

?? DIContainer Swift It is an ultra-light dependency injection container made to help developers to handle dependencies easily. We know that handle wi

Victor Carvalho Tavernari 10 Nov 23, 2022