├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .travis.yml ├── LICENSE ├── Package.swift ├── QLoop.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── QLoop-iOS.xcscheme │ ├── QLoop-macOS.xcscheme │ ├── QLoop-tvOS.xcscheme │ └── QLoop-watchOS.xcscheme ├── README.md ├── Sources └── QLoop │ ├── Common │ └── QLCommon.swift │ ├── Iterating │ ├── QLoopIterating.swift │ ├── QLoopIteratorContinueNil.swift │ ├── QLoopIteratorContinueNilMax.swift │ ├── QLoopIteratorContinueOutput.swift │ ├── QLoopIteratorContinueOutputMax.swift │ └── QLoopIteratorSingle.swift │ ├── QLAnchor+ConvenienceInit.swift │ ├── QLAnchor.swift │ ├── QLPath.swift │ ├── QLoop+ConvenienceInit.swift │ ├── QLoop.swift │ ├── Result+ErrorGettable.swift │ └── Segment │ ├── AnySegment.swift │ ├── Parallel │ ├── QLParallelSegment+OperationRunner.swift │ └── QLParallelSegment.swift │ ├── QLSegment.swift │ └── Serial │ └── QLSerialSegment.swift ├── Tests ├── Info.plist ├── LinuxMain.swift └── QLoopTests │ ├── Common │ └── QLCommonConfigAnchorTests.swift │ ├── FibonacciTest.swift │ ├── Iterating │ ├── QLoopIteratorContinueNilMaxTests copy.swift │ ├── QLoopIteratorContinueNilTests copy.swift │ ├── QLoopIteratorContinueOutputMaxTests.swift │ ├── QLoopIteratorContinueOutputTests.swift │ └── QLoopIteratorSingleTests.swift │ ├── QLAnchorTests.swift │ ├── QLPathTests.swift │ ├── QLoop+ConvenienceInitTests.swift │ ├── QLoopTests.swift │ ├── Segment │ ├── QLParallelSegmentTests.swift │ ├── QLSegmentTests.swift │ └── QLSerialSegmentTests.swift │ ├── XCTestManifests.swift │ ├── helpers │ └── CapturedCompletion.swift │ └── mocks │ ├── MockComponent.swift │ ├── MockLoopIterable.swift │ ├── MockLoopIterator.swift │ ├── MockOperation.swift │ └── SpyAnchor.swift └── docs ├── changelog.md ├── getting-started.md ├── icon.png ├── introduction.md ├── loops.png ├── reference.md ├── reference ├── QLAnchor.md ├── QLParallelSegment.md ├── QLPath.md ├── QLSerialSegment.md └── QLoop.md └── segments.png /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Additional context** 20 | Add any other context about the problem here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /Packages 3 | .build/ 4 | build/ 5 | DerivedData/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.moved-aside 16 | *.xccheckout 17 | *.xcscmblueprint 18 | *.hmap 19 | *.ipa 20 | *.dSYM.zip 21 | *.dSYM 22 | timeline.xctimeline 23 | playground.xcworkspace 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode10.2 3 | xcode_project: QLoop.xcodeproj 4 | xcode_scheme: QLoop-iOS 5 | xcode_destination: platform=iOS Simulator,name=iPhone X 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018-2019 James Hall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "QLoop", 7 | products: [ 8 | .library(name: "QLoop", targets: ["QLoop"]), 9 | ], 10 | dependencies: [], 11 | targets: [ 12 | .target(name: "QLoop", dependencies: []), 13 | .testTarget(name: "QLoopTests", dependencies: ["QLoop"]), 14 | ] 15 | ) 16 | -------------------------------------------------------------------------------- /QLoop.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /QLoop.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-iOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 48 | 49 | 55 | 56 | 57 | 58 | 60 | 66 | 67 | 68 | 69 | 70 | 80 | 81 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-macOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 48 | 49 | 55 | 56 | 57 | 58 | 60 | 66 | 67 | 68 | 69 | 70 | 80 | 81 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-tvOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 48 | 49 | 55 | 56 | 57 | 58 | 60 | 66 | 67 | 68 | 69 | 70 | 80 | 81 | 87 | 88 | 89 | 90 | 96 | 97 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /QLoop.xcodeproj/xcshareddata/xcschemes/QLoop-watchOS.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 53 | 59 | 60 | 61 | 62 | 68 | 69 | 75 | 76 | 77 | 78 | 80 | 81 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![qloop](docs/icon.png) QLoop 2 | 3 | ![release_version](https://img.shields.io/github/tag/quickthyme/qloop.svg?label=release) 4 | [![Build Status](https://travis-ci.com/quickthyme/qloop.svg?branch=master)](https://travis-ci.com/quickthyme/qloop) 5 | [![swiftpm_compatible](https://img.shields.io/badge/swift_pm-compatible-brightgreen.svg?style=flat) ](https://swift.org/package-manager/) 6 | ![license](https://img.shields.io/github/license/quickthyme/qloop.svg?color=black) 7 | 8 | **QLoop** /'kyoo•loop/ - *n* - Declarative asynchronous operation loops 9 | 10 | - compose asynchronous operation paths as reusable "loop" constructs 11 | - *test-friendly* observer-pattern module favoring declarative composition 12 | - built-in error propagation 13 | - swiftPM compatible package 14 | - universal module; Swift 4.2+, 5 (default) 15 | 16 | Compose `paths` of asynchronous operation `segments`, then bind them to anchors 17 | or wrap them up into *observable* loops. Simply decorate an entity with empty `loops` 18 | and/or `anchors`, and implement the `onChange` and/or `onError` events. 19 | 20 | Designed to be simple to use, test, and debug. *(Or so it's intended.)* 21 | 22 |
23 | 24 | ## [Introduction](docs/introduction.md) 25 | 26 | a.k.a. *[what it is and what it does](docs/introduction.md)*. 27 | 28 | 29 | ## [Getting Started](docs/getting-started.md) 30 | 31 | How to *[install and start using](docs/getting-started.md)* it. 32 | 33 | 34 | ## [API Reference](docs/reference.md) 35 | 36 | Basically just a listing of the *[classes, functions, and arguments](docs/reference.md)* that make up QLoop. 37 | 38 | 39 | ## [Change Log](docs/changelog.md) 40 | 41 | On-going *[summary of pertinent changes](docs/changelog.md)* from one version to the next. 42 | 43 | 44 | ## [Demo App](https://github.com/quickthyme/qloop-demo) 45 | 46 | The example app, *[qloop-demo](https://github.com/quickthyme/qloop-demo)*, 47 | demonstrates how to write a declarative iOS app using QLoop, which includes 48 | real-world working examples of static composition, error handling, concurrent 49 | threads, and unit-testing. 50 | 51 | 52 |
53 | 54 | --- 55 | 56 | Enjoying QLoop? You might check out its soul-mate: 57 | *[QRoute](https://github.com/quickthyme/qroute)*, 58 | a library providing declarative navigation and routing features with similar 59 | enthusiasm. Using them together, or separately, is up to you. 60 | 61 | :) 62 | -------------------------------------------------------------------------------- /Sources/QLoop/Common/QLCommon.swift: -------------------------------------------------------------------------------- 1 | public struct QLCommon { 2 | 3 | public struct Config { 4 | 5 | public struct Anchor { 6 | 7 | public static var releaseValues: Bool = { 8 | #if !DEBUG 9 | return true 10 | #else 11 | return false 12 | #endif 13 | }() 14 | 15 | public static var autoThrowResultFailures: Bool = true 16 | } 17 | } 18 | 19 | public enum Error: Swift.Error { 20 | case AnchorMismatch 21 | case Unknown 22 | case ThrownButNotSet 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/QLoop/Iterating/QLoopIterating.swift: -------------------------------------------------------------------------------- 1 | public protocol QLoopIterating: AnyObject { 2 | @discardableResult 3 | func iterate(_ loop: QLoopIterable) -> Bool 4 | } 5 | 6 | public protocol QLoopIteratingResettable: QLoopIterating { 7 | func reset() 8 | } 9 | 10 | public protocol QLoopIterable { 11 | var discontinue: Bool { get } 12 | func iteration() 13 | func iterationFromLastOutput() 14 | } 15 | -------------------------------------------------------------------------------- /Sources/QLoop/Iterating/QLoopIteratorContinueNil.swift: -------------------------------------------------------------------------------- 1 | public final class QLoopIteratorContinueNil: QLoopIterating { 2 | 3 | public init() { 4 | } 5 | 6 | @discardableResult 7 | public func iterate(_ loop: QLoopIterable) -> Bool { 8 | guard (loop.discontinue == false) else { return false } 9 | loop.iteration() 10 | return true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/QLoop/Iterating/QLoopIteratorContinueNilMax.swift: -------------------------------------------------------------------------------- 1 | public final class QLoopIteratorContinueNilMax: QLoopIteratingResettable { 2 | 3 | public var iterations: Int = 0 4 | public var maxIterations: Int 5 | 6 | public init(_ maxIterations: Int) { 7 | self.maxIterations = maxIterations 8 | } 9 | 10 | public func reset() { 11 | iterations = 0 12 | } 13 | 14 | @discardableResult 15 | public func iterate(_ loop: QLoopIterable) -> Bool { 16 | guard (loop.discontinue == false) else { return false } 17 | iterations += 1 18 | if (iterations < maxIterations) { 19 | loop.iteration() 20 | return true 21 | } 22 | return false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/QLoop/Iterating/QLoopIteratorContinueOutput.swift: -------------------------------------------------------------------------------- 1 | public final class QLoopIteratorContinueOutput: QLoopIterating { 2 | 3 | public init() { 4 | } 5 | 6 | @discardableResult 7 | public func iterate(_ loop: QLoopIterable) -> Bool { 8 | guard (loop.discontinue == false) else { return false } 9 | loop.iterationFromLastOutput() 10 | return true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/QLoop/Iterating/QLoopIteratorContinueOutputMax.swift: -------------------------------------------------------------------------------- 1 | public final class QLoopIteratorContinueOutputMax: QLoopIteratingResettable { 2 | 3 | public var iterations: Int = 0 4 | public var maxIterations: Int 5 | 6 | public init(_ maxIterations: Int) { 7 | self.maxIterations = maxIterations 8 | } 9 | 10 | public func reset() { 11 | iterations = 0 12 | } 13 | 14 | @discardableResult 15 | public func iterate(_ loop: QLoopIterable) -> Bool { 16 | guard (loop.discontinue == false) else { return false } 17 | iterations += 1 18 | if (iterations < maxIterations) { 19 | loop.iterationFromLastOutput() 20 | return true 21 | } 22 | return false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/QLoop/Iterating/QLoopIteratorSingle.swift: -------------------------------------------------------------------------------- 1 | public final class QLoopIteratorSingle: QLoopIterating { 2 | 3 | public init() { 4 | } 5 | 6 | @discardableResult 7 | public func iterate(_ loop: QLoopIterable) -> Bool { 8 | return false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/QLoop/QLAnchor+ConvenienceInit.swift: -------------------------------------------------------------------------------- 1 | public extension QLAnchor { 2 | private static func emptyIn(_ o: Input?)->() {/**/} 3 | private static func emptyErr(_ e: Error)->() {/**/} 4 | 5 | convenience init() { 6 | self.init(onChange: QLAnchor.emptyIn, 7 | onError: QLAnchor.emptyErr) 8 | } 9 | 10 | convenience init(onChange: @escaping OnChange) { 11 | self.init(onChange: onChange, 12 | onError: QLAnchor.emptyErr) 13 | } 14 | 15 | convenience init(earlyRepeaters: QLAnchor...) { 16 | self.init(echoFilter: QLAnchor.DefaultEchoFilter, 17 | earlyRepeaters: earlyRepeaters, 18 | lateRepeaters: []) 19 | } 20 | 21 | convenience init(lateRepeaters: QLAnchor...) { 22 | self.init(echoFilter: QLAnchor.DefaultEchoFilter, 23 | earlyRepeaters: [], 24 | lateRepeaters: lateRepeaters) 25 | } 26 | 27 | convenience init(earlyRepeaters: [QLAnchor], 28 | lateRepeaters: [QLAnchor]) { 29 | self.init(echoFilter: QLAnchor.DefaultEchoFilter, 30 | earlyRepeaters: earlyRepeaters, 31 | lateRepeaters: lateRepeaters) 32 | } 33 | 34 | convenience init(echoFilter: @escaping EchoFilter, 35 | earlyRepeaters: QLAnchor...) { 36 | self.init(echoFilter: echoFilter, 37 | earlyRepeaters: earlyRepeaters, 38 | lateRepeaters: []) 39 | } 40 | 41 | convenience init(echoFilter: @escaping EchoFilter, 42 | lateRepeaters: QLAnchor...) { 43 | self.init(echoFilter: echoFilter, 44 | earlyRepeaters: [], 45 | lateRepeaters: lateRepeaters) 46 | } 47 | 48 | convenience init(echoFilter: @escaping EchoFilter, 49 | earlyRepeaters: [QLAnchor], 50 | lateRepeaters: [QLAnchor]) { 51 | self.init(onChange: QLAnchor.emptyIn, 52 | onError: QLAnchor.emptyErr) 53 | self.addRepeaters(earlyRepeaters, timing: .early) 54 | self.addRepeaters(lateRepeaters, timing: .late) 55 | self.echoFilter = echoFilter 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Sources/QLoop/QLAnchor.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | public protocol AnyAnchor: AnyObject { 4 | var inputSegment: AnySegment? { get } 5 | } 6 | 7 | public final class QLAnchor: AnyAnchor { 8 | public typealias OnChange = (Input?)->() 9 | public typealias OnError = (Error)->() 10 | 11 | public typealias EchoFilter = (Input?, QLAnchor) -> (Bool) 12 | internal static var DefaultEchoFilter: EchoFilter { return { _, _ in return true } } 13 | 14 | public enum Timing { 15 | case early, late 16 | } 17 | 18 | internal final class Repeater { 19 | 20 | weak var anchor: QLAnchor? 21 | 22 | let timing: Timing 23 | 24 | init(_ anchor: QLAnchor, timing: Timing) { 25 | self.timing = timing 26 | self.anchor = anchor 27 | } 28 | 29 | func echo(value: Input?, filter: EchoFilter, timing: Timing) { 30 | if let repeater = self.anchor, 31 | filter(value, repeater) { 32 | repeater.value = value 33 | } 34 | } 35 | 36 | func echo(error: Error) { 37 | anchor?.error = error 38 | } 39 | } 40 | 41 | lazy var inputQueue = DispatchQueue(label: "\(self).inputQueue", 42 | qos: .userInitiated) 43 | 44 | public required init(onChange: @escaping OnChange, 45 | onError: @escaping OnError) { 46 | self.onChange = onChange 47 | self.onError = onError 48 | } 49 | 50 | public var onChange: OnChange 51 | 52 | public var onError: OnError 53 | 54 | public var inputSegment: AnySegment? 55 | 56 | public func addRepeaters(_ repeaters: [QLAnchor], timing: Timing) { 57 | let repeaters = repeaters.map { Repeater($0, timing: timing) } 58 | self._repeaters.append(contentsOf: repeaters) 59 | } 60 | 61 | public func getRepeaters(timing: Timing) -> [QLAnchor] { 62 | return _repeaters.compactMap { ($0.timing == timing) ? $0.anchor : nil } 63 | } 64 | 65 | internal var _repeaters: [Repeater] = [] 66 | 67 | public var echoFilter: EchoFilter = DefaultEchoFilter 68 | 69 | private var _value: Input? 70 | public var value: Input? { 71 | get { 72 | var safeInput: Input? = nil 73 | inputQueue.sync { safeInput = self._value } 74 | return safeInput 75 | } 76 | set { 77 | inputQueue.sync { self._value = newValue } 78 | 79 | if let err = getReroutableError(newValue) { 80 | self.error = err 81 | } else { 82 | echo(value: newValue, timing: .early) 83 | dispatch(value: newValue) 84 | echo(value: newValue, timing: .late) 85 | } 86 | 87 | if (QLCommon.Config.Anchor.releaseValues) { 88 | inputQueue.sync { self._value = nil } 89 | } 90 | } 91 | } 92 | 93 | private var _error: Error? 94 | public var error: Error? { 95 | get { 96 | var safeError: Error? = nil 97 | inputQueue.sync { safeError = self._error } 98 | return safeError 99 | } 100 | set { 101 | let err: Error = newValue ?? QLCommon.Error.ThrownButNotSet 102 | inputQueue.sync { self._error = err } 103 | dispatch(error: err) 104 | echo(error: err) 105 | 106 | if (QLCommon.Config.Anchor.releaseValues) { 107 | inputQueue.sync { self._error = nil } 108 | } 109 | } 110 | } 111 | 112 | private func getReroutableError(_ newValue: Input?) -> Error? { 113 | guard QLCommon.Config.Anchor.autoThrowResultFailures, 114 | let errGettable = newValue as? ErrorGettable, 115 | let err = errGettable.getError() 116 | else { return nil } 117 | return err 118 | } 119 | 120 | private func dispatch(value: Input?) { 121 | DispatchQueue.main.async { 122 | self.onChange(value) 123 | } 124 | } 125 | 126 | private func dispatch(error: Error) { 127 | DispatchQueue.main.async { 128 | self.onError(error) 129 | } 130 | } 131 | 132 | private func echo(value: Input?, timing: Timing) { 133 | for repeater in _repeaters { 134 | guard repeater.timing == timing else { continue } 135 | repeater.echo(value: value, filter: echoFilter, timing: timing) 136 | } 137 | } 138 | 139 | private func echo(error: Error) { 140 | for repeater in _repeaters { 141 | repeater.echo(error: error) 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Sources/QLoop/QLPath.swift: -------------------------------------------------------------------------------- 1 | open class QLPath { 2 | public typealias Operation = QLSegment.Operation 3 | public typealias Completion = QLSegment.Completion 4 | 5 | public required init?(_ segments: AnySegment...) { 6 | 7 | guard 8 | let lastSegment = segments.last, 9 | let firstSegment = segments.first 10 | else { return nil } 11 | 12 | do { 13 | let _: AnySegment = 14 | 15 | try segments.reduce( 16 | firstSegment, 17 | ({ result, next in 18 | guard (result !== next) else { return result } 19 | guard let _ = result.linked(to: next) 20 | else { throw QLCommon.Error.AnchorMismatch } 21 | return next 22 | }) 23 | ) 24 | 25 | lastSegment.applyOutputAnchor(self.output) 26 | self.input = firstSegment.inputAnchor as! QLAnchor 27 | 28 | } catch { return nil } 29 | } 30 | 31 | public final var input: QLAnchor = QLAnchor() 32 | public final var output: QLAnchor = QLAnchor() 33 | 34 | public final func findSegments(with operationId: AnyHashable) -> [AnySegment] { 35 | return output.inputSegment?.findSegments(with: operationId) ?? [] 36 | } 37 | 38 | public final func describeOperationPath() -> String { 39 | return output.inputSegment?.describeOperationPath() ?? "" 40 | } 41 | 42 | public final func operationPath() -> QLoopOperationPath { 43 | return output.inputSegment?.operationPath() ?? [] 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Sources/QLoop/QLoop+ConvenienceInit.swift: -------------------------------------------------------------------------------- 1 | public extension QLoop { 2 | private static func emptyOut(_ o: Output?)->() {/**/} 3 | private static func emptyErr(_ e: Error)->() {/**/} 4 | 5 | convenience init() { 6 | self.init(iterator: QLoopIteratorSingle(), 7 | onFinal: QLoop.emptyOut, 8 | onChange: QLoop.emptyOut, 9 | onError: QLoop.emptyErr) 10 | } 11 | 12 | convenience init(iterator: QLoopIterating) { 13 | self.init(iterator: iterator, 14 | onFinal: QLoop.emptyOut, 15 | onChange: QLoop.emptyOut, 16 | onError: QLoop.emptyErr) 17 | } 18 | 19 | convenience init(onChange: @escaping OnChange) { 20 | self.init(iterator: QLoopIteratorSingle(), 21 | onFinal: QLoop.emptyOut, 22 | onChange: onChange, 23 | onError: QLoop.emptyErr) 24 | } 25 | 26 | convenience init(onChange: @escaping OnChange, 27 | onError: @escaping OnError) { 28 | self.init(iterator: QLoopIteratorSingle(), 29 | onFinal: QLoop.emptyOut, 30 | onChange: onChange, 31 | onError: onError) 32 | } 33 | 34 | convenience init(iterator: QLoopIterating, 35 | onChange: @escaping OnChange) { 36 | self.init(iterator: iterator, 37 | onFinal: QLoop.emptyOut, 38 | onChange: onChange, 39 | onError: QLoop.emptyErr) 40 | } 41 | 42 | convenience init(iterator: QLoopIterating, 43 | onError: @escaping OnError) { 44 | self.init(iterator: iterator, 45 | onFinal: QLoop.emptyOut, 46 | onChange: QLoop.emptyOut, 47 | onError: onError) 48 | } 49 | 50 | convenience init(iterator: QLoopIterating, 51 | onChange: @escaping OnChange, 52 | onError: @escaping OnError) { 53 | self.init(iterator: iterator, 54 | onFinal: QLoop.emptyOut, 55 | onChange: onChange, 56 | onError: onError) 57 | } 58 | 59 | convenience init(onFinal: @escaping OnChange) { 60 | self.init(iterator: QLoopIteratorSingle(), 61 | onFinal: onFinal, 62 | onChange: QLoop.emptyOut, 63 | onError: QLoop.emptyErr) 64 | } 65 | 66 | convenience init(iterator: QLoopIterating, 67 | onFinal: @escaping OnChange) { 68 | self.init(iterator: iterator, 69 | onFinal: onFinal, 70 | onChange: QLoop.emptyOut, 71 | onError: QLoop.emptyErr) 72 | } 73 | 74 | convenience init(iterator: QLoopIterating, 75 | onFinal: @escaping OnChange, 76 | onChange: @escaping OnChange) { 77 | self.init(iterator: iterator, 78 | onFinal: onFinal, 79 | onChange: onChange, 80 | onError: QLoop.emptyErr) 81 | } 82 | 83 | convenience init(iterator: QLoopIterating, 84 | onFinal: @escaping OnChange, 85 | onError: @escaping OnError) { 86 | self.init(iterator: iterator, 87 | onFinal: onFinal, 88 | onChange: QLoop.emptyOut, 89 | onError: onError) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/QLoop/QLoop.swift: -------------------------------------------------------------------------------- 1 | public final class QLoop: QLoopIterable { 2 | public typealias Operation = QLSegment.Operation 3 | public typealias Completion = QLSegment.Completion 4 | public typealias OnChange = QLAnchor.OnChange 5 | public typealias OnError = QLAnchor.OnError 6 | 7 | public required init(iterator: QLoopIterating, 8 | onFinal: @escaping OnChange, 9 | onChange: @escaping OnChange, 10 | onError: @escaping OnError) { 11 | self.iterator = iterator 12 | self.onFinal = onFinal 13 | self.onChange = onChange 14 | self.onError = onError 15 | self.applyOutputObservers() 16 | } 17 | 18 | public var input: QLAnchor = QLAnchor() 19 | public var output: QLAnchor = QLAnchor() { 20 | didSet { applyOutputObservers() } 21 | } 22 | 23 | private var lastValue: Output? = nil 24 | 25 | public func perform() { 26 | self.perform(nil) 27 | } 28 | 29 | public func perform(_ inputValue: Input?) { 30 | if let iterator = self.iterator as? QLoopIteratingResettable { 31 | iterator.reset() 32 | } 33 | self.input.value = inputValue 34 | } 35 | 36 | public func iteration() { 37 | self.input.value = nil 38 | } 39 | 40 | public func iterationFromLastOutput() { 41 | self.input.value = (self.lastValue as? Input) 42 | } 43 | 44 | public var discontinue: Bool = false 45 | public var shouldResume: Bool = false 46 | 47 | public var onFinal: OnChange 48 | public var onChange: OnChange 49 | public var onError: OnError 50 | 51 | public var iterator: QLoopIterating 52 | 53 | public func bind(path: QLPath) { 54 | self.input = path.input 55 | self.output = path.output 56 | } 57 | 58 | public func bind(segment: QLSegment) { 59 | segment.output = self.output 60 | self.input = segment.input 61 | } 62 | 63 | public func destroy() { 64 | self.input = QLAnchor() 65 | self.output = QLAnchor() 66 | } 67 | 68 | public func findSegments(with operationId: AnyHashable) -> [AnySegment] { 69 | return output.inputSegment?.findSegments(with: operationId) ?? [] 70 | } 71 | 72 | public func describeOperationPath() -> String { 73 | return output.inputSegment?.describeOperationPath() ?? "" 74 | } 75 | 76 | public func operationPath() -> QLoopOperationPath { 77 | return output.inputSegment?.operationPath() ?? [] 78 | } 79 | 80 | private func applyOutputObservers() { 81 | self.output.onChange = ({ 82 | self.lastValue = $0 83 | self.onChange($0) 84 | if (self.iterator.iterate(self) == false) { 85 | self.onFinal($0) 86 | } 87 | }) 88 | 89 | self.output.onError = ({ 90 | self.onError($0) 91 | self.discontinue = !self.shouldResume 92 | let _ = self.iterator.iterate(self) 93 | }) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/QLoop/Result+ErrorGettable.swift: -------------------------------------------------------------------------------- 1 | internal protocol ErrorGettable { 2 | func getError() -> Error? 3 | } 4 | 5 | extension Result: ErrorGettable { 6 | func getError() -> Error? { 7 | if case let .failure(err) = self { 8 | return err 9 | } 10 | return nil 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/QLoop/Segment/AnySegment.swift: -------------------------------------------------------------------------------- 1 | public protocol AnySegment: AnyObject { 2 | var inputAnchor: AnyAnchor { get } 3 | var outputAnchor: AnyAnchor? { get } 4 | var operationIds: [AnyHashable] { get } 5 | var hasErrorHandler: Bool { get } 6 | func linked(to otherSegment: AnySegment) -> Self? 7 | func applyOutputAnchor(_ otherAnchor: AnyAnchor) 8 | func findSegments(with operationId: AnyHashable) -> [AnySegment] 9 | func describeOperationPath() -> String 10 | func operationPath() -> QLoopOperationPath 11 | } 12 | 13 | public typealias QLoopOperationPath = [(operationIds: [AnyHashable], hasErrorHandler: Bool)] 14 | -------------------------------------------------------------------------------- /Sources/QLoop/Segment/Parallel/QLParallelSegment+OperationRunner.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | internal extension QLParallelSegment { 4 | 5 | class OperationBox { 6 | var id: AnyHashable 7 | var operation: ParallelOperation 8 | var completed: Bool 9 | var value: Any? 10 | var queue: DispatchQueue? 11 | init(_ id: AnyHashable, 12 | _ operation: @escaping ParallelOperation, 13 | _ queue: DispatchQueue?) { 14 | self.id = id 15 | self.operation = operation 16 | self.completed = false 17 | self.value = nil 18 | self.queue = queue 19 | } 20 | } 21 | 22 | class OperationRunner: Hashable { 23 | let completionQueue = DispatchQueue.init(label: "QLParallelSegment.CompletionQueue") 24 | 25 | init(_ id: AnyHashable, 26 | _ operations: [(AnyHashable, ParallelOperation)], 27 | _ queues: [AnyHashable:DispatchQueue]) { 28 | self.id = id 29 | self.operations = operations.map { 30 | return OperationBox($0.0, $0.1, queues[$0.0]) 31 | } 32 | } 33 | 34 | var id: AnyHashable 35 | var operations: [OperationBox] = [] 36 | var completion: ( ([(AnyHashable, Any?)])->() )! 37 | var errorCompletion: ( (Error)->() )! 38 | 39 | func run(_ input: Input?) { 40 | for opBox in self.operations { 41 | let queue = opBox.queue ?? DispatchQueue.main 42 | queue.async { self.runOperation(input, opBox) } 43 | } 44 | } 45 | 46 | func runOperation(_ input: Input?, _ opBox: OperationBox) { 47 | do { try opBox.operation(input, { output in 48 | opBox.value = output 49 | opBox.completed = true 50 | self.allCompleted = true }) 51 | } catch { 52 | self.errorCompletion(error) 53 | } 54 | } 55 | 56 | var allCompleted: Bool { 57 | get { 58 | var isCompleted: Bool = false 59 | completionQueue.sync { isCompleted = self.__allCompleted } 60 | return isCompleted 61 | } 62 | set { 63 | completionQueue.sync { 64 | if (self.__allCompleted == true) { return } 65 | self.__allCompleted = self.operations 66 | .reduce(true, { ($1.completed) ? $0 : false } ) 67 | if (self.__allCompleted == true) { 68 | self.completion(operations.map { ($0.id, $0.value) }) 69 | } 70 | } 71 | } 72 | } 73 | private var __allCompleted: Bool = false 74 | 75 | static func == (lhs: QLParallelSegment.OperationRunner, 76 | rhs: QLParallelSegment.OperationRunner) -> Bool { 77 | return lhs.id == rhs.id 78 | } 79 | 80 | func hash(into hasher: inout Hasher) { 81 | hasher.combine(id) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/QLoop/Segment/Parallel/QLParallelSegment.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | public typealias QLps = QLParallelSegment 4 | 5 | public final class QLParallelSegment: QLSegment { 6 | public typealias ParallelOperation = QLSegment.Operation 7 | public typealias ErrorHandler = QLSegment.ErrorHandler 8 | public typealias Completion = QLSegment.Completion 9 | public typealias ErrorCompletion = QLSegment.ErrorCompletion 10 | public typealias Combiner = (Output?, (Output?, (AnyHashable, Any?)) -> Output?) 11 | 12 | lazy var startingQueue = DispatchQueue(label: "\(self).startingQueue") 13 | 14 | public required init(_ operations: [AnyHashable:ParallelOperation], 15 | combiner: Combiner?, 16 | errorHandler: ErrorHandler?, 17 | output: QLAnchor?) { 18 | super.init() 19 | if let tx = combiner { self.combiner = tx } 20 | self.errorHandler = errorHandler 21 | self.operations = operations.map { ($0.key, $0.value) } 22 | self.output = output 23 | self.input = QLAnchor() 24 | } 25 | 26 | public convenience init(_ operations: [AnyHashable:ParallelOperation]) { 27 | self.init(operations, 28 | combiner: nil, 29 | errorHandler: nil, 30 | output: nil) 31 | } 32 | 33 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 34 | operationQueues: [AnyHashable:DispatchQueue]) { 35 | self.init(operations) 36 | self.operationQueues = operationQueues 37 | } 38 | 39 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 40 | errorHandler: ErrorHandler?) { 41 | self.init(operations, 42 | combiner: nil, 43 | errorHandler: errorHandler, 44 | output: nil) 45 | } 46 | 47 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 48 | operationQueues: [AnyHashable:DispatchQueue], 49 | errorHandler: ErrorHandler?) { 50 | self.init(operations, errorHandler: errorHandler) 51 | self.operationQueues = operationQueues 52 | } 53 | 54 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 55 | combiner: Combiner?) { 56 | self.init(operations, 57 | combiner: combiner, 58 | errorHandler: nil, 59 | output: nil) 60 | } 61 | 62 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 63 | operationQueues: [AnyHashable:DispatchQueue], 64 | combiner: Combiner?) { 65 | self.init(operations, combiner: combiner) 66 | self.operationQueues = operationQueues 67 | } 68 | 69 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 70 | combiner: Combiner?, 71 | errorHandler: ErrorHandler?) { 72 | self.init(operations, 73 | combiner: combiner, 74 | errorHandler: errorHandler, 75 | output: nil) 76 | } 77 | 78 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 79 | operationQueues: [AnyHashable:DispatchQueue], 80 | combiner: Combiner?, 81 | errorHandler: ErrorHandler?) { 82 | self.init(operations, combiner: combiner, errorHandler: errorHandler) 83 | self.operationQueues = operationQueues 84 | } 85 | 86 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 87 | combiner: Combiner?, 88 | output: QLAnchor?) { 89 | self.init(operations, 90 | combiner: combiner, 91 | errorHandler: nil, 92 | output: output) 93 | } 94 | 95 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 96 | combiner: Combiner?, 97 | outputSegment: QLSegment?) { 98 | self.init(operations, 99 | combiner: combiner, 100 | errorHandler: nil, 101 | output: outputSegment?.input) 102 | } 103 | 104 | public convenience init(_ operations: [AnyHashable:ParallelOperation], 105 | combiner: Combiner?, 106 | errorHandler: ErrorHandler?, 107 | outputSegment: QLSegment?) { 108 | self.init(operations, 109 | combiner: combiner, 110 | errorHandler: errorHandler, 111 | output: outputSegment?.input) 112 | } 113 | 114 | 115 | public override var input: QLAnchor { 116 | didSet { applyInputObservers() } 117 | } 118 | 119 | public override weak var output: QLAnchor? { 120 | didSet { 121 | self.output?.inputSegment = self 122 | applyInputObservers() 123 | } 124 | } 125 | 126 | public override var operationIds: [AnyHashable] { return operations.map { $0.0 } } 127 | 128 | public var operationQueues: [AnyHashable:DispatchQueue] = [:] 129 | 130 | private var combiner: Combiner = (nil, { r,n in return (n.1 as? Output ?? r) }) 131 | 132 | private var operations: [(AnyHashable, ParallelOperation)] = [] 133 | 134 | private var operationRunnersStarted: Int = 0 135 | 136 | private var runningOperations: Set = [] 137 | 138 | private final func applyInputObservers() { 139 | guard let _ = self.output else { return } 140 | 141 | self.input.onChange = ({ input in 142 | let startId = self.getSynchronizedStartId() 143 | let runner = self.getNewOperationRunner(startId) 144 | self.startOperationRunner(runner, input) 145 | }) 146 | 147 | self.input.onError = makeOnError() 148 | } 149 | 150 | private func getSynchronizedStartId() -> Int { 151 | var startId: Int = 0 152 | startingQueue.sync { 153 | startId = self.operationRunnersStarted + 1 154 | self.operationRunnersStarted = startId 155 | } 156 | return startId 157 | } 158 | 159 | private func getNewOperationRunner(_ startId: AnyHashable) -> OperationRunner { 160 | let runner = OperationRunner(startId, self.operations, self.operationQueues) 161 | runner.completion = makeRunnerCompletion(for: runner) 162 | runner.errorCompletion = makeOnError() 163 | return runner 164 | } 165 | 166 | private func makeRunnerCompletion(for runner: OperationRunner) -> ([(AnyHashable, Any?)])->() { 167 | return ({ results in 168 | self.combineOperationResults(results) 169 | self.destoryOperationRunner(runner) 170 | }) 171 | } 172 | 173 | private func makeOnError() -> (Error)->() { 174 | return ({ error in 175 | type(of: self).handleError(error, self) 176 | }) 177 | } 178 | 179 | fileprivate func combineOperationResults(_ opResults:[(AnyHashable, Any?)]) { 180 | let tr = self.combiner 181 | self.output?.value = opResults.reduce(tr.0, tr.1) 182 | } 183 | 184 | private func startOperationRunner(_ runner: OperationRunner, _ input: Input?) { 185 | startingQueue.sync { let _ = self.runningOperations.insert(runner) } 186 | runner.run(input) 187 | } 188 | 189 | private func destoryOperationRunner(_ runner: OperationRunner) { 190 | startingQueue.sync { let _ = runningOperations.remove(runner) } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Sources/QLoop/Segment/QLSegment.swift: -------------------------------------------------------------------------------- 1 | open class QLSegment: AnySegment { 2 | public typealias Operation = (Input?, @escaping Completion) throws -> () 3 | public typealias ErrorHandler = (Error, @escaping Completion, @escaping ErrorCompletion) -> () 4 | public typealias Completion = (Output?) -> () 5 | public typealias ErrorCompletion = (Error) -> () 6 | 7 | internal init() {} 8 | 9 | open var input: QLAnchor = QLAnchor() 10 | open weak var output: QLAnchor? 11 | 12 | open var operation: Operation = {_,_ in } 13 | open var operationIds: [AnyHashable] { return [] } 14 | 15 | public var hasErrorHandler: Bool { return self.errorHandler != nil } 16 | 17 | public var errorHandler: ErrorHandler? = nil 18 | 19 | internal static func handleError(_ error: Error, _ segment: QLSegment) { 20 | guard let outAnchor = segment.output else { return } 21 | guard let handler = segment.errorHandler 22 | else { outAnchor.error = error; return } 23 | let completion: Completion = { outAnchor.value = $0 } 24 | let errorCompletion: ErrorCompletion = { outAnchor.error = $0 } 25 | handler(error, completion, errorCompletion) 26 | } 27 | 28 | 29 | // MARK: - Shared type-erased functions 30 | 31 | public var inputAnchor: AnyAnchor { 32 | return self.input 33 | } 34 | 35 | public var outputAnchor: AnyAnchor? { 36 | return self.output 37 | } 38 | 39 | public func linked(to otherSegment: AnySegment) -> Self? { 40 | guard 41 | let compatibleInput = otherSegment.inputAnchor as? QLAnchor 42 | else { return nil } 43 | self.output = compatibleInput 44 | return self 45 | } 46 | 47 | public func applyOutputAnchor(_ otherAnchor: AnyAnchor) { 48 | self.output = otherAnchor as? QLAnchor 49 | } 50 | 51 | 52 | public func findSegments(with operationId: AnyHashable) -> [AnySegment] { 53 | return type(of: self).findSegments(with: operationId, fromSegment: self) 54 | } 55 | 56 | public static func findSegments(with operationId: AnyHashable, fromSegment: AnySegment, 57 | currentResults: [AnySegment] = []) -> [AnySegment] { 58 | let newResults = (fromSegment.operationIds.contains(operationId)) 59 | ? [fromSegment] + currentResults 60 | : currentResults 61 | guard let next = fromSegment.inputAnchor.inputSegment else { return newResults } 62 | return findSegments(with: operationId, fromSegment: next, currentResults: newResults) 63 | } 64 | 65 | 66 | public func describeOperationPath() -> String { 67 | return type(of: self).describeOperationPath(fromSegment: self) 68 | } 69 | 70 | public static func describeOperationPath(fromSegment: AnySegment) -> String { 71 | let opPath = operationPath(fromSegment: fromSegment) 72 | return opPath.reduce("", { (currentOps, next) in 73 | let nextOps: String = 74 | "{" 75 | + next.0.map({ "\($0)\((next.1) ? "*" : "")" }) 76 | .sorted() 77 | .joined(separator: ":") 78 | + "}" 79 | return (currentOps != "") 80 | ? [currentOps, nextOps].joined(separator: "-") 81 | : nextOps 82 | }) 83 | } 84 | 85 | public func operationPath() -> QLoopOperationPath { 86 | return type(of: self).operationPath(fromSegment: self) 87 | } 88 | 89 | public static func operationPath(fromSegment: AnySegment, 90 | _ preResults: QLoopOperationPath = []) -> QLoopOperationPath { 91 | let newResults = [(fromSegment.operationIds, fromSegment.hasErrorHandler)] + preResults 92 | guard let next = fromSegment.inputAnchor.inputSegment else { return newResults } 93 | return operationPath(fromSegment: next, newResults) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/QLoop/Segment/Serial/QLSerialSegment.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | public typealias QLss = QLSerialSegment 4 | 5 | public final class QLSerialSegment: QLSegment { 6 | public typealias Operation = QLSegment.Operation 7 | public typealias ErrorHandler = QLSegment.ErrorHandler 8 | public typealias Completion = QLSegment.Completion 9 | public typealias ErrorCompletion = QLSegment.ErrorCompletion 10 | 11 | public required init(operationId: AnyHashable, 12 | operation: @escaping Operation, 13 | errorHandler: ErrorHandler?, 14 | output: QLAnchor?) { 15 | self.operationId = operationId 16 | super.init() 17 | self.operation = operation 18 | self.errorHandler = errorHandler 19 | self.output = output 20 | self.input = QLAnchor() 21 | } 22 | 23 | public convenience init(_ operationId: AnyHashable, 24 | _ operation: @escaping Operation) { 25 | self.init(operationId: operationId, 26 | operation: operation, 27 | errorHandler: nil, 28 | output: nil) 29 | } 30 | 31 | public convenience init(_ operationId: AnyHashable, 32 | _ operation: @escaping Operation, 33 | errorHandler: ErrorHandler?) { 34 | self.init(operationId: operationId, 35 | operation: operation, 36 | errorHandler: errorHandler, 37 | output: nil) 38 | } 39 | 40 | public convenience init(_ operationId: AnyHashable, 41 | _ operation: @escaping Operation, 42 | operationQueue: DispatchQueue?) { 43 | self.init(operationId, operation) 44 | if let queue = operationQueue { 45 | self.operationQueue = queue 46 | } 47 | } 48 | 49 | public convenience init(_ operationId: AnyHashable, 50 | _ operation: @escaping Operation, 51 | operationQueue: DispatchQueue?, 52 | errorHandler: ErrorHandler?) { 53 | self.init(operationId, operation, 54 | errorHandler: errorHandler) 55 | if let queue = operationQueue { 56 | self.operationQueue = queue 57 | } 58 | } 59 | 60 | public convenience init(_ operationId: AnyHashable, 61 | _ operation: @escaping Operation, 62 | output: QLAnchor?) { 63 | self.init(operationId: operationId, 64 | operation: operation, 65 | errorHandler: nil, 66 | output: output) 67 | } 68 | 69 | public convenience init(_ operationId: AnyHashable, 70 | _ operation: @escaping Operation, 71 | errorHandler: ErrorHandler?, 72 | output: QLAnchor?) { 73 | self.init(operationId: operationId, 74 | operation: operation, 75 | errorHandler: errorHandler, 76 | output: output) 77 | } 78 | 79 | public convenience init(_ operationId: AnyHashable, 80 | _ operation: @escaping Operation, 81 | outputSegment: QLSegment?) { 82 | self.init(operationId: operationId, 83 | operation: operation, 84 | errorHandler: nil, 85 | output: outputSegment?.input) 86 | } 87 | 88 | public convenience init(_ operationId: AnyHashable, 89 | _ operation: @escaping Operation, 90 | errorHandler: ErrorHandler?, 91 | outputSegment: QLSegment?) { 92 | self.init(operationId: operationId, 93 | operation: operation, 94 | errorHandler: errorHandler, 95 | output: outputSegment?.input) 96 | } 97 | 98 | 99 | public override var input: QLAnchor { 100 | didSet { applyInputObservers() } 101 | } 102 | 103 | public override weak var output: QLAnchor? { 104 | didSet { 105 | self.output?.inputSegment = self 106 | applyInputObservers() 107 | } 108 | } 109 | 110 | public let operationId: AnyHashable 111 | 112 | public override var operationIds: [AnyHashable] { return [operationId] } 113 | 114 | public lazy var operationQueue: DispatchQueue = DispatchQueue.main 115 | 116 | private final func applyInputObservers() { 117 | 118 | self.input.onChange = ({ input in 119 | self.operationQueue.async { 120 | self.tryOperation(input) 121 | } 122 | }) 123 | 124 | self.input.onError = ({ error in 125 | type(of: self).handleError(error, self) 126 | }) 127 | } 128 | 129 | private final func tryOperation(_ input: Input?) { 130 | guard let outAnchor = self.output else { return } 131 | let completion: Completion = { outAnchor.value = $0 } 132 | do { try self.operation(input, completion) } 133 | catch { type(of: self).handleError(error, self) } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import QLoopTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += QLoopTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Common/QLCommonConfigAnchorTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLCommonConfigAnchorTests: XCTestCase { 6 | 7 | override func tearDown() { 8 | QLCommon.Config.Anchor.releaseValues = false 9 | super.tearDown() 10 | } 11 | 12 | func test_when_releaseValues_is_enabled_it_remembers_value() { 13 | var received: Int = -1 14 | let expect = expectation(description: "shouldSet") 15 | let subject = QLAnchor(onChange: { received = $0!; expect.fulfill() }) 16 | 17 | QLCommon.Config.Anchor.releaseValues = true 18 | subject.value = 99 19 | 20 | wait(for: [expect], timeout: 8.0) 21 | XCTAssertEqual(received, 99) 22 | XCTAssertNil(subject.value) 23 | } 24 | 25 | func test_when_releaseValues_is_enabled_it_remembers_error() { 26 | var received: Int? = nil 27 | var receivedError: Error? = nil 28 | let expect = expectation(description: "shouldSet") 29 | let subject = QLAnchor(onChange: { received = $0; expect.fulfill() }, 30 | onError: { receivedError = $0; expect.fulfill() }) 31 | 32 | QLCommon.Config.Anchor.releaseValues = true 33 | subject.error = QLCommon.Error.Unknown 34 | 35 | wait(for: [expect], timeout: 8.0) 36 | XCTAssertNil(received) 37 | XCTAssertEqual(receivedError as? QLCommon.Error, QLCommon.Error.Unknown) 38 | XCTAssertNil(subject.value) 39 | XCTAssertNil(subject.error) 40 | } 41 | 42 | func test_when_releaseValues_is_disabled_it_forgets_value() { 43 | var received: Int = -1 44 | let expect = expectation(description: "shouldSet") 45 | let subject = QLAnchor(onChange: { received = $0!; expect.fulfill() }) 46 | 47 | QLCommon.Config.Anchor.releaseValues = false 48 | subject.value = 99 49 | 50 | wait(for: [expect], timeout: 8.0) 51 | XCTAssertEqual(received, 99) 52 | XCTAssertEqual(subject.value, 99) 53 | } 54 | 55 | func test_when_releaseValues_is_disabled_it_forgets_error() { 56 | var received: Int? = nil 57 | var receivedError: Error? = nil 58 | let expect = expectation(description: "shouldSet") 59 | let subject = QLAnchor(onChange: { received = $0; expect.fulfill() }, 60 | onError: { receivedError = $0; expect.fulfill() }) 61 | 62 | QLCommon.Config.Anchor.releaseValues = false 63 | subject.error = QLCommon.Error.Unknown 64 | 65 | wait(for: [expect], timeout: 8.0) 66 | XCTAssertNil(received) 67 | XCTAssertEqual(receivedError as? QLCommon.Error, QLCommon.Error.Unknown) 68 | XCTAssertEqual(subject.error as? QLCommon.Error, QLCommon.Error.Unknown) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Tests/QLoopTests/FibonacciTest.swift: -------------------------------------------------------------------------------- 1 | 2 | import QLoop 3 | import XCTest 4 | 5 | class FibonacciTest: XCTestCase { 6 | 7 | func test_using_loop_to_generate_fibonacci_sequence() { 8 | let expect = expectation(description: "should cycle 64 times") 9 | var finalValue: Int = 0 10 | let loop = QLoop<(Int, Int), (Int, Int)>( 11 | iterator: QLoopIteratorContinueOutputMax(64), 12 | onFinal: ({ 13 | finalValue = $0!.0 14 | expect.fulfill() 15 | })) 16 | 17 | loop.bind(segment: 18 | QLss("fibonacci", ({ 19 | let (cur, pre) = $0! 20 | $1( (cur + pre, cur) ) 21 | }))) 22 | loop.perform((1,0)) 23 | 24 | wait(for: [expect], timeout: 6.0) 25 | XCTAssertEqual(finalValue , 17167680177565) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Iterating/QLoopIteratorContinueNilMaxTests copy.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoopIteratorContinueNilMaxTests: XCTestCase { 6 | 7 | func test_iterate_max1_shouldCallPerform_0_times() { 8 | let mockLoop = MockLoopIterable() 9 | let subject = QLoopIteratorContinueNilMax(1) 10 | subject.iterate(mockLoop) 11 | subject.iterate(mockLoop) 12 | subject.iterate(mockLoop) 13 | 14 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 15 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 16 | } 17 | 18 | func test_iterate_max2_shouldCallPerform_1_times() { 19 | let mockLoop = MockLoopIterable() 20 | let subject = QLoopIteratorContinueNilMax(2) 21 | subject.iterate(mockLoop) 22 | subject.iterate(mockLoop) 23 | subject.iterate(mockLoop) 24 | 25 | XCTAssertEqual(mockLoop.timesCalled_iteration, 1) 26 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 27 | } 28 | 29 | func test_iterate_max3_shouldCallPerform_2_times() { 30 | let mockLoop = MockLoopIterable() 31 | let subject = QLoopIteratorContinueNilMax(3) 32 | subject.iterate(mockLoop) 33 | subject.iterate(mockLoop) 34 | subject.iterate(mockLoop) 35 | 36 | XCTAssertEqual(mockLoop.timesCalled_iteration, 2) 37 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 38 | } 39 | 40 | func test_when_discontinue_becomes_true_then_it_should_stop() { 41 | let mockLoop = MockLoopIterable() 42 | let subject = QLoopIteratorContinueNilMax(6) 43 | subject.iterate(mockLoop) 44 | subject.iterate(mockLoop) 45 | subject.iterate(mockLoop) 46 | mockLoop.discontinue = true 47 | subject.iterate(mockLoop) 48 | subject.iterate(mockLoop) 49 | 50 | XCTAssertEqual(mockLoop.timesCalled_iteration, 3) 51 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Iterating/QLoopIteratorContinueNilTests copy.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoopIteratorContinueNilTests: XCTestCase { 6 | 7 | func test_iterate_1_times_shouldCallPerform_1_times() { 8 | let mockLoop = MockLoopIterable() 9 | let subject = QLoopIteratorContinueNil() 10 | subject.iterate(mockLoop) 11 | 12 | XCTAssertEqual(mockLoop.timesCalled_iteration, 1) 13 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 14 | } 15 | 16 | func test_iterate_2_times_shouldCallPerform_2_times() { 17 | let mockLoop = MockLoopIterable() 18 | let subject = QLoopIteratorContinueNil() 19 | subject.iterate(mockLoop) 20 | subject.iterate(mockLoop) 21 | 22 | XCTAssertEqual(mockLoop.timesCalled_iteration, 2) 23 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 24 | } 25 | 26 | func test_iterate_3_times_shouldCallPerform_3_times() { 27 | let mockLoop = MockLoopIterable() 28 | let subject = QLoopIteratorContinueNil() 29 | subject.iterate(mockLoop) 30 | subject.iterate(mockLoop) 31 | subject.iterate(mockLoop) 32 | 33 | XCTAssertEqual(mockLoop.timesCalled_iteration, 3) 34 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 35 | } 36 | 37 | func test_when_discontinue_becomes_true_then_it_should_stop() { 38 | let mockLoop = MockLoopIterable() 39 | let subject = QLoopIteratorContinueNil() 40 | subject.iterate(mockLoop) 41 | subject.iterate(mockLoop) 42 | mockLoop.discontinue = true 43 | subject.iterate(mockLoop) 44 | 45 | XCTAssertEqual(mockLoop.timesCalled_iteration, 2) 46 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Iterating/QLoopIteratorContinueOutputMaxTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoopIteratorContinueOutputMaxTests: XCTestCase { 6 | 7 | func test_iterate_max1_should_call_iterationFromLastOutput_0_times() { 8 | let mockLoop = MockLoopIterable() 9 | let subject = QLoopIteratorContinueOutputMax(1) 10 | subject.iterate(mockLoop) 11 | subject.iterate(mockLoop) 12 | subject.iterate(mockLoop) 13 | 14 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 15 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 16 | } 17 | 18 | func test_iterate_max2_should_call_iterationFromLastOutput_1_times() { 19 | let mockLoop = MockLoopIterable() 20 | let subject = QLoopIteratorContinueOutputMax(2) 21 | subject.iterate(mockLoop) 22 | subject.iterate(mockLoop) 23 | subject.iterate(mockLoop) 24 | 25 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 26 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 1) 27 | } 28 | 29 | func test_iterate_max3_should_call_iterationFromLastOutput_2_times() { 30 | let mockLoop = MockLoopIterable() 31 | let subject = QLoopIteratorContinueOutputMax(3) 32 | subject.iterate(mockLoop) 33 | subject.iterate(mockLoop) 34 | subject.iterate(mockLoop) 35 | 36 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 37 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 2) 38 | } 39 | 40 | func test_when_discontinue_becomes_true_then_it_should_stop() { 41 | let mockLoop = MockLoopIterable() 42 | let subject = QLoopIteratorContinueOutputMax(6) 43 | subject.iterate(mockLoop) 44 | subject.iterate(mockLoop) 45 | subject.iterate(mockLoop) 46 | mockLoop.discontinue = true 47 | subject.iterate(mockLoop) 48 | subject.iterate(mockLoop) 49 | 50 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 51 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 3) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Iterating/QLoopIteratorContinueOutputTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoopIteratorContinueOutputTests: XCTestCase { 6 | 7 | func test_iterate_1_times_should_call_iterationFromLastOutput_1_times() { 8 | let mockLoop = MockLoopIterable() 9 | let subject = QLoopIteratorContinueOutput() 10 | subject.iterate(mockLoop) 11 | 12 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 13 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 1) 14 | } 15 | 16 | func test_iterate_2_times_should_call_iterationFromLastOutput_2_times() { 17 | let mockLoop = MockLoopIterable() 18 | let subject = QLoopIteratorContinueOutput() 19 | subject.iterate(mockLoop) 20 | subject.iterate(mockLoop) 21 | 22 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 23 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 2) 24 | } 25 | 26 | func test_iterate_3_times_should_call_iterationFromLastOutput_3_times() { 27 | let mockLoop = MockLoopIterable() 28 | let subject = QLoopIteratorContinueOutput() 29 | subject.iterate(mockLoop) 30 | subject.iterate(mockLoop) 31 | subject.iterate(mockLoop) 32 | 33 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 34 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 3) 35 | } 36 | 37 | func test_when_discontinue_becomes_true_then_it_should_stop() { 38 | let mockLoop = MockLoopIterable() 39 | let subject = QLoopIteratorContinueOutput() 40 | subject.iterate(mockLoop) 41 | subject.iterate(mockLoop) 42 | mockLoop.discontinue = true 43 | subject.iterate(mockLoop) 44 | 45 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 46 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 2) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Iterating/QLoopIteratorSingleTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoopIteratorSingleTests: XCTestCase { 6 | 7 | func test_iteratorSingle_iterate_shouldNotCallIteration() { 8 | let mockLoop = MockLoopIterable() 9 | let subject = QLoopIteratorSingle() 10 | subject.iterate(mockLoop) 11 | 12 | XCTAssertEqual(mockLoop.timesCalled_iteration, 0) 13 | XCTAssertEqual(mockLoop.timesCalled_iterationFromLastOutput, 0) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/QLoopTests/QLAnchorTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | final class QLAnchorTests: XCTestCase { 6 | 7 | func test_using_default_constructor_can_still_set_onChange_later() { 8 | var received: Int = -1 9 | let expect = expectation(description: "should set") 10 | 11 | let subject = QLAnchor() 12 | subject.onChange(nil) 13 | subject.onError(QLCommon.Error.Unknown) 14 | subject.onChange = { received = $0!; expect.fulfill() } 15 | subject.value = 99 16 | 17 | wait(for: [expect], timeout: 8.0) 18 | XCTAssertEqual(received, 99) 19 | } 20 | 21 | func test_when_input_set_then_it_invokes_onChange() { 22 | var received: Int = -1 23 | let expect = expectation(description: "should set") 24 | let subject = QLAnchor(onChange: { received = $0!; expect.fulfill() }) 25 | 26 | subject.value = 99 27 | 28 | wait(for: [expect], timeout: 8.0) 29 | XCTAssertEqual(received, 99) 30 | } 31 | 32 | func test_when_input_set_is_result_error_then_it_invokes_onError_and_not_onChange() { 33 | var receivedInput: Result? = nil 34 | var receivedError: Error? = nil 35 | let expect = expectation(description: "should error") 36 | let subject = QLAnchor>(onChange: { receivedInput = $0; expect.fulfill() }, 37 | onError: { receivedError = $0; expect.fulfill() }) 38 | 39 | subject.value = .failure(QLCommon.Error.Unknown) 40 | 41 | wait(for: [expect], timeout: 8.0) 42 | XCTAssertNil(receivedInput) 43 | XCTAssertNotNil(receivedError) 44 | } 45 | 46 | func test_when_input_set_is_result_value_then_it_invokes_onChange_and_not_onError_like_normal() { 47 | var receivedInput: Result? = nil 48 | var receivedError: Error? = nil 49 | let expect = expectation(description: "should value") 50 | let subject = QLAnchor>(onChange: { receivedInput = $0; expect.fulfill() }, 51 | onError: { receivedError = $0; expect.fulfill() }) 52 | 53 | subject.value = .success(11) 54 | 55 | wait(for: [expect], timeout: 8.0) 56 | XCTAssertNotNil(receivedInput) 57 | XCTAssertNil(receivedError) 58 | } 59 | 60 | func test_when_error_set_then_it_invokes_onError() { 61 | var receivedError: Error? = nil 62 | let expect = expectation(description: "should set") 63 | let subject = QLAnchor(onChange: { _ in }, 64 | onError: { receivedError = $0; expect.fulfill() }) 65 | 66 | subject.error = QLCommon.Error.Unknown 67 | 68 | wait(for: [expect], timeout: 8.0) 69 | XCTAssert((receivedError as? QLCommon.Error) == QLCommon.Error.Unknown) 70 | } 71 | 72 | func test_when_error_set_nil_then_it_invokes_onError_with_ErrorThrownButNotSet() { 73 | var receivedError: Error? = nil 74 | let expect = expectation(description: "should set") 75 | let subject = QLAnchor(onChange: { _ in }, 76 | onError: { receivedError = $0; expect.fulfill() }) 77 | 78 | subject.error = nil 79 | 80 | wait(for: [expect], timeout: 8.0) 81 | XCTAssert((receivedError as? QLCommon.Error) == QLCommon.Error.ThrownButNotSet) 82 | } 83 | 84 | func test_given_it_has_early_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() { 85 | var receivedVal0: Int = -1 86 | var receivedVal1: Int = -1 87 | var receivedVal2: Int = -1 88 | let expectOriginal0 = expectation(description: "should dispatch value") 89 | let expectRepeater1 = expectation(description: "should echo value to repeater1") 90 | let expectRepeater2 = expectation(description: "should echo value to repeater2") 91 | let repeater1 = QLAnchor(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() }) 92 | let repeater2 = QLAnchor(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() }) 93 | let subject = QLAnchor(earlyRepeaters: repeater1, repeater2) 94 | XCTAssertEqual(subject.getRepeaters(timing: .early).count, 2) 95 | 96 | subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() } 97 | 98 | subject.value = 99 99 | 100 | wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0) 101 | XCTAssertEqual(receivedVal0, 99) 102 | XCTAssertEqual(receivedVal1, 99) 103 | XCTAssertEqual(receivedVal2, 99) 104 | } 105 | 106 | func test_given_it_has_late_repeaters_with_default_filter_when_input_set_then_it_echoes_to_them_as_well() { 107 | var receivedVal0: Int = -1 108 | var receivedVal1: Int = -1 109 | var receivedVal2: Int = -1 110 | let expectOriginal0 = expectation(description: "should dispatch value") 111 | let expectRepeater1 = expectation(description: "should echo value to repeater1") 112 | let expectRepeater2 = expectation(description: "should echo value to repeater2") 113 | let repeater1 = QLAnchor(onChange: { receivedVal1 = $0!; expectRepeater1.fulfill() }) 114 | let repeater2 = QLAnchor(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() }) 115 | let subject = QLAnchor(lateRepeaters: repeater1, repeater2) 116 | XCTAssertEqual(subject.getRepeaters(timing: .late).count, 2) 117 | 118 | subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() } 119 | 120 | subject.value = 99 121 | 122 | wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0) 123 | XCTAssertEqual(receivedVal0, 99) 124 | XCTAssertEqual(receivedVal1, 99) 125 | XCTAssertEqual(receivedVal2, 99) 126 | } 127 | 128 | func test_given_it_has_repeaters_with_default_filter_when_error_set_then_it_echoes_to_them_as_well() { 129 | var receivedErr0: Error? = nil 130 | var receivedErr1: Error? = nil 131 | var receivedErr2: Error? = nil 132 | let expectOriginal0 = expectation(description: "should dispatch error") 133 | let expectRepeater1 = expectation(description: "should echo error to repeater1") 134 | let expectRepeater2 = expectation(description: "should echo error to repeater2") 135 | let repeater1 = QLAnchor(onChange: { _ in }, 136 | onError: { receivedErr1 = $0; expectRepeater1.fulfill() }) 137 | let repeater2 = QLAnchor(onChange: { _ in }, 138 | onError: { receivedErr2 = $0; expectRepeater2.fulfill() }) 139 | let subject = QLAnchor(earlyRepeaters: [repeater1], lateRepeaters: [repeater2]) 140 | XCTAssertEqual(subject.getRepeaters(timing: .early).count, 1) 141 | XCTAssertEqual(subject.getRepeaters(timing: .late).count, 1) 142 | 143 | subject.onError = { receivedErr0 = $0; expectOriginal0.fulfill() } 144 | 145 | subject.error = QLCommon.Error.Unknown 146 | 147 | wait(for: [expectOriginal0, expectRepeater1, expectRepeater2], timeout: 8.0) 148 | XCTAssertNotNil(receivedErr0) 149 | XCTAssertNotNil(receivedErr1) 150 | XCTAssertNotNil(receivedErr2) 151 | } 152 | 153 | func test_given_it_has_earlyRepeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() { 154 | var receivedVal0: Int = -1 155 | var receivedVal1: Int = -1 156 | var receivedVal2: Int = -1 157 | let expectOriginal0 = expectation(description: "should dispatch value") 158 | let expectRepeater2 = expectation(description: "should echo value to repeater2") 159 | let repeater1 = QLAnchor(onChange: { receivedVal1 = $0! }) 160 | let repeater2 = QLAnchor(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() }) 161 | 162 | let subject = QLAnchor( 163 | echoFilter: ({ val, repeater in 164 | return (val == 11 && repeater === repeater1) 165 | || (val == 22 && repeater === repeater2) 166 | }), 167 | earlyRepeaters: repeater1, repeater2 168 | ) 169 | 170 | subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() } 171 | 172 | subject.value = 22 173 | 174 | wait(for: [expectOriginal0, expectRepeater2], timeout: 8.0) 175 | XCTAssertEqual(receivedVal0, 22) 176 | XCTAssertEqual(receivedVal1, -1) 177 | XCTAssertEqual(receivedVal2, 22) 178 | } 179 | 180 | func test_given_it_has_lateRepeaters_with_custom_filter_when_input_set_then_it_dispatches_then_echoes_to_them_conditionally() { 181 | var receivedVal0: Int = -1 182 | var receivedVal1: Int = -1 183 | var receivedVal2: Int = -1 184 | let expectOriginal0 = expectation(description: "should dispatch value") 185 | let expectRepeater2 = expectation(description: "should echo value to repeater2") 186 | let repeater1 = QLAnchor(onChange: { receivedVal1 = $0! }) 187 | let repeater2 = QLAnchor(onChange: { receivedVal2 = $0!; expectRepeater2.fulfill() }) 188 | 189 | let subject = QLAnchor( 190 | echoFilter: ({ val, repeater in 191 | return (val == 11 && repeater === repeater1) 192 | || (val == 22 && repeater === repeater2) 193 | }), 194 | lateRepeaters: repeater1, repeater2 195 | ) 196 | 197 | subject.onChange = { receivedVal0 = $0!; expectOriginal0.fulfill() } 198 | 199 | subject.value = 22 200 | 201 | wait(for: [expectOriginal0, expectRepeater2], timeout: 8.0) 202 | XCTAssertEqual(receivedVal0, 22) 203 | XCTAssertEqual(receivedVal1, -1) 204 | XCTAssertEqual(receivedVal2, 22) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Tests/QLoopTests/QLPathTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLPathTests: XCTestCase { 6 | 7 | func test_givenTwoCompatibleSegments_whenTypelesslyLinked_returnsFirst_linkedToSecond() { 8 | let seg1 = QLSerialSegment(1, MockOp.IntToStr()) 9 | let seg2 = QLSerialSegment(2, MockOp.AddToStr("++")) 10 | 11 | XCTAssertNotNil(seg1.linked(to: seg2)) 12 | XCTAssert(seg1.output === seg2.input) 13 | } 14 | 15 | func test_givenTwoIncompatibleSegments_whenTypelesslyLinked_returnsNil() { 16 | let seg1 = QLSerialSegment(1, MockOp.IntToStr()) 17 | let seg2 = QLSerialSegment(2, MockOp.IntToStr()) 18 | 19 | XCTAssertNil(seg1.linked(to: seg2)) 20 | } 21 | 22 | func test_givenNoSegments_whenConstructingPath_returnsNil() { 23 | XCTAssertNil(QLPath()) 24 | } 25 | 26 | func test_givenSeveralCompatibleSegments_whenConstructingPath_returnsThemAllLinkedUp() { 27 | let seg1 = QLSerialSegment(1, MockOp.AddToStr("++")) 28 | let seg2 = QLSerialSegment(2, MockOp.AddToStr("--")) 29 | let seg3 = QLSerialSegment(3, MockOp.AddToStr("**")) 30 | let seg4 = QLSerialSegment(4, MockOp.AddToStr("//")) 31 | 32 | let path = QLPath(seg1, seg2, seg3, seg4) 33 | 34 | XCTAssertNotNil(path) 35 | XCTAssert(seg1.output === seg2.input) 36 | XCTAssert(seg2.output === seg3.input) 37 | XCTAssert(seg3.output === seg4.input) 38 | } 39 | 40 | func test_givenIncompatibleCompatibleSegment_whenConstructingPath_returnsNil() { 41 | XCTAssertNil(QLPath( 42 | QLSerialSegment(1, MockOp.AddToStr("++")), 43 | QLSerialSegment(2, MockOp.AddToInt(9998)), 44 | QLSerialSegment(3, MockOp.AddToStr("**")))) 45 | } 46 | 47 | func test_path_whenFindingSegmentsByOperationId_succeeds() { 48 | let seg1 = QLSerialSegment(1, MockOp.AddToStr("A")) 49 | let seg2 = QLSerialSegment(2, MockOp.AddToStr("B")) 50 | let seg3 = QLParallelSegment( 51 | [3.1:MockOp.AddToStr("C"), 52 | 3.2:MockOp.AddToStr("D")]) 53 | let seg4 = QLSerialSegment(4, MockOp.AddToStr("E")) 54 | let path = QLPath(seg1, seg2, seg3, seg4)! 55 | 56 | XCTAssertEqual(path.findSegments(with: 1).count, 1) 57 | XCTAssertEqual(path.findSegments(with: 2).count, 1) 58 | XCTAssertEqual(path.findSegments(with: 3.1).count, 1) 59 | XCTAssertEqual(path.findSegments(with: 3.2).count, 1) 60 | XCTAssertEqual(path.findSegments(with: 4).count, 1) 61 | } 62 | 63 | func test_operation_path() { 64 | let seg1 = QLSerialSegment("animal", MockOp.AddToStr("!")) 65 | let seg2 = QLSerialSegment("vegetable", MockOp.AddToStr("@")) 66 | let seg3 = QLSerialSegment("mineral", MockOp.AddToStr("#")) 67 | let path = QLPath(seg1, seg2, seg3)! 68 | let opPath = path.operationPath() 69 | 70 | XCTAssertEqual(opPath.map {$0.0}, [["animal"],["vegetable"],["mineral"]]) 71 | XCTAssertEqual(opPath.map {$0.1}, [false,false,false]) 72 | } 73 | 74 | func test_describe_operation_path() { 75 | let seg1 = QLSerialSegment("animal", MockOp.AddToStr("!")) 76 | let seg2 = QLSerialSegment("vegetable", MockOp.AddToStr("@")) 77 | let seg3 = QLSerialSegment("mineral", MockOp.AddToStr("#")) 78 | let path = QLPath(seg1, seg2, seg3)! 79 | let opPath = path.describeOperationPath() 80 | 81 | XCTAssertEqual(opPath, "{animal}-{vegetable}-{mineral}") 82 | } 83 | 84 | func test_givenTypeErasedPathBoundToLoop_objectReceivesFinalValue() { 85 | let expect = expectation(description: "receive final value") 86 | let mockComponent = MockPhoneComponent(expect) 87 | let loopPath = QLPath( 88 | QLSerialSegment(1, MockOp.VoidToInt(5)), 89 | QLSerialSegment(2, MockOp.AddToInt(5)), 90 | QLSerialSegment(3, MockOp.IntToStr()), 91 | QLSerialSegment(4, MockOp.AddToStr("Z")))! 92 | mockComponent.phoneDataLoop.bind(path: loopPath) 93 | 94 | mockComponent.userAction() 95 | 96 | wait(for: [expect], timeout: 6.0) 97 | XCTAssertEqual(mockComponent.userPhoneNumberField, "10Z") 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/QLoopTests/QLoop+ConvenienceInitTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoop_ConvenienceInitTests: XCTestCase { 6 | 7 | func test_qloop_constructor() { 8 | let loop = QLoop() 9 | XCTAssert(loop.iterator is QLoopIteratorSingle) 10 | XCTAssertFalse(loop.discontinue) 11 | } 12 | 13 | func test_qloop_constructor_iterator() { 14 | let loop = QLoop(iterator: QLoopIteratorContinueNil()) 15 | XCTAssert(loop.iterator is QLoopIteratorContinueNil) 16 | XCTAssertFalse(loop.discontinue) 17 | } 18 | 19 | func test_qloop_constructor_onChange() { 20 | var onChangeCorrect: Bool = false 21 | let loop = QLoop(onChange: ({ _ in onChangeCorrect = true })) 22 | XCTAssert(loop.iterator is QLoopIteratorSingle) 23 | XCTAssertFalse(loop.discontinue) 24 | loop.onChange(nil) 25 | XCTAssertTrue(onChangeCorrect) 26 | } 27 | 28 | func test_qloop_constructor_onChange_onError() { 29 | var onChangeCorrect: Bool = false 30 | var onErrorCorrect: Bool = false 31 | let loop = QLoop( 32 | onChange: ({ _ in onChangeCorrect = true }), 33 | onError: ({ _ in onErrorCorrect = true }) 34 | ) 35 | XCTAssert(loop.iterator is QLoopIteratorSingle) 36 | XCTAssertFalse(loop.discontinue) 37 | loop.onChange(nil) 38 | loop.onError(QLCommon.Error.ThrownButNotSet) 39 | XCTAssertTrue(onChangeCorrect) 40 | XCTAssertTrue(onErrorCorrect) 41 | } 42 | 43 | func test_qloop_constructor_iterator_onChange() { 44 | var onChangeCorrect: Bool = false 45 | let loop = QLoop( 46 | iterator: QLoopIteratorContinueNil(), 47 | onChange: ({ _ in onChangeCorrect = true }) 48 | ) 49 | XCTAssert(loop.iterator is QLoopIteratorContinueNil) 50 | XCTAssertFalse(loop.discontinue) 51 | loop.onChange(nil) 52 | XCTAssertTrue(onChangeCorrect) 53 | } 54 | 55 | func test_qloop_constructor_iterator_onError() { 56 | var onErrorCorrect: Bool = false 57 | let loop = QLoop( 58 | iterator: QLoopIteratorContinueOutputMax(1), 59 | onError: ({ _ in onErrorCorrect = true }) 60 | ) 61 | XCTAssert(loop.iterator is QLoopIteratorContinueOutputMax) 62 | XCTAssertFalse(loop.discontinue) 63 | loop.onError(QLCommon.Error.ThrownButNotSet) 64 | XCTAssertTrue(onErrorCorrect) 65 | } 66 | 67 | func test_qloop_constructor_iterator_onChange_onError() { 68 | var onChangeCorrect: Bool = false 69 | var onErrorCorrect: Bool = false 70 | let loop = QLoop( 71 | iterator: QLoopIteratorContinueOutputMax(1), 72 | onChange: ({ _ in onChangeCorrect = true }), 73 | onError: ({ _ in onErrorCorrect = true }) 74 | ) 75 | XCTAssert(loop.iterator is QLoopIteratorContinueOutputMax) 76 | XCTAssertFalse(loop.discontinue) 77 | loop.onChange(nil) 78 | loop.onError(QLCommon.Error.ThrownButNotSet) 79 | XCTAssertTrue(onChangeCorrect) 80 | XCTAssertTrue(onErrorCorrect) 81 | } 82 | 83 | func test_qloop_constructor_onFinal() { 84 | var onFinalCorrect: Bool = false 85 | let loop = QLoop( 86 | onFinal: ({ _ in onFinalCorrect = true }) 87 | ) 88 | XCTAssert(loop.iterator is QLoopIteratorSingle) 89 | XCTAssertFalse(loop.discontinue) 90 | loop.onFinal(nil) 91 | XCTAssertTrue(onFinalCorrect) 92 | } 93 | 94 | func test_qloop_constructor_iterator_onFinal() { 95 | var onFinalCorrect: Bool = false 96 | let loop = QLoop( 97 | iterator: QLoopIteratorContinueOutput(), 98 | onFinal: ({ _ in onFinalCorrect = true }) 99 | ) 100 | XCTAssert(loop.iterator is QLoopIteratorContinueOutput) 101 | XCTAssertFalse(loop.discontinue) 102 | loop.onFinal(nil) 103 | XCTAssertTrue(onFinalCorrect) 104 | } 105 | 106 | func test_qloop_constructor_iterator_onFinal_onChange() { 107 | var onFinalCorrect: Bool = false 108 | var onChangeCorrect: Bool = false 109 | let loop = QLoop( 110 | iterator: QLoopIteratorContinueOutput(), 111 | onFinal: ({ _ in onFinalCorrect = true }), 112 | onChange: ({ _ in onChangeCorrect = true }) 113 | ) 114 | XCTAssert(loop.iterator is QLoopIteratorContinueOutput) 115 | XCTAssertFalse(loop.discontinue) 116 | loop.onFinal(nil) 117 | loop.onChange(nil) 118 | XCTAssertTrue(onFinalCorrect) 119 | XCTAssertTrue(onChangeCorrect) 120 | } 121 | 122 | func test_qloop_constructor_iterator_onFinal_onError() { 123 | var onFinalCorrect: Bool = false 124 | var onErrorCorrect: Bool = false 125 | let loop = QLoop( 126 | iterator: QLoopIteratorContinueOutput(), 127 | onFinal: ({ _ in onFinalCorrect = true }), 128 | onError: ({ _ in onErrorCorrect = true }) 129 | ) 130 | XCTAssert(loop.iterator is QLoopIteratorContinueOutput) 131 | XCTAssertFalse(loop.discontinue) 132 | loop.onFinal(nil) 133 | loop.onError(QLCommon.Error.ThrownButNotSet) 134 | XCTAssertTrue(onFinalCorrect) 135 | XCTAssertTrue(onErrorCorrect) 136 | } 137 | 138 | func test_qloop_constructor_iterator_onFinal_onChange_onError() { 139 | var onFinalCorrect: Bool = false 140 | var onChangeCorrect: Bool = false 141 | var onErrorCorrect: Bool = false 142 | let loop = QLoop( 143 | iterator: QLoopIteratorContinueOutput(), 144 | onFinal: ({ _ in onFinalCorrect = true }), 145 | onChange: ({ _ in onChangeCorrect = true }), 146 | onError: ({ _ in onErrorCorrect = true }) 147 | ) 148 | XCTAssert(loop.iterator is QLoopIteratorContinueOutput) 149 | XCTAssertFalse(loop.discontinue) 150 | loop.onFinal(nil) 151 | loop.onChange(nil) 152 | loop.onError(QLCommon.Error.ThrownButNotSet) 153 | XCTAssertTrue(onFinalCorrect) 154 | XCTAssertTrue(onChangeCorrect) 155 | XCTAssertTrue(onErrorCorrect) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /Tests/QLoopTests/QLoopTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLoopTests: XCTestCase { 6 | 7 | func test_calling_perform_with_non_resettable_iterator_does_not_reset_the_iterator() { 8 | let mockIterator = MockLoopIterator() 9 | let loop = QLoop(iterator: mockIterator) 10 | loop.perform() 11 | XCTAssertFalse(mockIterator.didCall_reset) 12 | } 13 | 14 | func test_calling_perform_with_resettable_iterator_resets_the_iterator() { 15 | let mockIterator = MockLoopResettableIterator() 16 | let loop = QLoop(iterator: mockIterator) 17 | loop.perform() 18 | XCTAssertTrue(mockIterator.didCall_reset) 19 | } 20 | 21 | func test_calling_perform_with_input_resets_the_iterator() { 22 | let mockIterator = MockLoopResettableIterator() 23 | let loop = QLoop(iterator: mockIterator) 24 | loop.perform(6) 25 | XCTAssertTrue(mockIterator.didCall_reset) 26 | } 27 | 28 | func test_givenLoopWithSegments_outputtingNil_objectReceivesFinalNil() { 29 | let expect = expectation(description: "shouldComplete receive final nil") 30 | let mockComponent = MockPhoneComponent(expect) 31 | mockComponent.userPhoneNumberField = "...and then" 32 | mockComponent.phoneDataLoop.input = 33 | QLSerialSegment( 34 | 1, MockOp.VoidToStr(nil), 35 | output: mockComponent.phoneDataLoop.output) 36 | .input 37 | 38 | mockComponent.userAction() 39 | 40 | wait(for: [expect], timeout: 8.0) 41 | XCTAssertEqual(mockComponent.userPhoneNumberField, "") 42 | } 43 | 44 | func test_givenLoopWithSegments_objectReceivesFinalValue() { 45 | let expect = expectation(description: "shouldComplete receive final value") 46 | let mockComponent = MockPhoneComponent(expect) 47 | 48 | mockComponent.phoneDataLoop.input = 49 | QLSerialSegment( 50 | 1, MockOp.VoidToStr("(210) "), outputSegment: 51 | QLSerialSegment( 52 | 2, MockOp.AddToStr("555-"), outputSegment: 53 | QLSerialSegment( 54 | 3, MockOp.AddToStr("1212"), output: 55 | mockComponent.phoneDataLoop.output))).input 56 | 57 | mockComponent.userAction() 58 | 59 | wait(for: [expect], timeout: 8.0) 60 | XCTAssertEqual(mockComponent.userPhoneNumberField, "(210) 555-1212") 61 | } 62 | 63 | func test_loop_whenFindingSegmentsByOperationId_succeeds() { 64 | let seg1 = QLSerialSegment(1, MockOp.AddToStr("A")) 65 | let seg2 = QLSerialSegment(2, MockOp.AddToStr("B")) 66 | let seg3 = QLParallelSegment( 67 | [3:MockOp.AddToStr("C")]) 68 | let path = QLPath(seg1, seg2, seg3)! 69 | let loop = QLoop() 70 | loop.bind(path: path) 71 | 72 | XCTAssertEqual(loop.findSegments(with: 1).count, 1) 73 | XCTAssertEqual(loop.findSegments(with: 2).count, 1) 74 | XCTAssertEqual(loop.findSegments(with: 3).count, 1) 75 | } 76 | 77 | func test_givenLoopWithSegments_withIteratorCountNil_losesValueBetweenIterations() { 78 | let expect = expectation(description: "shouldComplete loses value") 79 | let mockComponent = MockProgressComponent() 80 | mockComponent.progressDataLoop.input = 81 | QLSerialSegment( 82 | 1, MockOp.AddToStr("#"), 83 | output: mockComponent.progressDataLoop.output).input 84 | mockComponent.progressDataLoop.iterator = QLoopIteratorContinueNilMax(2) 85 | mockComponent.progressDataLoop.onFinal = ({ _ in 86 | expect.fulfill() 87 | }) 88 | 89 | mockComponent.userAction() 90 | 91 | wait(for: [expect], timeout: 8.0) 92 | XCTAssertEqual(mockComponent.progressField, "#") 93 | } 94 | 95 | func test_givenLoopWithSegments_withIteratorCountOutput_accumulatesValueBetweenIterations() { 96 | let expect = expectation(description: "shouldComplete accumulates value") 97 | let mockComponent = MockProgressComponent() 98 | mockComponent.progressDataLoop.input = 99 | QLSerialSegment( 100 | 1, MockOp.AddToStr("#"), 101 | output: mockComponent.progressDataLoop.output).input 102 | mockComponent.progressDataLoop.iterator = QLoopIteratorContinueOutputMax(3) 103 | mockComponent.progressDataLoop.onFinal = ({ _ in 104 | expect.fulfill() 105 | }) 106 | 107 | mockComponent.userAction() 108 | 109 | wait(for: [expect], timeout: 8.0) 110 | XCTAssertEqual(mockComponent.progressField, "###") 111 | } 112 | 113 | func test_givenShouldResumeFalse_whenErrorIsReceived_itPropagatesErrorToOutputAnchor_itDiscontinues() { 114 | let expect = expectation(description: "shouldComplete it propagates error and discontinues") 115 | let mockComponent = MockProgressComponent(expect) 116 | mockComponent.progressDataLoop.iterator = MockLoopIterator() 117 | mockComponent.progressDataLoop.input = 118 | QLSerialSegment( 119 | 1, MockOp.StrThrowsError(QLCommon.Error.Unknown), 120 | output: mockComponent.progressDataLoop.output).input 121 | 122 | mockComponent.userAction() 123 | 124 | wait(for: [expect], timeout: 8.0) 125 | XCTAssert((mockComponent.progressDataLoop.output.error as? QLCommon.Error) == QLCommon.Error.Unknown) 126 | XCTAssertNil(mockComponent.progressDataLoop.output.value) 127 | XCTAssertTrue(mockComponent.progressDataLoop.discontinue) 128 | } 129 | 130 | func test_givenShouldResumeTrue_whenInputErrorIsReceived_itPropagatesErrorToOutput_itContinues() { 131 | let expect = expectation(description: "shouldComplete it continues") 132 | let mockComponent = MockProgressComponent(expect) 133 | mockComponent.progressDataLoop.iterator = MockLoopIterator() 134 | mockComponent.progressDataLoop.shouldResume = true 135 | mockComponent.progressDataLoop.input = 136 | QLss(1, MockOp.StrThrowsError(QLCommon.Error.Unknown), 137 | output: mockComponent.progressDataLoop.output).input 138 | 139 | mockComponent.userAction() 140 | 141 | wait(for: [expect], timeout: 8.0) 142 | XCTAssert((mockComponent.progressDataLoop.output.error as? QLCommon.Error) == QLCommon.Error.Unknown) 143 | XCTAssertNotNil(mockComponent.progressDataLoop.output.error) 144 | XCTAssertFalse(mockComponent.progressDataLoop.discontinue) 145 | } 146 | 147 | func test_operation_path_without_segments_should_be_empty() { 148 | let loop = QLoop() 149 | let opPath = loop.operationPath() 150 | 151 | XCTAssertTrue(opPath.isEmpty) 152 | } 153 | 154 | func test_operation_path_with_segments_should_have_correct_shape() { 155 | let seg1 = QLSerialSegment("animal", MockOp.AddToStr("!")) 156 | let seg2 = QLSerialSegment("vegetable", MockOp.AddToStr("@")) 157 | let seg3 = QLSerialSegment("mineral", MockOp.AddToStr("#")) 158 | let loop = QLoop() 159 | loop.bind(path: QLPath(seg1, seg2, seg3)!) 160 | let opPath = loop.operationPath() 161 | 162 | XCTAssertEqual(opPath.map {$0.0}, [["animal"],["vegetable"],["mineral"]]) 163 | XCTAssertEqual(opPath.map {$0.1}, [false,false,false]) 164 | } 165 | 166 | func test_describe_operation_path() { 167 | let seg1 = QLSerialSegment("animal", MockOp.AddToStr("!")) 168 | let seg2 = QLSerialSegment("vegetable", MockOp.AddToStr("@")) 169 | let seg3 = QLSerialSegment("mineral", MockOp.AddToStr("#")) 170 | let loop = QLoop() 171 | loop.bind(path: QLPath(seg1, seg2, seg3)!) 172 | let opPath = loop.describeOperationPath() 173 | 174 | XCTAssertEqual(opPath, "{animal}-{vegetable}-{mineral}") 175 | } 176 | 177 | func test_describe_operation_path_with_error_handler() { 178 | let seg1 = QLSerialSegment("animal", MockOp.AddToStr("!")) 179 | let seg2 = QLSerialSegment("vegetable", MockOp.AddToStr("@"), 180 | errorHandler: MockOp.StrErrorHandler(false)) 181 | let seg3 = QLSerialSegment("mineral", MockOp.AddToStr("#")) 182 | let loop = QLoop() 183 | loop.bind(path: QLPath(seg1, seg2, seg3)!) 184 | let opPath = loop.describeOperationPath() 185 | 186 | XCTAssertEqual(opPath, "{animal}-{vegetable*}-{mineral}") 187 | } 188 | 189 | func test_when_destroy_is_called_then_it_should_have_no_path() { 190 | let loop = QLoop() 191 | loop.bind(path: QLPath( 192 | QLSerialSegment("who", MockOp.AddToStr("me")), 193 | QLSerialSegment("why", MockOp.AddToStr("yes")), 194 | QLSerialSegment("wrd", MockOp.AddToStr("huh")))!) 195 | XCTAssertEqual(loop.describeOperationPath(), "{who}-{why}-{wrd}") 196 | loop.destroy() 197 | XCTAssertEqual(loop.describeOperationPath(), "") 198 | } 199 | } 200 | 201 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Segment/QLParallelSegmentTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | import Dispatch 5 | 6 | class QLParallelSegmentTests: XCTestCase { 7 | 8 | func test_reveals_its_operation_ids() { 9 | 10 | let subject = QLParallelSegment( 11 | [0xAB:MockOp.VoidToInt(), 12 | 0xCD:MockOp.VoidToInt()], 13 | combiner: nil) 14 | 15 | XCTAssert(subject.operationIds.contains(0xAB)) 16 | XCTAssert(subject.operationIds.contains(0xCD)) 17 | } 18 | 19 | func test_basicSegmentWithOutputAnchor_whenInputSet_itCallsCompletionWithoutResult() { 20 | let expect = expectation(description: "shouldComplete") 21 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 22 | let subject = QLParallelSegment( 23 | ["genStr":MockOp.VoidToStr()], 24 | combiner: nil, 25 | output: finalAnchor) 26 | 27 | subject.input.value = nil 28 | 29 | wait(for: [expect], timeout: 8.0) 30 | XCTAssertTrue(captured.didHappen) 31 | XCTAssertNil(captured.value) 32 | } 33 | 34 | func test_givenIntToStringAndOutputAnchor_whenInputSet_itCallsCompletionWithResult() { 35 | let expect = expectation(description: "shouldComplete") 36 | let (captured, _, finalAnchor) = SpyAnchor<[String]>().CapturingAnchor(expect: expect) 37 | let subject = QLParallelSegment( 38 | ["numStr":MockOp.IntToStr()], 39 | combiner: (([] as [String]), 40 | { r,n in r! + [n.1 as? String ?? ""] }), 41 | output: finalAnchor) 42 | 43 | subject.input.value = 3 44 | 45 | wait(for: [expect], timeout: 8.0) 46 | XCTAssertTrue(captured.didHappen) 47 | XCTAssertEqual(captured.value, ["3"]) 48 | } 49 | 50 | func test_whenInputSet_repeatedly_itShouldStillOutputResults() { 51 | let expect = expectation(description: "shouldComplete") 52 | let captured = Captured() 53 | let iterationsExpected: Int = 32 54 | var iterationsCounted: Int = 0 55 | let finalAnchor = QLAnchor(onChange: ({ 56 | captured.capture($0) 57 | iterationsCounted += 1 58 | if (iterationsCounted == iterationsExpected) { 59 | expect.fulfill() 60 | } 61 | })) 62 | 63 | let subject = QLParallelSegment( 64 | [1:MockOp.AddToInt(32), 65 | 2:MockOp.AddToInt(24)], 66 | combiner: ("", { r, n in r! + "\(n.1 as? Int ?? 0)" }), 67 | errorHandler: nil) 68 | subject.output = finalAnchor 69 | 70 | for i in 0..().CapturingAnchor(expect: expect) 83 | let subject = QLParallelSegment( 84 | ["numStr":MockOp.IntToStr()], 85 | combiner: nil, 86 | outputSegment: 87 | QLParallelSegment( 88 | ["addStr":MockOp.AddToStr(" eleven")], 89 | combiner: nil, 90 | output: finalAnchor)) 91 | 92 | subject.input.value = 7 93 | 94 | wait(for: [expect], timeout: 8.0) 95 | XCTAssertTrue(captured.didHappen) 96 | XCTAssertEqual(captured.value, "7 eleven") 97 | } 98 | 99 | func test_givenTwoSegments_oneWithParallelOperationsAndCombiner_whenInputSet_itReduces_andCallsEndCompletionWithCorrectResult() { 100 | let expect = expectation(description: "shouldComplete") 101 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 102 | let subject = QLParallelSegment( 103 | ["add5":MockOp.AddToInt(5), 104 | "add4":MockOp.AddToInt(4)], 105 | combiner: (0, { $0! + ($1.1 as? Int ?? 0) }), 106 | outputSegment: QLSerialSegment("add10", MockOp.AddToInt(10), 107 | output: finalAnchor)) 108 | 109 | subject.input.value = 10 110 | 111 | wait(for: [expect], timeout: 8.0) 112 | XCTAssertTrue(captured.didHappen) 113 | XCTAssertEqual(captured.value, 39) 114 | } 115 | 116 | func test_givenTwoSegments_oneWithParallelOperationsAndCombiner_withAdditionalType_whenInputSet_itReduces_andCallsEndCompletionWithCorrectResult() { 117 | let expect = expectation(description: "shouldComplete") 118 | let (captured, _, finalAnchor) = SpyAnchor<[Int]>().CapturingAnchor(expect: expect) 119 | let subject = QLParallelSegment( 120 | ["add8":MockOp.AddToInt(8), 121 | "add4":MockOp.AddToInt(4)], 122 | combiner: ([] as [Int], { $0! + ([$1.1 as? Int ?? 0]) }), 123 | output: finalAnchor) 124 | 125 | subject.input.value = 10 126 | 127 | wait(for: [expect], timeout: 8.0) 128 | XCTAssertTrue(captured.didHappen) 129 | XCTAssertEqual(captured.value?.sorted(), [14,18]) 130 | } 131 | 132 | func test_whenErrorThrown_itPropagatesErrorToOutputAnchor() { 133 | let expect = expectation(description: "shouldComplete") 134 | let (captured, capturedError, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 135 | let subject = QLParallelSegment( 136 | ["numNum":MockOp.IntThrowsError(QLCommon.Error.Unknown)], 137 | combiner: nil, 138 | output: finalAnchor) 139 | 140 | subject.input.value = 404 141 | 142 | wait(for: [expect], timeout: 8.0) 143 | XCTAssert((capturedError.value as? QLCommon.Error) == QLCommon.Error.Unknown) 144 | XCTAssertNil(finalAnchor.value) 145 | XCTAssertFalse(captured.didHappen) 146 | XCTAssertNotEqual(captured.value, 404) 147 | } 148 | 149 | func test_whenInputErrorIsReceived_itPropagatesErrorToOutputAnchor() { 150 | let expect = expectation(description: "shouldComplete") 151 | let (captured, capturedError, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 152 | let subject = QLParallelSegment( 153 | ["numNum":MockOp.AddToInt(5)], 154 | combiner: nil, 155 | output: finalAnchor) 156 | 157 | subject.input.error = QLCommon.Error.Unknown 158 | 159 | wait(for: [expect], timeout: 8.0) 160 | XCTAssert((capturedError.value as? QLCommon.Error) == QLCommon.Error.Unknown) 161 | XCTAssertNil(finalAnchor.value) 162 | XCTAssertFalse(captured.didHappen) 163 | XCTAssertNotEqual(captured.value, 404) 164 | } 165 | 166 | func test_givenParallelSegment_withErrorHandlerSet_whenErrorThrown_itHandles() { 167 | let expect = expectation(description: "shouldComplete") 168 | let (captured, _, output) = SpyAnchor().CapturingAnchor(expect: expect) 169 | var err: Error? = nil 170 | let handler: QLParallelSegment.ErrorHandler = { 171 | error, completion, errCompletion in 172 | err = error 173 | completion(0) 174 | } 175 | 176 | let seg1 = QLParallelSegment( 177 | [1:MockOp.IntThrowsError(QLCommon.Error.Unknown)], 178 | errorHandler: handler) 179 | seg1.output = output 180 | 181 | seg1.input.value = 4 182 | 183 | wait(for: [expect], timeout: 8.0) 184 | XCTAssertNotNil(err) 185 | XCTAssertTrue(captured.didHappen) 186 | } 187 | 188 | func test_givenParallelSegment_withErrorHandler_whenChoosesErrorPath_outputGetsError() { 189 | let expect = expectation(description: "shouldComplete") 190 | let (captured, _, output) = SpyAnchor().CapturingAnchor(expect: expect) 191 | var err: Error? = nil 192 | let handler: QLParallelSegment.ErrorHandler = { 193 | error, _, errCompletion in 194 | err = error 195 | errCompletion(error) 196 | } 197 | 198 | let seg1 = QLParallelSegment( 199 | [1:MockOp.IntThrowsError(QLCommon.Error.Unknown)], 200 | combiner: nil, 201 | errorHandler: handler, 202 | output: output) 203 | 204 | seg1.input.value = 4 205 | 206 | wait(for: [expect], timeout: 8.0) 207 | XCTAssertNotNil(err) 208 | XCTAssertFalse(captured.didHappen) 209 | XCTAssertNotNil(output.error) 210 | XCTAssertEqual(output.error as? QLCommon.Error, QLCommon.Error.Unknown) 211 | } 212 | 213 | func test_operation_path_when_single() { 214 | let output = QLAnchor() 215 | let _ = QLParallelSegment( 216 | ["plus":MockOp.AddToStr("plus"), 217 | "minus":MockOp.AddToStr("minus")], 218 | combiner: nil, 219 | output: output) 220 | 221 | let last = output.inputSegment 222 | let opPath = last?.operationPath() 223 | 224 | XCTAssertNotNil(opPath?.first?.0.first {$0 as? String == "plus"}) 225 | XCTAssertNotNil(opPath?.first?.0.first {$0 as? String == "minus"}) 226 | } 227 | 228 | func test_operation_path_when_multiple() { 229 | let output = QLAnchor() 230 | let _ = QLSerialSegment( 231 | 10, MockOp.VoidToStr("One"), outputSegment: 232 | QLParallelSegment( 233 | ["plus":MockOp.AddToStr("plus"), 234 | "minus":MockOp.AddToStr("minus")], 235 | combiner: nil, 236 | outputSegment: 237 | QLSerialSegment( 238 | 12, MockOp.AddToStr("Three"), 239 | output: output))) 240 | 241 | let last = output.inputSegment 242 | let opPath = last?.operationPath() 243 | 244 | XCTAssertEqual(opPath?[0].0, [10]) 245 | XCTAssertNotNil(opPath?[1].0.first {$0 as? String == "plus"}) 246 | XCTAssertNotNil(opPath?[1].0.first {$0 as? String == "minus"}) 247 | XCTAssertEqual(opPath?[2].0, [12]) 248 | } 249 | 250 | func test_describe_operation_path_when_multiple() { 251 | let output = QLAnchor() 252 | let _ = QLSerialSegment( 253 | 0x0A, MockOp.VoidToStr("One"), outputSegment: 254 | 255 | QLParallelSegment( 256 | ["plus":MockOp.AddToStr("plus"), 257 | "minus":MockOp.AddToStr("minus")], 258 | combiner: nil, errorHandler: nil, 259 | outputSegment: QLSerialSegment( 260 | 0x0C, MockOp.AddToStr("Three"), 261 | 262 | output: output))) 263 | 264 | let last = output.inputSegment 265 | let opPath = last?.describeOperationPath() 266 | 267 | XCTAssertEqual(opPath, "{10}-{minus:plus}-{12}") 268 | } 269 | 270 | func test_when_starting_with_multiple_operation_queues_it_should_launch_operations_accordingly() { 271 | let expect = expectation(description: "expect all operations completed using correct queues") 272 | let utilQueue = DispatchQueue.global(qos: DispatchQoS.QoSClass.userInitiated) 273 | let mainQueue = DispatchQueue.main 274 | let output = QLAnchor<[AnyHashable:Bool]>( 275 | onChange: ({ _ in 276 | expect.fulfill() 277 | }) 278 | ) 279 | 280 | let subject = QLParallelSegment( 281 | ["main":GetIsMainThread("main"), 282 | "util":GetIsMainThread("util")], 283 | combiner: ([:], { var d = $0; d?[$1.0] = ($1.1 as? Bool ?? false); return d }), 284 | errorHandler: nil, 285 | output: output) 286 | 287 | subject.operationQueues["main"] = mainQueue 288 | subject.operationQueues["util"] = utilQueue 289 | 290 | subject.input.value = () 291 | 292 | wait(for: [expect], timeout: 8.0) 293 | XCTAssertEqual(output.value, ["main":true,"util":false]) 294 | } 295 | 296 | private func GetIsMainThread(_ id: AnyHashable) -> QLSegment.Operation { 297 | return { input, completion in 298 | completion( Thread.current.isMainThread ) 299 | } 300 | } 301 | } 302 | 303 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Segment/QLSegmentTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLSegmentTests: XCTestCase { 6 | 7 | func test_operation_conformance() throws { 8 | let seg = QLSerialSegment(0x0F, MockOp.VoidToStr("a-ok")) 9 | var out: String? 10 | try seg.operation((), { out = $0 }) 11 | XCTAssertEqual(out, "a-ok") 12 | } 13 | 14 | func test_type_erased_anchors_return_correct_anchors() { 15 | let seg = QLSerialSegment(0xA4, MockOp.IntToStr()) 16 | XCTAssert(seg.inputAnchor as! QLAnchor === seg.input) 17 | XCTAssert(seg.outputAnchor as? QLAnchor === seg.output) 18 | } 19 | 20 | func test_segment_performs_operation_on_nil_input() { 21 | let expect = expectation(description: "shouldComplete") 22 | let (captured, _, output) = SpyAnchor().CapturingAnchor(expect: expect) 23 | let seg = QLSerialSegment("numStr", MockOp.IntToStr(), 24 | output: output) 25 | 26 | seg.input.value = nil 27 | 28 | wait(for: [expect], timeout: 8.0) 29 | XCTAssertTrue(captured.didHappen) 30 | XCTAssertEqual(captured.value, "-1") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/QLoopTests/Segment/QLSerialSegmentTests.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class QLSerialSegmentTests: XCTestCase { 6 | 7 | func test_reveals_its_operation_ids() { 8 | let subject = QLSerialSegment(7, MockOp.VoidToStr()) 9 | 10 | XCTAssertNotNil(subject.operation) 11 | XCTAssertEqual(subject.operationIds, [7]) 12 | } 13 | 14 | func test_basicSegmentWithOutputAnchor_whenInputSet_itCallsCompletionWithoutResult() { 15 | let expect = expectation(description: "shouldComplete") 16 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 17 | let subject = QLSerialSegment(0, MockOp.VoidToStr(), output: finalAnchor) 18 | 19 | subject.input.value = nil 20 | 21 | wait(for: [expect], timeout: 8.0) 22 | XCTAssertTrue(captured.didHappen) 23 | XCTAssertNil(captured.value) 24 | } 25 | 26 | func test_givenIntToStringAndOutputAnchor_andCustomDispatchQueue_whenInputSet_itCallsCompletionWithResult() { 27 | let expect = expectation(description: "shouldComplete") 28 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 29 | let subject = QLSerialSegment( 30 | 0, 31 | MockOp.IntToStr(), 32 | operationQueue: DispatchQueue(label: "test queue") 33 | ) 34 | subject.output = finalAnchor 35 | 36 | subject.input.value = 5 37 | 38 | wait(for: [expect], timeout: 8.0) 39 | XCTAssertTrue(captured.didHappen) 40 | XCTAssertEqual(captured.value, "5") 41 | } 42 | 43 | func test_givenIntToStringAndOutputAnchor_whenInputSet_itCallsCompletionWithResult() { 44 | let expect = expectation(description: "shouldComplete") 45 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 46 | let subject = QLSerialSegment(0, MockOp.IntToStr(), output: finalAnchor) 47 | 48 | subject.input.value = 3 49 | 50 | wait(for: [expect], timeout: 8.0) 51 | XCTAssertTrue(captured.didHappen) 52 | XCTAssertEqual(captured.value, "3") 53 | } 54 | 55 | func test_givenTwoSegments_whenInputSetNil_itShouldTriggerOperationChain() { 56 | let expect = expectation(description: "shouldComplete") 57 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 58 | let subject = QLSerialSegment(0, MockOp.IntToStr(), 59 | errorHandler: nil, 60 | outputSegment: QLSerialSegment(0, MockOp.AddToStr(" eleven"), 61 | output: finalAnchor)) 62 | subject.input.value = nil 63 | 64 | wait(for: [expect], timeout: 8.0) 65 | XCTAssertTrue(captured.didHappen) 66 | XCTAssertEqual(captured.value, "-1 eleven") 67 | } 68 | 69 | func test_givenTwoSegments_whenInputSet_itShouldCallEndCompletionWithCorrectResult() { 70 | let expect = expectation(description: "shouldComplete") 71 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 72 | let subject = QLSerialSegment(0, MockOp.IntToStr(), 73 | outputSegment: QLSerialSegment(0, MockOp.AddToStr(" eleven"), 74 | output: finalAnchor)) 75 | subject.input.value = 7 76 | 77 | wait(for: [expect], timeout: 8.0) 78 | XCTAssertTrue(captured.didHappen) 79 | XCTAssertEqual(captured.value, "7 eleven") 80 | } 81 | 82 | func test_whenErrorThrown_itPropagatesErrorToOutputAnchor() { 83 | let expect = expectation(description: "shouldComplete") 84 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 85 | let subject = QLSerialSegment(0, MockOp.IntThrowsError(QLCommon.Error.Unknown), output: finalAnchor) 86 | 87 | subject.input.value = 404 88 | 89 | wait(for: [expect], timeout: 8.0) 90 | XCTAssert((finalAnchor.error as? QLCommon.Error) == QLCommon.Error.Unknown) 91 | XCTAssertNil(finalAnchor.value) 92 | XCTAssertFalse(captured.didHappen) 93 | XCTAssertNotEqual(captured.value, 404) 94 | } 95 | 96 | func test_whenInputErrorIsReceived_itPropagatesErrorToOutputAnchor() { 97 | let expect = expectation(description: "shouldComplete") 98 | let (captured, _, finalAnchor) = SpyAnchor().CapturingAnchor(expect: expect) 99 | let subject = QLSerialSegment(0, MockOp.AddToInt(5), output: finalAnchor) 100 | 101 | subject.input.error = QLCommon.Error.Unknown 102 | 103 | wait(for: [expect], timeout: 8.0) 104 | XCTAssert((finalAnchor.error as? QLCommon.Error) == QLCommon.Error.Unknown) 105 | XCTAssertNil(finalAnchor.value) 106 | XCTAssertFalse(captured.didHappen) 107 | XCTAssertNotEqual(captured.value, 404) 108 | } 109 | 110 | func test_givenSerialSegment_withErrorHandlerSet_whenErrorThrown_itHandles() { 111 | let expect = expectation(description: "shouldComplete") 112 | let (captured, _, output) = SpyAnchor().CapturingAnchor(expect: expect) 113 | var err: Error? = nil 114 | let handler: QLSerialSegment.ErrorHandler = { 115 | error, completion, _ in 116 | err = error 117 | completion(0) 118 | } 119 | 120 | let seg1 = QLSerialSegment(1, MockOp.IntThrowsError(QLCommon.Error.Unknown), 121 | errorHandler: handler, 122 | output: output) 123 | seg1.input.value = 4 124 | 125 | wait(for: [expect], timeout: 8.0) 126 | XCTAssertNotNil(err) 127 | XCTAssertTrue(captured.didHappen) 128 | } 129 | 130 | func test_givenSerialSegment_withErrorHandler_whenChoosesErrorPath_outputGetsError() { 131 | let expect = expectation(description: "shouldComplete") 132 | let (captured, _, output) = SpyAnchor().CapturingAnchor(expect: expect) 133 | var err: Error? = nil 134 | let handler: QLSerialSegment.ErrorHandler = { 135 | error, _, errCompletion in 136 | err = error 137 | errCompletion(error) 138 | } 139 | 140 | let seg1 = QLSerialSegment(1, MockOp.IntThrowsError(QLCommon.Error.Unknown), 141 | errorHandler: handler, 142 | output: output) 143 | seg1.input.value = 4 144 | 145 | wait(for: [expect], timeout: 8.0) 146 | XCTAssertNotNil(err) 147 | XCTAssertFalse(captured.didHappen) 148 | XCTAssertNotNil(output.error) 149 | XCTAssertEqual(output.error as? QLCommon.Error, QLCommon.Error.Unknown) 150 | } 151 | 152 | func test_find_segments_for_operation_succeeds_when_single() { 153 | let (_, _, output) = SpyAnchor().CapturingAnchor() 154 | 155 | let _ = QLSerialSegment(1, MockOp.VoidToStr("One"), 156 | output: output) 157 | let last = output.inputSegment 158 | 159 | XCTAssertEqual(last?.findSegments(with: 1).count, 1) 160 | } 161 | 162 | func test_find_segments_for_operation_succeeds_when_mix() { 163 | let (_, _, output) = SpyAnchor().CapturingAnchor() 164 | 165 | let _ = QLSerialSegment( 166 | 0x0A, MockOp.VoidToStr("One"), outputSegment: 167 | QLSerialSegment( 168 | 0x0B, MockOp.AddToStr("Two"), outputSegment: 169 | QLSerialSegment( 170 | 0x0C, MockOp.AddToStr("Three"), outputSegment: 171 | QLSerialSegment( 172 | 0x0B, MockOp.AddToStr("Four"), 173 | output: output)))) 174 | let last = output.inputSegment 175 | 176 | XCTAssertEqual(last?.findSegments(with: 0x0A).count, 1) 177 | XCTAssertEqual(last?.findSegments(with: 0x0B).count, 2) 178 | XCTAssertEqual(last?.findSegments(with: 0x0C).count, 1) 179 | } 180 | 181 | func test_operation_path_when_single() { 182 | let output = QLAnchor() 183 | let _ = QLSerialSegment(1, MockOp.VoidToStr("One"), output: output) 184 | 185 | let last = output.inputSegment 186 | let opPath = last?.operationPath() 187 | 188 | XCTAssertEqual(opPath?[0].0, [1]) 189 | XCTAssertEqual(opPath?[0].1, false) 190 | } 191 | 192 | func test_operation_path_when_multiple() { 193 | let output = QLAnchor() 194 | let _ = QLSerialSegment( 195 | 0x0A, MockOp.VoidToStr("One"), outputSegment: 196 | QLSerialSegment( 197 | 0x0B, MockOp.AddToStr("Two"), outputSegment: 198 | QLSerialSegment( 199 | 0x0C, MockOp.AddToStr("Three"), 200 | output: output))) 201 | 202 | let last = output.inputSegment 203 | let opPath = last?.operationPath() 204 | 205 | XCTAssertEqual(opPath?[0].0, [0x0A]) 206 | XCTAssertEqual(opPath?[1].0, [0x0B]) 207 | XCTAssertEqual(opPath?[2].0, [0x0C]) 208 | XCTAssertEqual(opPath?[0].1, false) 209 | XCTAssertEqual(opPath?[1].1, false) 210 | XCTAssertEqual(opPath?[2].1, false) 211 | } 212 | 213 | func test_describe_operation_path_when_multiple() { 214 | let output = QLAnchor() 215 | let _ = QLSerialSegment( 216 | "open", MockOp.VoidToStr("One"), outputSegment: 217 | QLSerialSegment( 218 | "speak", MockOp.AddToStr("Two"), outputSegment: 219 | QLSerialSegment( 220 | "close", MockOp.AddToStr("Three"), 221 | output: output))) 222 | 223 | let last = output.inputSegment 224 | let opPath = last?.describeOperationPath() 225 | 226 | XCTAssertEqual(opPath, "{open}-{speak}-{close}") 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /Tests/QLoopTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !os(macOS) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(qloopTests.allTests), 7 | ] 8 | } 9 | #endif -------------------------------------------------------------------------------- /Tests/QLoopTests/helpers/CapturedCompletion.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | import QLoop 4 | 5 | class Captured { 6 | var timesHappened: Int = 0 7 | var valueStream: [T?] = [] 8 | func capture(_ value: T?) { 9 | self.timesHappened += 1 10 | self.valueStream.append(value) } 11 | var didHappen: Bool { return timesHappened > 0 } 12 | var value: T? { return valueStream.last ?? nil } 13 | } 14 | 15 | extension Captured { 16 | 17 | static func captureCompletion(expect: XCTestExpectation? = nil) -> (Captured, (T?)->()) { 18 | let captured = Captured() 19 | let completion: (T?)->() = { 20 | captured.capture($0) 21 | expect?.fulfill() 22 | } 23 | return(captured, completion) 24 | } 25 | 26 | static func captureCompletionNonOpt(expect: XCTestExpectation? = nil) -> (Captured, (T)->()) { 27 | let captured = Captured() 28 | let completion: (T)->() = { 29 | captured.capture($0) 30 | expect?.fulfill() 31 | } 32 | return(captured, completion) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Tests/QLoopTests/mocks/MockComponent.swift: -------------------------------------------------------------------------------- 1 | 2 | import QLoop 3 | import XCTest 4 | 5 | class MockPhoneComponent { 6 | init(_ expect: XCTestExpectation? = nil) { 7 | self.expect = expect 8 | } 9 | var expect: XCTestExpectation? = nil 10 | lazy var phoneDataLoop = QLoop( 11 | onChange: ({ 12 | self.userPhoneNumberField = $0 ?? "" 13 | self.expect?.fulfill() 14 | }) 15 | ) 16 | var userPhoneNumberField: String = "" 17 | func userAction() { 18 | phoneDataLoop.perform() 19 | } 20 | } 21 | 22 | class MockProgressComponent { 23 | init(_ expect: XCTestExpectation? = nil) { 24 | self.expect = expect 25 | } 26 | var expect: XCTestExpectation? = nil 27 | lazy var progressDataLoop = QLoop( 28 | iterator: QLoopIteratorContinueOutput(), 29 | onChange: ({ 30 | self.progressField = $0 ?? "" 31 | self.expect?.fulfill() 32 | }), 33 | onError: ({ error in 34 | self.error = error 35 | self.expect?.fulfill() 36 | }) 37 | ) 38 | var progressField: String = "" 39 | var error: Error? = nil 40 | func userAction() { 41 | progressDataLoop.perform() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/QLoopTests/mocks/MockLoopIterable.swift: -------------------------------------------------------------------------------- 1 | 2 | import QLoop 3 | 4 | class MockLoopIterable: QLoopIterable { 5 | 6 | var discontinue: Bool = false 7 | 8 | var timesCalled_iteration: Int = 0 9 | var timesCalled_iterationFromLastOutput: Int = 0 10 | 11 | func iteration() { 12 | timesCalled_iteration += 1 13 | } 14 | 15 | func iterationFromLastOutput() { 16 | timesCalled_iterationFromLastOutput += 1 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/QLoopTests/mocks/MockLoopIterator.swift: -------------------------------------------------------------------------------- 1 | 2 | import QLoop 3 | 4 | class MockLoopIterator: QLoopIterating { 5 | 6 | var didCall_reset: Bool = false 7 | func reset() { 8 | didCall_reset = true 9 | } 10 | 11 | var didCall_iterate: Bool = false 12 | var valueFor_iterate_loop: QLoopIterable? = nil 13 | var shouldIterate: Bool = true 14 | func iterate(_ loop: QLoopIterable) -> Bool { 15 | didCall_iterate = true 16 | valueFor_iterate_loop = loop 17 | return shouldIterate 18 | } 19 | } 20 | 21 | class MockLoopResettableIterator: MockLoopIterator, QLoopIteratingResettable {} 22 | -------------------------------------------------------------------------------- /Tests/QLoopTests/mocks/MockOperation.swift: -------------------------------------------------------------------------------- 1 | 2 | class MockOp { 3 | 4 | typealias VoidToIntOp = (Void?, @escaping (Int?) -> ()) throws -> () 5 | typealias VoidToStrOp = (Void?, @escaping (String?) -> ()) throws -> () 6 | 7 | typealias IntToIntOp = (Int?, @escaping (Int?) -> ()) throws -> () 8 | typealias IntToStrOp = (Int?, @escaping (String?) -> ()) throws -> () 9 | 10 | typealias StrToStrOp = (String?, @escaping (String?) -> ()) throws -> () 11 | typealias StrToStrEr = (Error, @escaping (String?) -> (), @escaping (Error) -> ()) -> () 12 | 13 | 14 | static func IntToStr() -> IntToStrOp { 15 | return { input, compl in compl("\(input ?? -1)") } 16 | } 17 | 18 | static func AddToInt(_ value: Int) -> IntToIntOp { 19 | return { input, compl in 20 | compl((input ?? 0) + value) } 21 | } 22 | 23 | static func AddToStr(_ value: String) -> StrToStrOp { 24 | return { input, compl in 25 | compl((input ?? "") + value) } 26 | } 27 | 28 | static func VoidToStr(_ value: String? = nil) -> VoidToStrOp { 29 | return { input, compl in compl(value) } 30 | } 31 | 32 | static func VoidToInt(_ value: Int? = nil) -> VoidToIntOp { 33 | return { input, compl in compl(value) } 34 | } 35 | 36 | static func IntThrowsError(_ err: Error) -> IntToIntOp { 37 | return { input, compl in throw err } 38 | } 39 | 40 | static func StrThrowsError(_ err: Error) -> StrToStrOp { 41 | return { input, compl in throw err } 42 | } 43 | 44 | static func StrErrorHandler(_ shouldRetry: Bool) -> StrToStrEr { 45 | return { err, compl, errCompl in 46 | if shouldRetry { compl("") } 47 | else { errCompl(err) } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Tests/QLoopTests/mocks/SpyAnchor.swift: -------------------------------------------------------------------------------- 1 | 2 | import QLoop 3 | import XCTest 4 | import Dispatch 5 | 6 | class SpyAnchor { 7 | 8 | func CapturingAnchor(expect: XCTestExpectation? = nil) -> (Captured, Captured, QLAnchor) { 9 | let capturedCompletion = Captured.captureCompletion(expect: expect) 10 | let capturedError = Captured.captureCompletionNonOpt(expect: expect) 11 | let anchor = QLAnchor(onChange: capturedCompletion.1, onError: capturedError.1) 12 | return(capturedCompletion.0, capturedError.0, anchor) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # ![qloop](icon.png) QLoop 2 | 3 | **QLoop** /'kyoo•loop/ - *n* - Declarative asynchronous operation loops 4 | 5 | ## Change Log 6 | 7 | 8 |
9 | 10 | ### 0.1.7 11 | 12 | - `QLAnchor` repeater timing (early/late) 13 | 14 |
15 | 16 | ### 0.1.6 17 | 18 | - `Swift.Result` failures auto-route through error path 19 | - `QLAnchor` supports echoing to repeaters, with optional `EchoFilters` 20 | 21 |
22 | 23 | ### 0.1.5 24 | 25 | - added parameters for including custom dispatch queue when creating segments 26 | - moved convenience initializers back into main class (out of extension) due to swift 27 | compiler SIL bug. 28 | 29 |
30 | 31 | ### 0.1.4 32 | 33 | - updated for swift 5 34 | - `QLoopIteratingResettable` separated from `QLoopIterating` 35 | (avoids having to implement empty `reset()` functions on iterators that 36 | never reset.) 37 | 38 |
39 | 40 | ### 0.1.3 41 | 42 | - added `bind(segment:)` to `QLoop` 43 | - fixed dispatch bug in `QLParallelSegment` 44 | 45 | 46 |
47 | 48 | ### 0.1.2 49 | 50 | - `QLoop` upgrades 51 | - `onFinal` is now a thing ( called the same way as onChange, except only on final *iteration* ) 52 | - `QLAnchor` upgrades 53 | - default output and error sent on main thread (can be overriden if necessary) 54 | - `QLParallelSegment` upgrades 55 | - guarantees async dispatch of operations 56 | - more reliable operation completion handling 57 | - `QLSerialSegment` upgrades 58 | - now can assign dispatch queue 59 | - guarantees async dispatch of operation 60 | 61 | 62 |
63 | 64 | ### 0.1.1 65 | 66 | - renaming and simplified interfaces 67 | - `QLParallelSegment` upgrades 68 | - supports assignment of dispatch queues per `operationId` 69 | - thread safety around operation runs 70 | - operations can now output any type 71 | - simpler `combiner` no longer requires intermediate type 72 | 73 | 74 |
75 | 76 | ### 0.1.0 77 | 78 | - initial alpha testing release 79 | - cleaner testing support 80 | 81 |
82 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # ![qloop](icon.png) QLoop 2 | 3 | **QLoop** /'kyoo•loop/ - *n* - Declarative asynchronous operation loops 4 | 5 | ## Getting Started 6 | 7 | This guide will help you get started with using QLoop, and assumes you already 8 | know your way around [Xcode](https://developer.apple.com/xcode/) and/or 9 | the [Swift Package Manager](https://swift.org/package-manager/). 10 | 11 |
12 | 13 | ### 1) Install the package 14 | 15 | Either add it as a *submodule* or use **SwiftPM** ... 16 | 17 |
18 | 19 | #### Using Xcode 20 | 21 | When importing the library for use in an Xcode project (such as for an iOS or 22 | OSX app), then one solid choice is to simply add it as a git *submodule*: 23 | 24 | ``` 25 | mkdir -p submodule 26 | git submodule add https://github.com/quickthyme/qloop.git submodule/qloop 27 | 28 | ``` 29 | 30 | Next, link the **QLoop.xcodeproj** as a dependency of your project by dragging 31 | it from the Finder into your open project or workspace. 32 | 33 | Once you have the project linked, it's build scheme and products will be 34 | selectable from drop-downs in your Xcode project. Just add `QLoop` to your 35 | target's dependencies and `libQLoop-.a` to the linking phase, and you're 36 | all set! 37 | 38 |
39 | 40 | #### Using the Swift Package Manager 41 | 42 | QLoop supports the [Swift Package Manager](https://swift.org/package-manager/). 43 | It works fine in any Swift project on any Swift platform, including OSX and Linux. 44 | Just add the dependency to your `Package.swift` file: 45 | 46 | - package: `QLoop` 47 | - url: `https://github.com/quickthyme/qloop.git` 48 | 49 | Then just ... 50 | 51 | ``` 52 | swift resolve 53 | ``` 54 | That's it, nothing else to do except start using it... 55 | 56 |
57 | 58 | ### 2) Pin an "empty" *loop* to some entity: 59 | 60 | ``` 61 | import UIKit 62 | import QLoop 63 | 64 | class ManaViewController: UIViewController { 65 | 66 | @IBAction func magicAction(_ sender: AnyObject?) { 67 | doMagicLoop.perform(true) 68 | } 69 | 70 | lazy var doMagicLoop = QLoop( 71 | 72 | onChange: ({ sparkles in 73 | self.manaView.showSparkles() 74 | }), 75 | 76 | onError: ({ error in 77 | self.manaView.showError() 78 | })) 79 | } 80 | 81 | ``` 82 | 83 |
84 | 85 | ### 3) Implement your asynchronous *operations*: 86 | 87 | ``` 88 | func MakeSparkles(awesomeSauce: Combustible) -> (((String?), (String?)->()) throws -> ()) { 89 | let powder = grind(awesomeSauce) 90 | return { input, completion in 91 | result = Heat(powder, level: 11) 92 | completion(result) 93 | } 94 | } 95 | ``` 96 | 97 |
98 | 99 | ### 4) Bind a *path* to the loop: 100 | 101 | ``` 102 | func inject() { 103 | manaViewController.doMagicLoop 104 | .bind(path: QLPath( 105 | QLss(1, MakeSparkles(awesomeSauce: .unicornSriracha)), 106 | QLss(2, SafetyInspectSparkles())) 107 | } 108 | 109 | ``` 110 | 111 |
112 | 113 | ### 5) Write *tests*: 114 | 115 | ``` 116 | ... 117 | func test_when_magicAction_then_it_runs_the_loop() { 118 | subject.magicAction(nil) 119 | XCTAssertEqual(subject.doMagicLoop.input.value, true) 120 | } 121 | ... 122 | ``` 123 | 124 |
125 | 126 | For more, please check out the [demo app](https://github.com/quickthyme/qloop-demo) 127 | , 128 | or read the [Introduction](introduction.md). 129 | 130 | -------------------------------------------------------------------------------- /docs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quickthyme/qloop/17cdd67d2e58b58348605bb09e720187712577e8/docs/icon.png -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | # ![qloop](icon.png) QLoop 2 | 3 | **QLoop** /'kyoo•loop/ - *n* - Declarative asynchronous operation loops 4 | 5 | ## Introduction 6 | 7 | Here is an introduction to the [QLoop](https://github.com/quickthyme/qloop) 8 | library, and what it does. It assumes that you already have a basic 9 | understanding of Swift. Although, knowledge of Xcode or any other IDE will 10 | be irrelevant to the topics covered here. 11 | 12 | At a high level, features it provides include: 13 | 14 | - compose asynchronous operation paths as reusable "loop" constructs 15 | - *test-friendly* observer-pattern module favoring declarative composition 16 | - built-in error propagation 17 | - swiftPM compatible package 18 | - universal module; Swift 4.2+, 5 (default) 19 | 20 | #### More promising 21 | 22 | If you're familiar with "promise chains", then you will no doubt feel right at 23 | home, here. In many ways, QLoop works very much like promises: link together 24 | asynchronous operations, propagate output and/or errors, handle exceptions 25 | safely, allow for synchronous testing, and expose observable results. 26 | 27 | The differences, however, are far less subtle. There's obvious stuff, such 28 | as iteration control and operation grouping. (it is a loop, after all) 29 | 30 | But what really makes QLoop stand out, is how well one can statically compose, 31 | thoroughly test, inspect, and reason over complex operations with ease. 32 | 33 | QLoop enables *declarative-reactive* development **without obfuscation** or 34 | *callback hell*. And when compared to some similar frameworks, it is extremely 35 | light-weight, non-intrusive, and universally cross-platform. 36 | 37 | #### Less mocking 38 | 39 | Testing, composition, and inspection are the three top priorities of QLoop. 40 | Rather than make setting up the test environment more difficult, it actually 41 | simplifies it a great deal. 42 | 43 | Loops essentially mock themselves, given that they are naturally 44 | empty and void of personality until both of its anchors are bound (which they 45 | are not, by default). 46 | 47 | Pretty much everything you need to simulate reactions for testing 48 | purposes comes entirely built-in, saving you the trouble of writing mocks and 49 | other such foolery. 50 | 51 | Then there's `describeOperationPath()`, which produces human-readable, 52 | as well as reliably-parsable, "snapshots" that can be used for diagnostic 53 | purposes and/or for test comparisons. You can see exactly which operations 54 | are to be called and in what order. 55 | 56 | 57 |
58 | 59 | ### Loops 60 | 61 | ![loops](loops.png) 62 | 63 | Compose `paths` of asynchronous operation `segments`, then bind them to anchors 64 | or wrap them up into *observable* loops. Simply decorate an entity with empty `loops` 65 | and/or `anchors`, and implement the `onChange` and/or `onError` events. 66 | 67 | - Observation & Delegation 68 | 69 | Loops are circular, providing both `Input` (**delegation**) and `Output` (**observation**) 70 | capabilities. 71 | 72 | - Flavorless 73 | 74 | By default, a QLoop has no bound functionality on creation. You must bestow 75 | its behavior by binding it to paths and/or segments. 76 | 77 | - Iteration 78 | 79 | Loops provide **iteration**, should it be desired. By default, a loop will 80 | run once per input set, but they can be made to run however you like, simply 81 | by swapping out its `iterator`. There are several included out-of-the-box, 82 | but you can also create your own in order to extend the loop's functionality. 83 | 84 | Any iterator you wish to use with QLoop must conform to `QLoopIterating`. 85 | 86 | - Chaining 87 | 88 | Loops can also be connected to other `loops`, `paths`, or `segments`; 89 | basically anything that can bind to an `anchor`. 90 | 91 | 92 |
93 | 94 | ### Paths 95 | 96 | The default segment constructors allow them to be linked explicitly, 97 | in a type-safe manner, but they can quickly become difficult to read 98 | or muck around with for any practical use of chaining. 99 | 100 | `QLPath` addresses this, by allowing you to compose a series 101 | of segments together, in a less-violent, more readable way. 102 | 103 | This is accomplished using *type-erasure*, but we don't have to give up 104 | type safety completely. The *failable initializer* will return `nil` if 105 | any segment in the chain fails to link to one of its neighboring segments. 106 | 107 | When using paths, **you always have a way to ensure everything is correct.** 108 | Besides, it's trivial to verify the operation chains in our unit-tests, 109 | thanks to their ability to consistently describe themselves. 110 | 111 | Just like with a loop, a path behaves as a bundle for its segments. They 112 | are typically bound to a loop at some point, or combined with other paths 113 | and/or segments to form more complex chains. 114 | 115 | 116 |
117 | 118 | ### Segments 119 | 120 | Operation `segments` are the vehicles that drive your custom `operations`. 121 | Segments can be run independently, or linked together in order to form any 122 | number of complex sequences. 123 | 124 | ![segments](segments.png) 125 | 126 | You do not subclass segments. Instead, you simply attach your asynchronous 127 | operations to them using a contract enforced by a simple swift closure. 128 | 129 | There are currently two types of segments to choose from: 130 | 131 | - `QLSerialSegment` - (or `QLss`) performs a **single operation** and then moves on 132 | - `QLParallelSegment` - (or `QLps`) performs **multiple concurrent operations**, 133 | waiting for them all to complete before moving on. 134 | 135 | 136 |
137 | 138 | ##### Operations 139 | 140 | Operations are your application's workers, interactors, etc. In order to attach 141 | an operation to a segment, it must be compatible (either inately or wrapped) 142 | with this signature: 143 | 144 | ``` 145 | ( _ input: Input?, 146 | _ completion: (_ output: Output?) -> () ) throws -> () 147 | ``` 148 | 149 | That is to say, it must take in an `Input` of whatever type, perform whatever 150 | operation(s), then either call the completion handler with appropriate 151 | `Output`, or throw an error. 152 | 153 | 154 |
155 | 156 | ##### ErrorHandlers 157 | 158 | Error handlers are optional things you can add whenever you suspect errors might 159 | get thrown. The error handler signature looks like this: 160 | 161 | ``` 162 | ( _ error: Error, 163 | _ completion: @escaping (_ output: Output?) -> (), 164 | _ errCompletion: @escaping (_ error: Swift.Error?) -> () ) -> () 165 | ``` 166 | 167 | Whenever the operation throws an error, or if an error is passed via the input 168 | anchor, then the error handler (if set) will try and handle the error. If it is 169 | successful, then it may choose to call the `outputCompletion`. Otherwise, it 170 | should forward the error (or generate a new one, depending) down the line by 171 | calling `errCompletion` instead. 172 | 173 | The error handler should *not* throw an error. 174 | 175 | The default behavior is to just forward the error. 176 | 177 | 178 |
179 | 180 | ### Anchors 181 | 182 | An `anchor` is what facilitates the **contract** binding segments. To bind 183 | to an anchor essentially means to respond to its `onChange(_)` and/or 184 | `onError(_)` events. 185 | 186 | 187 |
188 | 189 | --- 190 | 191 | For more, please refer to the **[API Reference](reference.md)**. 192 | 193 | There's also the **[Getting Started](getting-started.md)** guide 194 | and a **[demo app](https://github.com/quickthyme/qloop-demo)**! 195 | 196 |
197 | -------------------------------------------------------------------------------- /docs/loops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quickthyme/qloop/17cdd67d2e58b58348605bb09e720187712577e8/docs/loops.png -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # ![qloop](icon.png) QLoop 2 | 3 | **QLoop** /'kyoo•loop/ - *n* - Declarative asynchronous operation loops 4 | 5 | ## Reference 6 | 7 | This section serves as the API reference to QLoop. For information about QLoop 8 | and what it is, please read the [introduction](introduction.md) and the 9 | [getting started](getting-started.md) guide if you have not already. 10 | 11 | 12 | #### [QLoop](reference/QLoop.md) 13 | 14 | 15 | #### [QLAnchor](reference/QLAnchor.md) 16 | 17 | 18 | #### [QLPath](reference/QLPath.md) 19 | 20 | 21 | #### [QLParallelSegment](reference/QLParallelSegment.md) 22 | 23 | 24 | #### [QLSerialSegment](reference/QLSerialSegment.md) 25 | 26 |
27 | -------------------------------------------------------------------------------- /docs/reference/QLAnchor.md: -------------------------------------------------------------------------------- 1 | 2 | ## QLoop Reference 3 | 4 |
5 | 6 | #### QLAnchor 7 | 8 | `QLAnchor` 9 | 10 | - type: `class` 11 | - conforms to: `AnyAnchor` 12 | 13 |
14 | 15 | ##### Creating 16 | 17 | - init( ) 18 | 19 | - init(onChange: `(Input?)->()` ) 20 | 21 | - init(onChange: `(Input?)->()`, onError: `(Error)->()` ) 22 | 23 | - init(earlyRepeaters: `QLAnchor.Repeater`, `...` ) 24 | 25 | - init(lateRepeaters: `QLAnchor.Repeater`, `...` ) 26 | 27 | 28 |
29 | 30 | ##### Instance Variables 31 | 32 | - value: `Input?` 33 | 34 | - error: `Error?` 35 | 36 | - onChange: `(Input?)->()` 37 | 38 | - onError: `(Error)->()` 39 | 40 | - inputSegment: `AnySegment?` 41 | 42 | 43 |
44 | 45 | ##### Configuration 46 | 47 | By default, `QLAnchor` always remembers the last `value` or `error` it received, but 48 | it **releases them in production builds**. This behavior can be disabled by 49 | setting the anchor global config `releaseValues` to `false`: 50 | 51 | `QLCommon.Config.Anchor.releaseValues = false` 52 | 53 |
54 | 55 | ##### Discussion 56 | 57 | An `anchor` is what facilitates the **contract** binding segments. To bind 58 | to an anchor essentially means to respond to its `onChange(_)` and/or 59 | `onError(_)` events. 60 | 61 | An `anchor` : 62 | - can only receive a `value` or an `error` 63 | - may only have **one subscriber** 64 | - may have **any number of input providers** 65 | - can only retain one *segment* at a time 66 | - `onChange` and `onError` default to the *main thread* 67 | 68 | `QLAnchor` implements a type of **semaphore** that makes use of synchronous dispatch 69 | queues around its `value` and `error` nodes. Inputs can safely arrive on any thread, 70 | and the events are guaranteed to arrive in serial fashion, although their order is not. 71 | 72 | 73 | ##### Repeaters 74 | 75 | Repeaters offer a way to fork multiple streams off of the main path. 76 | 77 | When an Anchor has repeaters applied, then it will `echo` any `value` and `error` changes 78 | to each of them. 79 | 80 | By default, it forwards all changes to all repeaters. In order to make it conditional, 81 | include an `EchoFilter`. Return `false` from the EchoFilter to block that repeater 82 | from receiving the change. 83 | 84 | 85 | ##### EchoFilter 86 | 87 | - `(Input?, QLAnchor) -> (Bool)` 88 | 89 | Default filter returns `true`. You can evaluate the input value and decide whether or not the 90 | particular `anchor` (repeater) should receive the new value. 91 | 92 | To identify the anchor, you will need to do so using the object reference. 93 | 94 | example: 95 | 96 | ``` 97 | let progressRepeater = viewController.progressAnchor 98 | let finalRepeater = viewController.downloadCompleteAnchor 99 | 100 | let baseAnchor = QLAnchor( 101 | echoFilter: ({ obj, repeater in 102 | return (obj.isProgress && repeater === progressRepeater) 103 | || (obj.isFinal && repeater === finalRepeater) 104 | }), 105 | repeaters: progressRepeater, finalRepeater 106 | ) 107 | ``` 108 | -------------------------------------------------------------------------------- /docs/reference/QLParallelSegment.md: -------------------------------------------------------------------------------- 1 | 2 | ## QLoop Reference 3 | 4 |
5 | 6 | #### QLParallelSegment 7 | 8 | `QLParallelSegment` 9 | 10 | - type: `class` 11 | - conforms to: `AnySegment` 12 | 13 |
14 | 15 | ##### Creating 16 | 17 | - init(_ operations: `[AnyHashable:ParallelOperation]`, combiner: `Combiner?`, errorHandler: `ErrorHandler?`, output: `QLAnchor?` ) 18 | 19 | 20 |
21 | 22 | ##### Instance Variables 23 | 24 | - input: `QLAnchor` 25 | 26 | - output: `QLAnchor` 27 | 28 | - operationIds: `[AnyHashable]` 29 | 30 | - operationQueues: `[AnyHashable: DispatchQueue]` 31 | 32 | 33 |
34 | 35 | ##### Diagnostics 36 | 37 | - findSegments(with operationId: `AnyHashable`) -> `[AnySegment]` 38 | 39 | - describeOperationPath( ) -> `String` 40 | 41 | 42 |
43 | 44 | ##### Discussion 45 | 46 | Operation `segments` are the vehicles that drive your custom `operations`. 47 | Segments can be run independently, or linked together in order to form any 48 | number of complex sequences. 49 | 50 | When connecting segments together, the `input` of the second 51 | gets *assigned* to the `output` of the first, and so on. 52 | 53 | - A `segment` only observes its own `input`. 54 | - A `segment` only runs its operation if it has an `output` assigned 55 | 56 | `QLParallelSegment` is more powerful than its serial counterpart, but also 57 | more complicated. 58 | 59 | Operations in a parallel segment run concurrently. Once they have all completed 60 | their work, the segment calls the `combiner` in order to reduce the values into 61 | the output type the segment is expected to pass. 62 | 63 | When setting up parallel segments, you can optionally assign dispatch queues 64 | to the operations by adding it to `operationQueues`. The key is the id of the 65 | operation, the value is the `DispatchQueue` you want it to use. 66 | -------------------------------------------------------------------------------- /docs/reference/QLPath.md: -------------------------------------------------------------------------------- 1 | 2 | ## QLoop Reference 3 | 4 |
5 | 6 | #### QLPath 7 | 8 | `QLPath` 9 | 10 | - type: `class` 11 | 12 | 13 |
14 | 15 | ##### Creating 16 | 17 | - init?(_ segments: `AnySegment...`) 18 | 19 | 20 |
21 | 22 | ##### Instance Variables 23 | 24 | - input: `QLAnchor` 25 | 26 | - output: `QLAnchor` 27 | 28 | 29 | 30 |
31 | 32 | ##### Diagnostics 33 | 34 | - findSegments(with operationId: `AnyHashable`) -> `[AnySegment]` 35 | 36 | - describeOperationPath( ) -> `String` 37 | -------------------------------------------------------------------------------- /docs/reference/QLSerialSegment.md: -------------------------------------------------------------------------------- 1 | 2 | ## QLoop Reference 3 | 4 |
5 | 6 | #### QLSerialSegment 7 | 8 | `QLSerialSegment` 9 | 10 | - type: `class` 11 | - conforms to: `AnySegment` 12 | 13 |
14 | 15 | ##### Creating 16 | 17 | - init(operationId: `AnyHashable`, operation: `Operation`, errorHandler: `ErrorHandler?`, output: `QLAnchor?`) 18 | 19 | 20 |
21 | 22 | ##### Instance Variables 23 | 24 | - input: `QLAnchor` 25 | 26 | - output: `QLAnchor` 27 | 28 | - operationId: `AnyHashable` 29 | 30 | - operationQueue: `DispatchQueue` 31 | 32 | 33 |
34 | 35 | ##### Diagnostics 36 | 37 | - findSegments(with operationId: `AnyHashable`) -> `[AnySegment]` 38 | 39 | - describeOperationPath( ) -> `String` 40 | 41 | 42 |
43 | 44 | ##### Discussion 45 | 46 | Operation `segments` are the vehicles that drive your custom `operations`. 47 | Segments can be run independently, or linked together in order to form any 48 | number of complex sequences. 49 | 50 | When connecting segments together, the `input` of the second 51 | gets *assigned* to the `output` of the first, and so on. 52 | 53 | - A `segment` only observes its own `input`. 54 | - A `segment` only runs its operation if it has an `output` assigned 55 | -------------------------------------------------------------------------------- /docs/reference/QLoop.md: -------------------------------------------------------------------------------- 1 | 2 | ## QLoop Reference 3 | 4 |
5 | 6 | #### QLoop 7 | 8 | `QLoop` 9 | 10 | - type: `class` 11 | - conforms to: `QLoopIterable` 12 | 13 | 14 |
15 | 16 | ##### Creating 17 | 18 | - init( ) 19 | 20 | - init(onChange: `(Output?)->()`) 21 | 22 | - init(iterator: `QLoopIterating`, onChange: `(Output?)->()`) 23 | 24 | - init(iterator: `QLoopIterating`, onChange: `(Output?)->()`, onError: `(Error)->()`) 25 | 26 | 27 |
28 | 29 | ##### Instance Variables 30 | 31 | - input: `QLAnchor` 32 | 33 | - output: `QLAnchor` 34 | 35 | - discontinue: `Bool` 36 | 37 | - shouldResume: `Bool` 38 | 39 | - onFinal: `(Input?)->()` 40 | 41 | - onChange: `(Input?)->()` 42 | 43 | - onError: `(Error)->()` 44 | 45 | - iterator: `QLoopIterating` 46 | 47 | 48 |
49 | 50 | ##### Managing behavior 51 | 52 | - bind(path: `QLPath` ) 53 | 54 | - bind(segment: `QLSegment` ) 55 | 56 | - destroy( ) 57 | 58 | 59 |
60 | 61 | ##### Executing 62 | 63 | - perform( ) 64 | 65 | - perform( `Input?` ) 66 | 67 | 68 |
69 | 70 | ##### Diagnostics 71 | 72 | - findSegments(with operationId: `AnyHashable` ) -> `[AnySegment]` 73 | 74 | - describeOperationPath( ) -> `String` 75 | 76 |
77 | 78 | ##### Discussion 79 | 80 | When subscribing to loop events, understand that both `onChange` and `onFinal` calls 81 | will occur on final loop output. When using the default iteration of "single", then there 82 | is no reason to subscribe to both events. When using one of the infinitely continuous modes, 83 | however, only `onChange` will ever get called. 84 | 85 | - `onChange` is called on every output change 86 | - `onFinal` is called for the last output when using iterations that have a finite state (in addition to `onChange`) 87 | - `onFinal` may also be called upon the next iteration if the `discontinue` flag gets set 88 | (and the iterator being used returns false, as all of the included ones do in this situation.) 89 | 90 | -------------------------------------------------------------------------------- /docs/segments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quickthyme/qloop/17cdd67d2e58b58348605bb09e720187712577e8/docs/segments.png --------------------------------------------------------------------------------