Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
dehesa committed Oct 24, 2019
0 parents commit f6ea531
Show file tree
Hide file tree
Showing 19 changed files with 1,146 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
/.swiftpm

# Project specific
/Assets/Originals
1 change: 1 addition & 0 deletions Assets/Conbini.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Marcos Sánchez-Dehesa

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// swift-tools-version:5.1
import PackageDescription

let package = Package(
name: "Conbini",
platforms: [
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)
],
products: [
.library(name: "Conbini", targets: ["Conbini"])
],
dependencies: [],
targets: [
.target(name: "Conbini", dependencies: []),
.testTarget(name: "ConbiniTests", dependencies: ["Conbini"])
]
)
122 changes: 122 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<p align="center">
<img src="Assets/Conbini.svg" alt="Conbini icon"/>
</p>

Conbini provides convenience `Publisher`s, operators, and `Subscriber`s to squeeze the most out of Apple's [Combine framework](https://developer.apple.com/documentation/combine).

![Swift 5.1](https://img.shields.io/badge/Swift-5.1-orange.svg) ![platforms](https://img.shields.io/badge/platforms-iOS%20%7C%20macOS%20%7C%20tvOS%20%7C%20watchOS-lightgrey.svg) [![License](http://img.shields.io/:license-mit-blue.svg)](http://doge.mit-license.org)

## Operators

- `then` ignores all values and executes the provided publisher once a successful completion is received.
If a failed completion is emitted, it is forwarded downstream.
```swift
let publisher = setConfigurationOnServer.then {
subscribeToWebsocket.publisher
}
```

## Publishers

- `Complete` never emits a value and just completes (whether successfully or with a failure).
It offers similar functionality to `Empty` and `Failure`, but in a single type. Also, the completion only happens once a _greater-than-zero_ demand is requested.
```swift
let publisherA = Complete<Int,CustomError>(error: nil)
let publisherB = Complete(error: CustomError())
```
There are two more convenience initializers setting the publisher's `Output` and/or `Failure` to `Never` if the generic types are not explicitly stated.
- `Deferred...` publishers accept a closure that is executed once a _greater-than-zero_ demand is requested.
They have several flavors:

- `DeferredValue` emits a single value and then completes; however, the value is not provided/cached, but instead a closure which will generate the emitted value is executed per subscription received.

```swift
let publisher = DeferredValue {
return try someHeavyCalculations()
}
```

- `DeferredResult` offers the same functionality as `DeferredValue`, but the closure generates a `Result` instead.

```swift
let publisher = DeferredResult {
guard someExpression else { return .failure(CustomError()) }
return .success(someValue)
}
```

- `DeferredCompletion` offers the same functionality as `DeferredValue`, but the closure only generates a completion event.

```swift
let publisher = DeferredCompletion {
try somethingThatMightFail()
}
```

- `DeferredPassthrough` is similar to wrapping a `Passthrough` subject on a `Deferred` closure, with the diferrence that the `Passthrough` given on the closure is already _wired_ on the publisher chain and can start sending values right away. Also, the memory management is taken care of and every new subscriber receives a new subject (closure re-execution).

```swift
let publisher = DeferredPassthrough { (subject) in
subject.send(something)
subject.send(completion: .finished)
}
```

There are several reason for these publishers to exist instead of using other `Combine`-provided closure such as `Just`, `Future`, or `Deferred`:

- `Future` publishers execute their provided closure right away (upon initialization) and then cache the returned value. That value is then forwarded for any future subscription.
`Deferred...` closures await for subscriptions and a _greater-than-zero_ demand before executing. This also means, the closure will re-execute for any new subscription.
- `Deferred` is the most similar in functionality, but it only accepts a publisher.

- `Then` provides the functionality of the `then` operator.

## Subscribers

The following operators are actually testing subscribers. They allow easier testing for publisher chains making the test wait till a specific expectation is fulfilled (or making the test fail in a negative case).

- `expectsCompletion` subscribes to a publisher making the running test wait for a successful completion while ignoring all emitted values.

```swift
publisherChain.expectsCompletion(timeout: 0.8)
```

- `expectsFailure` subscribes to a publisher making the running test wait for a failed completion while ignoring all emitted values.

```swift
publisherChain.expectsFailure(timeout: 0.8)
```

- `expectsOne` subscribes to a publisher making the running test wait for a single value and a successful completion.
If more than one values are emitted or the publisher fails, the subscription gets cancelled and the test fails.

```swift
let emittedValue = publisherChain.expectsOne(timeout: 0.8)
```

- `expectsAll` subscribes to a publisher making the running test wait for zero or more values and a successful completion.

```swift
let emittedValues = publisherChain.expectsAll(timeout: 0.8)
```

- `expectAtLeast` subscribes to a publisher making the running test wait for at least the provided amount of values.
Once the provided amount of values is received, the publisher gets cancelled and the values are returned.

```swift
let emittedValues = publisherChain.expectsAtLeast(5, timeout: 0.8)
```

This operator/subscriber accepts an optional closure to check every value received.

```swift
let emittedValues = publisherChain.expectsAtLeast(5, timeout: 0.8) { (value) in
XCTAssert...
}
```

## References

- Apple's [Combine documentation](https://developer.apple.com/documentation/combine).
- [OpenCombine](https://github.com/broadwaylamb/OpenCombine) is an open source implementation of Apple's Combine framework.

> The framework name references both the `Combine` framework and the helpful Japanese convenience stores 😄
11 changes: 11 additions & 0 deletions Sources/Conbini/Operators/ThenOp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Combine

extension Publisher {
/// Ignores all value events and on successful completion it transform that into a give `Downstream` publisher.
///
/// The `downstream` closure will only be executed once the successful completion event arrives. If it doesn't arrive, it is never executed.
/// - parameter transform: Closure generating the stream to be switched to once a completion event is received from upstream.
public func then<Child>(_ transform: @escaping ()->Child) -> Publishers.Then<Child,Self> where Child:Publisher, Self.Failure==Child.Failure {
.init(upstream: self, transform: transform)
}
}
59 changes: 59 additions & 0 deletions Sources/Conbini/Publishers/Complete.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Combine

/// A publisher that never emits any values and just completes successfully or with a failure (depending on whether an error was provided in the initializer).
///
/// This publisher is used at the origin of a chain and it only provides the completion/failure when it receives a request with a deman greater than zero.
public struct Complete<Output,Failure:Swift.Error>: Publisher {
/// The error to send as a failure; otherwise the publisher completes successfully.
private let error: Failure?

/// Creates a publisher that completes as soon as it receives a demand from a subscriber.
/// - parameter error: The error to send as a failure; otherwise, it completes successfully.
public init(error: Failure?) {
self.error = error
}

public func receive<S>(subscriber: S) where S:Subscriber, Output==S.Input, Failure==S.Failure {
let subscription = Conduit(downstream: subscriber, error: self.error)
subscriber.receive(subscription: subscription)
}

/// The shadow subscription chain's origin.
private final class Conduit<Downstream>: Subscription where Downstream:Subscriber {
@SubscriptionState
private var state: (downstream: Downstream, error: Downstream.Failure?)
/// Sets up the guarded state.
/// - parameter downstream: Downstream subscriber receiving the data from this instance.
/// - parameter error: The success or error to be sent upon subscription.
init(downstream: Downstream, error: Downstream.Failure?) {
self._state = .init(wrappedValue: (downstream, error))
}

func request(_ demand: Subscribers.Demand) {
guard demand > 0, let (downstream, error) = self._state.remove() else { return }
downstream.receive(completion: error.map { .failure($0) } ?? .finished)
}

func cancel() {
self._state.remove()
}
}
}

extension Complete where Output==Never, Failure==Never {
/// Creates a publisher that completes successfully as soon as it receives a demand from a subscriber.
///
/// This will perform a similar operation to an `Empty` publisher with its `completeImmediately` property set to `true`.
public init() {
self.error = nil
}
}

extension Complete where Output==Never {
/// Creates a publisher that completes with a failure as soon as it receives a demand from a subscriber.
///
/// This will perform a similar operation to `Fail`.
public init(error: Failure) {
self.error = error
}
}
50 changes: 50 additions & 0 deletions Sources/Conbini/Publishers/DeferredCompletion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import Combine

/// A publisher returning the result of a given closure only executed on the first positive demand.
///
/// This publisher is used at the origin of a publisher chain and it only provides the value when it receives a request with a demand greater than zero.
public struct DeferredCompletion: Publisher {
public typealias Output = Never
public typealias Failure = Swift.Error
/// The closure type being store for delated execution.
public typealias Closure = () throws -> Void
/// Deferred closure.
/// - note: The closure is kept in the publisher, thus if you keep the publisher around any reference in the closure will be kept too.
private let closure: Closure

/// Creates a publisher that send a value and completes successfully or just fails depending on the result of the given closure.
public init(closure: @escaping Closure) {
self.closure = closure
}

public func receive<S>(subscriber: S) where S: Subscriber, Output==S.Input, Failure==S.Failure {
let subscription = Conduit(downstream: subscriber, closure: self.closure)
subscriber.receive(subscription: subscription)
}

/// The shadow subscription chain's origin.
private final class Conduit<Downstream>: Subscription where Downstream: Subscriber, Failure==Downstream.Failure {
@SubscriptionState
private var state: (downstream: Downstream, closure: Closure)

init(downstream: Downstream, closure: @escaping Closure) {
self._state = .init(wrappedValue: (downstream, closure))
}

func request(_ demand: Subscribers.Demand) {
guard demand > 0, let (downstream, closure) = self._state.remove() else { return }

do {
try closure()
} catch let error {
return downstream.receive(completion: .failure(error))
}

downstream.receive(completion: .finished)
}

func cancel() {
self._state.remove()
}
}
}

0 comments on commit f6ea531

Please sign in to comment.