├── .gitignore
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ └── contents.xcworkspacedata
│ └── xcshareddata
│ └── xcschemes
│ └── PiedPiper.xcscheme
├── .travis.yml
├── CHANGELOG.md
├── Cartfile.private
├── Cartfile.resolved
├── Example
├── Example.xcodeproj
│ ├── Example.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── project.pbxproj
│ └── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── Example
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ └── Contents.json
│ └── Contents.json
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ └── ViewController.swift
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── MIGRATING.md
├── Package.resolved
├── Package.swift
├── PiedPiper.playground
├── Contents.swift
├── Sources
│ └── SupportCode.swift
└── contents.xcplayground
├── PiedPiper.podspec
├── PiedPiper.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcshareddata
│ └── xcschemes
│ ├── PiedPiper iOS.xcscheme
│ ├── PiedPiper macOS.xcscheme
│ ├── PiedPiper tvOS.xcscheme
│ └── PiedPiper watchOS.xcscheme
├── PiedPiper.xcworkspace
├── contents.xcworkspacedata
└── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── README.md
├── Sources
└── PiedPiper
│ ├── FunctionComposition.swift
│ ├── Future+All.swift
│ ├── Future+Filter.swift
│ ├── Future+FlatMap.swift
│ ├── Future+Map.swift
│ ├── Future+MergeAll.swift
│ ├── Future+MergeSome.swift
│ ├── Future+Recover.swift
│ ├── Future+Reduce.swift
│ ├── Future+Retry.swift
│ ├── Future+Snooze.swift
│ ├── Future+Timeout.swift
│ ├── Future+Traverse.swift
│ ├── Future+Zip.swift
│ ├── Future+firstCompleted.swift
│ ├── Future.swift
│ ├── GCD.swift
│ ├── Info.plist
│ ├── PiedPiper.h
│ ├── Promise.swift
│ ├── ReadWriteLock.swift
│ ├── Result+Filter.swift
│ ├── Result+Map.swift
│ ├── Result+flatMap.swift
│ └── Result.swift
├── Tests
└── PiedPiperTests
│ ├── FunctionCompositionTests.swift
│ ├── Future+AllTests.swift
│ ├── Future+FilterTests.swift
│ ├── Future+FirstCompletedTests.swift
│ ├── Future+FlatMapTests.swift
│ ├── Future+MapTests.swift
│ ├── Future+MergeTests.swift
│ ├── Future+RecoverTests.swift
│ ├── Future+ReduceTests.swift
│ ├── Future+RetryTests.swift
│ ├── Future+SnoozeTests.swift
│ ├── Future+TimeoutTests.swift
│ ├── Future+TraverseTests.swift
│ ├── Future+ZipTests.swift
│ ├── FutureTests.swift
│ ├── Info.plist
│ ├── PromiseTests.swift
│ ├── Result+FilterTests.swift
│ ├── Result+FlatMapTests.swift
│ ├── Result+MapTests.swift
│ └── ResultTests.swift
├── fastlane
├── Fastfile
├── README.md
└── Scanfile
└── travis
├── bootstrap-if-needed.sh
└── bootstrap.sh
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## macOS
6 | .DS_Store
7 |
8 | ## Build generated
9 | build/
10 | DerivedData
11 |
12 | ## Various settings
13 | *.pbxuser
14 | !default.pbxuser
15 | *.mode1v3
16 | !default.mode1v3
17 | *.mode2v3
18 | !default.mode2v3
19 | *.perspectivev3
20 | !default.perspectivev3
21 | xcuserdata
22 |
23 | ## Other
24 | *.xccheckout
25 | *.moved-aside
26 | *.xcuserstate
27 | *.xcscmblueprint
28 |
29 | ## Obj-C/Swift specific
30 | *.hmap
31 | *.ipa
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | .build/
42 |
43 | # CocoaPods
44 | #
45 | # We recommend against adding the Pods directory to your .gitignore. However
46 | # you should judge for yourself, the pros and cons are mentioned at:
47 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
48 | #
49 | # Pods/
50 |
51 | # Carthage
52 | #
53 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
54 | # Carthage/Checkouts
55 |
56 | Carthage/
57 |
58 | # fastlane
59 | #
60 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
61 | # screenshots whenever they are needed.
62 | # For more information about the recommended setup visit:
63 | # https://github.com/fastlane/fastlane/blob/master/docs/Gitignore.md
64 |
65 | fastlane/report.xml
66 | fastlane/screenshots
67 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcshareddata/xcschemes/PiedPiper.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
29 |
35 |
36 |
37 |
38 |
39 |
46 |
47 |
53 |
54 |
55 |
56 |
58 |
64 |
65 |
66 |
67 |
68 |
78 |
79 |
85 |
86 |
92 |
93 |
94 |
95 |
97 |
98 |
101 |
102 |
103 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # references:
2 | # * http://www.objc.io/issue-6/travis-ci.html
3 | # * https://github.com/supermarin/xcpretty#usage
4 |
5 | language: objective-c
6 | cache:
7 | bundler: true
8 | directories:
9 | - "./Carthage"
10 | osx_image: xcode11.5
11 |
12 | # before_install:
13 | before_script:
14 | - "./travis/bootstrap-if-needed.sh"
15 |
16 | script:
17 | - bundle exec fastlane test
18 | - pod lib lint --quick
19 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 0.11.0
4 | **New features**
5 | - Added support for Swift Package Manager
6 | - Updated to Xcode 11.5
7 |
8 | ## 0.9
9 |
10 | **Breaking changes**
11 | - `PiedPiper` is now compiled with Swift 2.3
12 | - `merge` has been deprecated, please use `mergeAll` instead
13 |
14 | **New features**
15 | - Added `mergeSome` to a `SequenceType` of `Future`s to collapse a list of `Future`s into a single one that succeeds even if some of the `Future`s fail (contrast to `merge`)
16 | - Added `all` to a `SequenceType` of `Future`s to collapse a list of `Future`s into a single one that succeeds when all of the elements of the sequence succeed, and fails when one of the element fails (it's similar to `merge` but it doesn't bring the results with it).
17 | - Added `snooze` to `Future` in order to delay the result of a `Future` (either success or failure) by a given time
18 | - Added `timeout` to `Future` in order to set a deadline for the result of a `Future` after which it will automatically fail
19 | - Added `firstCompleted` to a `SequenceType` of `Future`s to get the result of the first `Future` that completes and ignore the others.
20 | - Added a `retry` global function to retry a given `Future` (generated through a provided closure) a certain number of times every given interval
21 |
22 | ## 0.8
23 |
24 | **Breaking changes**
25 | - The codebase has been migrated to Swift 2.2
26 | - `Promise` now has only an empty `init`. If you used one of the convenience `init` (with `value:`, with `error:` or with `value:error:`), they now moved to `Future`.
27 |
28 | **New features**
29 | - Adds `value` and `error` properties to `Result`
30 | - Added a way to initialize `Future`s through closures
31 | - It's now possible to `map` `Future`s through:
32 | - a simple transformation closure
33 | - a closure that `throws`
34 | - It's now possible to `flatMap` `Future`s through:
35 | - a closure that returns an `Optional`
36 | - a closure that returns another `Future`
37 | - a closure that returns a `Result`
38 | - It's now possible to `filter` `Future`s through:
39 | - a simple condition closure
40 | - a closure that returns a `Future`
41 | - It's now possible to `reduce` a `SequenceType` of `Future`s into a new `Future` through a `combine` closure
42 | - It's now possible to `zip` a `Future` with either another `Future` or with a `Result`
43 | - Added `merge` to a `SequenceType` of `Future`s to collapse a list of `Future`s into a single one
44 | - Added `traverse` to `SequenceType` to generate a list of `Future`s through a given closure and `merge` them together
45 | - Added `recover` to `Future` so that it's possible to provide a default value the `Future` can use instead of failing
46 | - It's now possible to `map` `Result`s through:
47 | - a simple transformation closure
48 | - a closure that `throws`
49 | - It's now possible to `flatMap` `Result`s through:
50 | - a closure that returns an `Optional`
51 | - a closure that returns a `Future`
52 | - a closure that returns another `Result`
53 | - It's now possible to `filter` `Result`s through a simple condition closure
54 | - Added `mimic` to `Result`
55 |
56 |
57 | ## 0.7
58 |
59 | First release of `Pied Piper` as a separate framework.
60 |
61 | **Breaking changes**
62 | - As documented in the `MIGRATING.md` file, you will have to add a `import PiedPiper` line everywhere you make use of Carlos' `Future`s or `Promise`s.
63 |
64 | **New features**
65 | - It's now possible to compose async functions and `Future`s through the `>>>` operator.
66 | - The implementation of `ReadWriteLock` taken from [Deferred](https://github.com/bignerdranch/Deferred) is now exposed as `public`.
67 | - It's now possible to take advantage of the `GCD` struct to execute asynchronous computation through the functions `main` and `background` for GCD built-in queues and `async` for GCD serial or custom queues.
68 |
69 | **Improvements**
70 | - `Promise`s are now safer to use with GCD and in multi-thread scenarios.
71 |
72 | **Fixes**
73 | - Fixes a bug where calling `succeed`, `fail` or `cancel` on a `Promise` or a `Future` didn't correctly release all the attached listeners.
74 | - Fixes a retain cycle between `Promise` and `Future` objects.
75 |
--------------------------------------------------------------------------------
/Cartfile.private:
--------------------------------------------------------------------------------
1 | github "Quick/Quick"
2 | github "Quick/Nimble"
3 |
--------------------------------------------------------------------------------
/Cartfile.resolved:
--------------------------------------------------------------------------------
1 | github "Quick/Nimble" "v8.1.1"
2 | github "Quick/Quick" "v3.0.0"
3 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/Example.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Example/Example/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Example
4 | //
5 | // Created by Lisovyi, Ivan on 30.06.20.
6 | // Copyright © 2020 WeltNews. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | final class AppDelegate: UIResponder, UIApplicationDelegate {
13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
14 | return true
15 | }
16 |
17 | // MARK: UISceneSession Lifecycle
18 |
19 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
20 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Example/Example/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Example/Example/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/Example/Example/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/Example/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIMainStoryboardFile
45 | Main
46 | UIRequiredDeviceCapabilities
47 |
48 | armv7
49 |
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationLandscapeLeft
54 | UIInterfaceOrientationLandscapeRight
55 |
56 | UISupportedInterfaceOrientations~ipad
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationPortraitUpsideDown
60 | UIInterfaceOrientationLandscapeLeft
61 | UIInterfaceOrientationLandscapeRight
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/Example/Example/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // Example
4 | //
5 | // Created by Lisovyi, Ivan on 30.06.20.
6 | // Copyright © 2020 WeltNews. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 | var window: UIWindow?
13 |
14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
15 | guard let _ = (scene as? UIWindowScene) else { return }
16 | }
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/Example/Example/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Example
4 | //
5 | // Created by Lisovyi, Ivan on 30.06.20.
6 | // Copyright © 2020 WeltNews. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import PiedPiper
11 |
12 | final class ViewController: UIViewController {
13 | override func viewDidLoad() {
14 | super.viewDidLoad()
15 |
16 | // MARK: Function composition
17 | let double = multiply(2)
18 | let plus3 = add(3)
19 |
20 | let composed = double >>> plus3 >>> triple
21 |
22 | print(composed(5)) // prints 39 ((5 * 2) + 3) * 3
23 |
24 | // MARK: Futures
25 | let future: Future = Future {
26 | composed(-5)
27 | }
28 |
29 | future
30 | .onSuccess {
31 | print("Succeeded with \($0)") // prints -21
32 | }
33 | .onCompletion { result in
34 | // MARK: Result
35 | if let value = result.value {
36 | print("Completed with \(value)") // prints -21 as well
37 | }
38 | }
39 |
40 | // MARK: Advanced operations on futures
41 | let processed = future
42 | .filter {
43 | $0 > 0
44 | }
45 | .flatMap {
46 | Future("Processed string result is \($0)")
47 | }
48 | .map {
49 | "\($0)".uppercased()
50 | }
51 |
52 | processed
53 | .onSuccess {
54 | print("Processed future succeeded with value \"\($0)\"") // doesn't print
55 | }
56 | .onFailure {
57 | print("Processed future failed with error \($0)") // prints with error ConditionedUnsatisfied (for the filter)
58 | }
59 |
60 | let willSucceed = processed.recover {
61 | "Failed because of negative input"
62 | }
63 |
64 | willSucceed.onSuccess {
65 | print("Recovered future succeeded with value \"\($0)\"") // prints "Recovered future succeeded with value "Failed because of negative input""
66 | }
67 |
68 | // MARK: Operations on arrays of Futures
69 | let inputs = [-5, 0, 5]
70 | let futures: [Future] = inputs.map { input in
71 | Future {
72 | composed(input)
73 | }
74 | }
75 |
76 | let reduced = futures.reduce(0, combine: +)
77 | let collapsed = futures.mergeAll()
78 |
79 | collapsed.onSuccess {
80 | print("Collapsed result: \($0)") // prints [-21, 9, 39]
81 | }
82 |
83 | reduced.onSuccess {
84 | print("Reduced result: \($0)") // prints 27 (-21 + 9 + 39)
85 | }
86 |
87 | let name = Future {
88 | "foo"
89 | }
90 |
91 | let value = Future {
92 | 0
93 | }
94 |
95 | name.zip(value).onSuccess {
96 | print("Zipped result: \($0)") // prints ("foo", 0)
97 | }
98 | }
99 |
100 | private func multiply(_ by: Int) -> (Int) -> Int {
101 | return { input in
102 | input * by
103 | }
104 | }
105 |
106 | private func add(_ input: Int) -> (Int) -> Int {
107 | return { num in
108 | num + input
109 | }
110 | }
111 |
112 | private func triple(_ input: Int) -> Int {
113 | return input * 3
114 | }
115 | }
116 |
117 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | gem 'fastlane', '~> 2.140'
4 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | CFPropertyList (3.0.2)
5 | addressable (2.7.0)
6 | public_suffix (>= 2.0.2, < 5.0)
7 | atomos (0.1.3)
8 | aws-eventstream (1.1.0)
9 | aws-partitions (1.337.0)
10 | aws-sdk-core (3.102.1)
11 | aws-eventstream (~> 1, >= 1.0.2)
12 | aws-partitions (~> 1, >= 1.239.0)
13 | aws-sigv4 (~> 1.1)
14 | jmespath (~> 1.0)
15 | aws-sdk-kms (1.35.0)
16 | aws-sdk-core (~> 3, >= 3.99.0)
17 | aws-sigv4 (~> 1.1)
18 | aws-sdk-s3 (1.72.0)
19 | aws-sdk-core (~> 3, >= 3.102.1)
20 | aws-sdk-kms (~> 1)
21 | aws-sigv4 (~> 1.1)
22 | aws-sigv4 (1.2.1)
23 | aws-eventstream (~> 1, >= 1.0.2)
24 | babosa (1.0.3)
25 | claide (1.0.3)
26 | colored (1.2)
27 | colored2 (3.1.2)
28 | commander-fastlane (4.4.6)
29 | highline (~> 1.7.2)
30 | declarative (0.0.20)
31 | declarative-option (0.1.0)
32 | digest-crc (0.5.1)
33 | domain_name (0.5.20190701)
34 | unf (>= 0.0.5, < 1.0.0)
35 | dotenv (2.7.5)
36 | emoji_regex (1.0.1)
37 | excon (0.75.0)
38 | faraday (1.0.1)
39 | multipart-post (>= 1.2, < 3)
40 | faraday-cookie_jar (0.0.6)
41 | faraday (>= 0.7.4)
42 | http-cookie (~> 1.0.0)
43 | faraday_middleware (1.0.0)
44 | faraday (~> 1.0)
45 | fastimage (2.1.7)
46 | fastlane (2.149.1)
47 | CFPropertyList (>= 2.3, < 4.0.0)
48 | addressable (>= 2.3, < 3.0.0)
49 | aws-sdk-s3 (~> 1.0)
50 | babosa (>= 1.0.2, < 2.0.0)
51 | bundler (>= 1.12.0, < 3.0.0)
52 | colored
53 | commander-fastlane (>= 4.4.6, < 5.0.0)
54 | dotenv (>= 2.1.1, < 3.0.0)
55 | emoji_regex (>= 0.1, < 2.0)
56 | excon (>= 0.71.0, < 1.0.0)
57 | faraday (>= 0.17, < 2.0)
58 | faraday-cookie_jar (~> 0.0.6)
59 | faraday_middleware (>= 0.13.1, < 2.0)
60 | fastimage (>= 2.1.0, < 3.0.0)
61 | gh_inspector (>= 1.1.2, < 2.0.0)
62 | google-api-client (>= 0.37.0, < 0.39.0)
63 | google-cloud-storage (>= 1.15.0, < 2.0.0)
64 | highline (>= 1.7.2, < 2.0.0)
65 | json (< 3.0.0)
66 | jwt (~> 2.1.0)
67 | mini_magick (>= 4.9.4, < 5.0.0)
68 | multi_xml (~> 0.5)
69 | multipart-post (~> 2.0.0)
70 | plist (>= 3.1.0, < 4.0.0)
71 | public_suffix (~> 2.0.0)
72 | rubyzip (>= 1.3.0, < 2.0.0)
73 | security (= 0.1.3)
74 | simctl (~> 1.6.3)
75 | slack-notifier (>= 2.0.0, < 3.0.0)
76 | terminal-notifier (>= 2.0.0, < 3.0.0)
77 | terminal-table (>= 1.4.5, < 2.0.0)
78 | tty-screen (>= 0.6.3, < 1.0.0)
79 | tty-spinner (>= 0.8.0, < 1.0.0)
80 | word_wrap (~> 1.0.0)
81 | xcodeproj (>= 1.13.0, < 2.0.0)
82 | xcpretty (~> 0.3.0)
83 | xcpretty-travis-formatter (>= 0.0.3)
84 | gh_inspector (1.1.3)
85 | google-api-client (0.38.0)
86 | addressable (~> 2.5, >= 2.5.1)
87 | googleauth (~> 0.9)
88 | httpclient (>= 2.8.1, < 3.0)
89 | mini_mime (~> 1.0)
90 | representable (~> 3.0)
91 | retriable (>= 2.0, < 4.0)
92 | signet (~> 0.12)
93 | google-cloud-core (1.5.0)
94 | google-cloud-env (~> 1.0)
95 | google-cloud-errors (~> 1.0)
96 | google-cloud-env (1.3.2)
97 | faraday (>= 0.17.3, < 2.0)
98 | google-cloud-errors (1.0.1)
99 | google-cloud-storage (1.26.2)
100 | addressable (~> 2.5)
101 | digest-crc (~> 0.4)
102 | google-api-client (~> 0.33)
103 | google-cloud-core (~> 1.2)
104 | googleauth (~> 0.9)
105 | mini_mime (~> 1.0)
106 | googleauth (0.13.0)
107 | faraday (>= 0.17.3, < 2.0)
108 | jwt (>= 1.4, < 3.0)
109 | memoist (~> 0.16)
110 | multi_json (~> 1.11)
111 | os (>= 0.9, < 2.0)
112 | signet (~> 0.14)
113 | highline (1.7.10)
114 | http-cookie (1.0.3)
115 | domain_name (~> 0.5)
116 | httpclient (2.8.3)
117 | jmespath (1.4.0)
118 | json (2.3.0)
119 | jwt (2.1.0)
120 | memoist (0.16.2)
121 | mini_magick (4.10.1)
122 | mini_mime (1.0.2)
123 | multi_json (1.14.1)
124 | multi_xml (0.6.0)
125 | multipart-post (2.0.0)
126 | nanaimo (0.2.6)
127 | naturally (2.2.0)
128 | os (1.1.0)
129 | plist (3.5.0)
130 | public_suffix (2.0.5)
131 | representable (3.0.4)
132 | declarative (< 0.1.0)
133 | declarative-option (< 0.2.0)
134 | uber (< 0.2.0)
135 | retriable (3.1.2)
136 | rouge (2.0.7)
137 | rubyzip (1.3.0)
138 | security (0.1.3)
139 | signet (0.14.0)
140 | addressable (~> 2.3)
141 | faraday (>= 0.17.3, < 2.0)
142 | jwt (>= 1.5, < 3.0)
143 | multi_json (~> 1.10)
144 | simctl (1.6.8)
145 | CFPropertyList
146 | naturally
147 | slack-notifier (2.3.2)
148 | terminal-notifier (2.0.0)
149 | terminal-table (1.8.0)
150 | unicode-display_width (~> 1.1, >= 1.1.1)
151 | tty-cursor (0.7.1)
152 | tty-screen (0.8.0)
153 | tty-spinner (0.9.3)
154 | tty-cursor (~> 0.7)
155 | uber (0.1.0)
156 | unf (0.1.4)
157 | unf_ext
158 | unf_ext (0.0.7.7)
159 | unicode-display_width (1.7.0)
160 | word_wrap (1.0.0)
161 | xcodeproj (1.17.0)
162 | CFPropertyList (>= 2.3.3, < 4.0)
163 | atomos (~> 0.1.3)
164 | claide (>= 1.0.2, < 2.0)
165 | colored2 (~> 3.1)
166 | nanaimo (~> 0.2.6)
167 | xcpretty (0.3.0)
168 | rouge (~> 2.0.7)
169 | xcpretty-travis-formatter (1.0.0)
170 | xcpretty (~> 0.2, >= 0.0.7)
171 |
172 | PLATFORMS
173 | ruby
174 |
175 | DEPENDENCIES
176 | fastlane (~> 2.140)
177 |
178 | BUNDLED WITH
179 | 1.16.2
180 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2020 SPRING AS Digital News Media GmbH
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 |
--------------------------------------------------------------------------------
/MIGRATING.md:
--------------------------------------------------------------------------------
1 | ## Migrating from 0.7 to 0.8
2 |
3 | ### `Promise` now has only an empty `init`.
4 |
5 | If you used one of the convenience `init` (with `value:`, with `error:` or with `value:error:`), they now moved to `Future`.
6 |
7 | ```swift
8 | // Before
9 | let future = Promise(value: 10).future
10 |
11 | // Now
12 | let future = Future(10)
13 | ```
14 |
15 | ```swift
16 | // Before
17 | let future = Promise(error: MyError.SomeError).future
18 |
19 | // Now
20 | let future = Future(MyError.SomeError)
21 | ```
22 |
23 | ```swift
24 | // Before
25 | let future = Promise(value: someOptionalInt, error: MyError.InvalidConversion).future
26 |
27 | // Now
28 | let future = Future(value: someOptionalInt, error: MyError.InvalidConversion)
29 | ```
--------------------------------------------------------------------------------
/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "object": {
3 | "pins": [
4 | {
5 | "package": "Nimble",
6 | "repositoryURL": "https://github.com/Quick/Nimble.git",
7 | "state": {
8 | "branch": null,
9 | "revision": "2b1809051b4a65c1d7f5233331daa24572cd7fca",
10 | "version": "8.1.1"
11 | }
12 | },
13 | {
14 | "package": "Quick",
15 | "repositoryURL": "https://github.com/Quick/Quick.git",
16 | "state": {
17 | "branch": null,
18 | "revision": "09b3becb37cb2163919a3842a4c5fa6ec7130792",
19 | "version": "2.2.1"
20 | }
21 | }
22 | ]
23 | },
24 | "version": 1
25 | }
26 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.0
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "PiedPiper",
7 | platforms: [
8 | .iOS(.v10),
9 | .macOS(.v10_12),
10 | .tvOS(.v10),
11 | .watchOS(.v3)
12 | ],
13 | products: [
14 | .library(
15 | name: "PiedPiper",
16 | targets: ["PiedPiper"]),
17 | ],
18 | dependencies: [
19 | .package(url: "https://github.com/Quick/Quick.git", .upToNextMajor(from: "3.0.0")),
20 | .package(url: "https://github.com/Quick/Nimble.git", .upToNextMajor(from: "8.1.0"))
21 | ],
22 | targets: [
23 | .target(
24 | name: "PiedPiper",
25 | path: "Sources"
26 | ),
27 | .testTarget(
28 | name: "PiedPiperTests",
29 | dependencies: [
30 | "PiedPiper",
31 | "Quick",
32 | "Nimble"
33 | ],
34 | path: "Tests"
35 | ),
36 | ],
37 | swiftLanguageVersions: [.v5]
38 | )
39 |
--------------------------------------------------------------------------------
/PiedPiper.playground/Contents.swift:
--------------------------------------------------------------------------------
1 | import PiedPiper
2 |
3 | func testPromise() -> Promise {
4 | return Promise()
5 | }
6 |
7 | let test = testPromise()
8 |
9 | test.onSuccess { value in
10 | let success = value
11 | print("Succeeded with value \(success)")
12 | }.onFailure { err in
13 | let failure = err
14 | print("Failed with error \(failure)")
15 | }
16 |
17 | // Pick your poison, but only one!
18 | test.succeed(102)
19 | //test.fail(NSError(domain: "Test", code: 10, userInfo: nil))
20 |
21 | // Async
22 |
23 | GCD.background { () -> Int in
24 | print("The result of this computation...")
25 | return 10
26 | }.main { result in
27 | let magic = result
28 | print("...goes straight here! \(magic)")
29 | }
30 |
31 | // Function composition
32 |
33 | func randomInt() -> Int {
34 | return 4 //Guaranteed random, inspired by http://xkcd.com/221/
35 | }
36 |
37 | func stringifyInt(number: Int) -> String {
38 | return "\(number)"
39 | }
40 |
41 | func helloString(input: String) -> String {
42 | return "Hello \(input)"
43 | }
44 |
45 | let composition = randomInt >>> stringifyInt >>> helloString
46 |
--------------------------------------------------------------------------------
/PiedPiper.playground/Sources/SupportCode.swift:
--------------------------------------------------------------------------------
1 | //
2 | // This file (and all other Swift source files in the Sources directory of this playground) will be precompiled into a framework which is automatically made available to Carlos.playground.
3 | //
4 |
5 | import PlaygroundSupport
6 |
7 | public func sharedSubfolder() -> String {
8 | return "\(PlaygroundSupport.playgroundSharedDataDirectory)/com.carlos.cache"
9 | }
10 |
11 | public func initializePlayground() {
12 | PlaygroundPage.current.needsIndefiniteExecution = true
13 | }
14 |
--------------------------------------------------------------------------------
/PiedPiper.playground/contents.xcplayground:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/PiedPiper.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # Be sure to run `pod lib lint CarlosFutures.podspec' to ensure this is a
3 | # valid spec and remove all comments before submitting the spec.
4 | #
5 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html
6 | #
7 |
8 | Pod::Spec.new do |s|
9 | s.name = "PiedPiper"
10 | s.version = "0.11.0"
11 | s.summary = "Asynchronous code made easy."
12 | s.description = <<-DESC
13 | Pied Piper is a small set of functions to write easy asynchronous code through Futures, Promises and some GCD love for your iOS, watchOS 3, tvOS and Mac OS X applications.
14 | DESC
15 | s.homepage = "https://github.com/spring-media/PiedPiper"
16 | s.license = 'MIT'
17 | s.author = { "Vittorio Monaco" => "vittorio.monaco1@gmail.com" }
18 | s.source = { :git => "https://github.com/spring-media/PiedPiper.git", :tag => s.version.to_s }
19 | s.swift_versions = '5.0'
20 |
21 | s.ios.deployment_target = '10.0'
22 | s.osx.deployment_target = '10.12'
23 | s.watchos.deployment_target = '3.0'
24 | s.tvos.deployment_target = '10.0'
25 |
26 | s.requires_arc = true
27 |
28 | s.source_files = 'Sources/PiedPiper/*.swift'
29 | end
30 |
--------------------------------------------------------------------------------
/PiedPiper.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/PiedPiper.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/PiedPiper.xcodeproj/xcshareddata/xcschemes/PiedPiper iOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
33 |
34 |
38 |
39 |
40 |
41 |
47 |
48 |
49 |
50 |
52 |
58 |
59 |
60 |
61 |
62 |
72 |
73 |
79 |
80 |
86 |
87 |
88 |
89 |
91 |
92 |
95 |
96 |
97 |
--------------------------------------------------------------------------------
/PiedPiper.xcodeproj/xcshareddata/xcschemes/PiedPiper macOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
39 |
40 |
41 |
42 |
44 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/PiedPiper.xcodeproj/xcshareddata/xcschemes/PiedPiper tvOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
32 |
33 |
39 |
40 |
41 |
42 |
44 |
50 |
51 |
52 |
53 |
54 |
64 |
65 |
71 |
72 |
78 |
79 |
80 |
81 |
83 |
84 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/PiedPiper.xcodeproj/xcshareddata/xcschemes/PiedPiper watchOS.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
42 |
43 |
53 |
54 |
60 |
61 |
67 |
68 |
69 |
70 |
72 |
73 |
76 |
77 |
78 |
--------------------------------------------------------------------------------
/PiedPiper.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/PiedPiper.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/FunctionComposition.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | infix operator >>>: CompositionPrecedence
4 | precedencegroup CompositionPrecedence {
5 | associativity: left
6 | higherThan: AssignmentPrecedence
7 | }
8 |
9 | /**
10 | Composes two sync closures
11 |
12 | - parameter f: A closure taking an A parameter and returning an Optional
13 | - parameter g: A closure taking a B parameter and returning an Optional
14 |
15 | - returns: A closure taking an A parameter and returning an Optional obtained by combining f and g in a way similar to g(f(x))
16 | */
17 | public func >>> (f: @escaping (A) -> B?, g: @escaping (B) -> C?) -> (A) -> C? {
18 | return { x in
19 | if let fx = f(x) {
20 | return g(fx)
21 | } else {
22 | return nil
23 | }
24 | }
25 | }
26 |
27 | /**
28 | Composes two sync closures
29 |
30 | - parameter f: A closure taking an A parameter and returning a value of type B
31 | - parameter g: A closure taking a B parameter and returning a value of type C
32 |
33 | - returns: A closure taking an A parameter and returning a value of type C obtained by combining f and g through g(f(x))
34 | */
35 | public func >>> (f: @escaping (A) -> B, g: @escaping (B) -> C) -> (A) -> C {
36 | return { x in
37 | g(f(x))
38 | }
39 | }
40 |
41 | /**
42 | Composes two async (Future) closures
43 |
44 | - parameter f: A closure taking an A parameter and returning a Future (basically a future for a B return type)
45 | - parameter g: A closure taking a B parameter and returning a Future (basically a future for a C return type)
46 |
47 | - returns: A closure taking an A parameter and returning a Future (basically a future for a C return type) obtained by combining f and g in a way similar to g(f(x)) (if the closures were sync)
48 | */
49 | public func >>> (f: @escaping (A) -> Future, g: @escaping (B) -> Future) -> (A) -> Future {
50 | return { param in
51 | return f(param).flatMap(g)
52 | }
53 | }
54 |
55 | //Expose later if it makes sense to
56 | /**
57 | Composes two async closures
58 |
59 | - parameter f: A closure taking an A parameter and a completion callback taking an Optional and returning Void
60 | - parameter g: A closure taking a B parameter and a completion callback taking an Optional and returning Void
61 |
62 | - returns: A closure taking an A parameter and a completion callback taking an Optional and returning Void obtained by combining f and g in a way similar to g(f(x)) (if the closures were sync)
63 | */
64 | internal func >>> (f: @escaping (A, (B?) -> Void) -> Void, g: @escaping (B, (C?) -> Void) -> Void) -> (A, (C?) -> Void) -> Void {
65 | return { x, completion in
66 | f(x) { fx in
67 | if let fx = fx {
68 | g(fx) { result in
69 | completion(result)
70 | }
71 | } else {
72 | completion(nil)
73 | }
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+All.swift:
--------------------------------------------------------------------------------
1 | extension Sequence where Iterator.Element: Async {
2 | /**
3 | - returns: A Future that succeeds when all the Futures contained in this sequence succeed, and fails when one of the Futures contained in this sequence fails.
4 | */
5 | public func all() -> Future<()> {
6 | return mergeAll().map { _ in }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Filter.swift:
--------------------------------------------------------------------------------
1 | /// Errors that can arise when filtering Futures
2 | public enum FutureFilteringError: Error {
3 | /// When the filter condition is not satisfied
4 | case conditionUnsatisfied
5 | }
6 |
7 | extension Future {
8 | /**
9 | Filters the Future with a condition
10 |
11 | - parameter filter: The condition closure that determines whether the result of the Future is valid or not
12 |
13 | - result: A new Future that only succeeds if the original Future succeeds with a value that passes the given condition
14 | */
15 | public func filter(_ filter: @escaping (T) -> Bool) -> Future {
16 | return _map { value, mapped in
17 | if filter(value) {
18 | mapped.succeed(value)
19 | } else {
20 | mapped.fail(FutureFilteringError.conditionUnsatisfied)
21 | }
22 | }
23 | }
24 |
25 | /**
26 | Filters the Future with a condition Future
27 |
28 | - parameter filter: The condition Future that determines whether the result of the Future is valid or not
29 |
30 | - result: A new Future that only succeeds if the original Future succeeds with a value that succeeds the Future returned by the given condition
31 | */
32 | public func filter(_ filter: @escaping (T) -> Future) -> Future {
33 | return _map { value, mapped in
34 | filter(value).onCompletion { filterResult in
35 | switch filterResult {
36 | case .success(let result):
37 | if result {
38 | mapped.succeed(value)
39 | } else {
40 | mapped.fail(FutureFilteringError.conditionUnsatisfied)
41 | }
42 | case .error(let error):
43 | mapped.fail(error)
44 | case .cancelled:
45 | mapped.cancel()
46 | }
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+FlatMap.swift:
--------------------------------------------------------------------------------
1 | /// Errors that can arise when mapping Futures
2 | public enum FutureMappingError: Error {
3 | /// When the value can't be mapped
4 | case cantMapValue
5 | }
6 |
7 | extension Future {
8 | /**
9 | Maps a Future into a Future through a function that takes a value of type T and returns a value of type U?
10 |
11 | - parameter f: The closure that takes a value of type T and returns a value of type U?
12 |
13 | - returns: A new Future that will behave as the original one w.r.t. cancelation and failure, but will succeed with a value of type U obtained through the given closure, unless the latter returns nil. In this case, the new Future will fail
14 | */
15 | public func flatMap(_ f: @escaping (T) -> U?) -> Future {
16 | return _map { value, mapped in
17 | if let mappedValue = f(value) {
18 | mapped.succeed(mappedValue)
19 | } else {
20 | mapped.fail(FutureMappingError.cantMapValue)
21 | }
22 | }
23 | }
24 |
25 | /**
26 | Maps a Future into a Future through a function that takes a value of type T and returns a Result
27 |
28 | - parameter f: The closure that takes a value of type T and returns a Result
29 |
30 | - returns: A new Future that will behave as the original one w.r.t. cancelation and failure, but will succeed with a value of type U obtained through the given closure if the returned Result is a success. Otherwise, the new Future will fail or get canceled depending on the state of the returned Result
31 | */
32 | public func flatMap(_ f: @escaping (T) -> Result) -> Future {
33 | return _map { value, mapped in
34 | mapped.mimic(f(value))
35 | }
36 | }
37 |
38 | /**
39 | Maps a Future into a Future through a function that takes a value of type T and returns a Future
40 |
41 | - parameter f: The closure that takes a value of type T and returns a Future
42 |
43 | - returns: A new Future that will behave as the original one w.r.t. cancelation and failure, but will succeed with a value of type U when the given Future will succeed. If the given Future fails or is canceled, the new Future will do so too.
44 | */
45 | public func flatMap(_ f: @escaping (T) -> Future) -> Future {
46 | return _map { value, mapped in
47 | mapped.mimic(f(value))
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Map.swift:
--------------------------------------------------------------------------------
1 | extension Future {
2 | func _map(_ handler: @escaping (T, Promise) -> Void) -> Future {
3 | let mapped = Promise()
4 |
5 | self.onCompletion { result in
6 | switch result {
7 | case .success(let value):
8 | handler(value, mapped)
9 | case .error(let error):
10 | mapped.fail(error)
11 | case .cancelled:
12 | mapped.cancel()
13 | }
14 | }
15 |
16 | mapped.onCancel(cancel)
17 |
18 | return mapped.future
19 | }
20 |
21 | /**
22 | Maps a Future into a Future through a function that takes a value of type T and returns a value of type U
23 |
24 | - parameter f: The closure that takes a value of type T and returns a value of type U
25 |
26 | - returns: A new Future that will behave as the original one w.r.t. cancelation and failure, but will succeed with a value of type U obtained through the given closure
27 | */
28 | public func map(_ f: @escaping (T) -> U) -> Future {
29 | return _map { value, mapped in
30 | mapped.succeed(f(value))
31 | }
32 | }
33 |
34 | /**
35 | Maps a Future into a Future through a function that takes a value of type T and returns a value of type U
36 |
37 | - parameter f: The closure that takes a value of type T and returns a value of type U. Please note the closure can throw
38 |
39 | - returns: A new Future that will behave as the original one w.r.t. cancelation and failure, but will succeed with a value of type U obtained through the given closure, unless the latter throws. In this case, the new Future will fail
40 | */
41 | public func map(_ f: @escaping (T) throws -> U) -> Future {
42 | return _map { value, mapped in
43 | do {
44 | mapped.succeed(try f(value))
45 | } catch {
46 | mapped.fail(error)
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+MergeAll.swift:
--------------------------------------------------------------------------------
1 | extension Sequence where Iterator.Element: Async {
2 | /**
3 | Merges this sequence of Futures into a single one containing the list of the results of each Future
4 |
5 | - returns: A Future that will succeed with the list of results of the single Futures contained in this Sequence. The resulting Future will fail or be canceled if one of the elements of this sequence fails or is canceled
6 | */
7 | @available(*, deprecated)
8 | public func merge() -> Future<[Iterator.Element.Value]> {
9 | return mergeAll()
10 | }
11 |
12 | /**
13 | Merges this sequence of Futures into a single one containing the list of the results of each Future
14 |
15 | - returns: A Future that will succeed with the list of results of the single Futures contained in this Sequence. The resulting Future will fail or be canceled if one of the elements of this sequence fails or is canceled
16 | */
17 | public func mergeAll() -> Future<[Iterator.Element.Value]> {
18 | let result = reduce([], combine: { accumulator, value in
19 | accumulator + [value]
20 | })
21 |
22 | result.onCancel {
23 | self.forEach {
24 | $0.future.cancel()
25 | }
26 | }
27 |
28 | return result
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+MergeSome.swift:
--------------------------------------------------------------------------------
1 | extension Sequence where Iterator.Element: Async {
2 | /**
3 | Merges this sequence of Futures into a single one containing the list of the results of each Future
4 |
5 | - returns: A Future that will succeed with the list of results of the single Futures contained in this Sequence. The resulting Future will fail or be canceled if one of the elements of this sequence fails or is canceled
6 | */
7 | public func mergeSome() -> Future<[Iterator.Element.Value]> {
8 | let result = reduce(Future([])) { accumulator, value in
9 | accumulator.flatMap { reduced in
10 | value.future.map { mapped in
11 | reduced + [mapped]
12 | }.recover(reduced)
13 | }
14 | }
15 |
16 | result.onCancel {
17 | self.forEach {
18 | $0.future.cancel()
19 | }
20 | }
21 |
22 | return result
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Recover.swift:
--------------------------------------------------------------------------------
1 | extension Future {
2 | private func _recover(_ handler: @escaping (Promise) -> Void) -> Future {
3 | let recovered = Promise()
4 |
5 | onCompletion { result in
6 | switch result {
7 | case .success(let value):
8 | recovered.succeed(value)
9 | case .error:
10 | handler(recovered)
11 | case .cancelled:
12 | recovered.cancel()
13 | }
14 | }
15 |
16 | return recovered.future
17 | }
18 |
19 | /**
20 | Recovers this Future so that if it fails it can actually use the "rescue value"
21 |
22 | - parameter handler: The closure that provides the rescue value
23 |
24 | - returns: A new Future that will behave as this Future, except when this Future fails. In that case, it will succeed with the rescue value
25 | */
26 | public func recover(_ handler: @escaping () -> T) -> Future {
27 | return _recover { recovered in
28 | recovered.succeed(handler())
29 | }
30 | }
31 |
32 | /**
33 | Recovers this Future so that if it fails it can actually use the "rescue value"
34 |
35 | - parameter handler: The rescue value
36 |
37 | - returns: A new Future that will behave as this Future, except when this Future fails. In that case, it will succeed with the rescue value
38 | */
39 | public func recover(_ value: T) -> Future {
40 | return recover({ value })
41 | }
42 |
43 | /**
44 | Recovers this Future so that if it fails it can actually use a "rescue value"
45 |
46 | - parameter handler: The closure that provides a Future that will try to provide a rescue value
47 |
48 | - returns: A new Future that will behave as this Future, except when this Future fails. In that case, it will mimic the outcome of the Future provided by the handler
49 | */
50 | public func recover(_ handler: @escaping () -> Future) -> Future {
51 | return _recover { recovered in
52 | recovered.mimic(handler())
53 | }
54 | }
55 |
56 | /**
57 | Recovers this Future so that if it fails it can actually use a "rescue value"
58 |
59 | - parameter handler: The closure that provides a Result that will try to provide a rescue value
60 |
61 | - returns: A new Future that will behave as this Future, except when this Future fails. In that case, it will mimic the outcome of the Result provided by the handler
62 | */
63 | public func recover(_ handler: @escaping () -> Result) -> Future {
64 | return _recover { recovered in
65 | recovered.mimic(handler())
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Reduce.swift:
--------------------------------------------------------------------------------
1 | extension Sequence where Iterator.Element: Async {
2 | /**
3 | Reduces a sequence of Future`` into a single Future`` through a closure that takes a value T and the current accumulated value of the previous iterations (starting from initialValue and following the order of the sequence) and returns a value of type U
4 |
5 | - parameter initialValue: The initial value for the reduction of this sequence
6 | - parameter combine: The closure used to reduce the sequence
7 |
8 | - returns: a new Future`` that will succeed when all the Future`` of this array will succeed, with a value obtained through the execution of the combine closure on each result of the original Futures in the same order. The result will fail or get canceled if one of the original futures fails or gets canceled
9 | */
10 | public func reduce(_ initialValue: U, combine: @escaping (U, Iterator.Element.Value) -> U) -> Future {
11 | let result = reduce(Future(initialValue)) { accumulator, value in
12 | accumulator.flatMap { reduced in
13 | value.future.map { mapped in
14 | combine(reduced, mapped)
15 | }
16 | }
17 | }
18 |
19 | result.onCancel {
20 | self.forEach {
21 | $0.future.cancel()
22 | }
23 | }
24 |
25 | return result
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Retry.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /**
4 | Retries a given Future for a given number of times
5 |
6 | - parameter count: How many times you want the future to be retried if failed (No retries: 0)
7 | - parameter every: How much you want to wait before retrying
8 | - parameter futureClosure: The closure generating a new instance of the future to retry
9 |
10 | - returns: A future that fails if all the generated futures have failed, or succeeds if one of the generated futures succeeds
11 | */
12 | public func retry(_ count: Int, every delay: TimeInterval, futureClosure: @escaping () -> Future) -> Future {
13 | if count <= 0 {
14 | return futureClosure()
15 | }
16 |
17 | let result = Promise()
18 |
19 | result.mimic(
20 | futureClosure()
21 | .recover { () -> Future in
22 | let delayed = Promise()
23 |
24 | GCD.delay(delay, closure: {}).onSuccess {
25 | delayed.mimic(retry(count - 1, every: delay, futureClosure: futureClosure))
26 | }
27 |
28 | return delayed.future
29 | }
30 | )
31 |
32 | return result.future
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Snooze.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Future {
4 | /**
5 | Snoozes the result (.Success or .Failure) of this Future by the given time
6 |
7 | - parameter time: The number of seconds this Future's result should be snoozed for
8 |
9 | - returns: A new Future that will return the result of this Future after the given snooze time
10 | */
11 | public func snooze(_ time: TimeInterval) -> Future {
12 | let snoozed = Promise()
13 |
14 | onCompletion { _ in
15 | GCD.delay(time, closure: {
16 | snoozed.mimic(self)
17 | })
18 | }
19 |
20 | return snoozed.future
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Timeout.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum FutureError: Error {
4 | case timeout
5 | }
6 |
7 | extension Future {
8 | /**
9 | Sets a timeout before this Future has to succeed or fail
10 |
11 | - parameter timeout: The number of seconds after which this Future will fail
12 |
13 | - returns: A new Future that will time out after the given number of seconds, or will behave as this Future
14 | */
15 | public func timeout(after timeout: TimeInterval) -> Future {
16 | let timedOut = Promise().mimic(self)
17 |
18 | GCD.delay(timeout, closure: { FutureError.timeout }).onSuccess(timedOut.fail)
19 |
20 | return timedOut.future
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Traverse.swift:
--------------------------------------------------------------------------------
1 | extension Sequence {
2 | /**
3 | Maps this sequence with the provided closure generating Futures, then merges the created Futures into a single one
4 |
5 | - parameter generator: The closure that generates a Future for each element in this sequence
6 |
7 | - returns: A new Future containing the list of results of the single Futures generated through the closure. The resulting Future will fail or be canceled if one of the Futures generated through the closure fails or is canceled
8 | */
9 | public func traverse(_ generator: (Iterator.Element) -> Future) -> Future<[U]> {
10 | return map(generator).mergeAll()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+Zip.swift:
--------------------------------------------------------------------------------
1 | extension Future {
2 | /**
3 | Zips this Future with another Future`` to obtain a Future of type (T,U)
4 |
5 | - parameter other: The other Future you want to zip
6 |
7 | - returns: A new Future of type (T, U) that will only succeed if both Futures succeed. It will fail or be canceled accordingly to its components
8 | */
9 | public func zip(_ other: Future) -> Future<(T, U)> {
10 | return flatMap { thisResult in
11 | other.map { otherResult in
12 | (thisResult, otherResult)
13 | }
14 | }
15 | }
16 |
17 | /**
18 | Zips this Future with a Result`` to obtain a Future of type (T,U)
19 |
20 | - parameter other: The Result you want to zip
21 |
22 | - returns: A new Future of type (T, U) that will only succeed if both this Future and the Result succeed. It will fail or be canceled accordingly to its components
23 | */
24 | public func zip(_ other: Result) -> Future<(T, U)> {
25 | return other.flatMap { otherResult in
26 | self.map { thisResult in
27 | (thisResult, otherResult)
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future+firstCompleted.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension Sequence where Iterator.Element: Async {
4 | /**
5 | Starts a race between the Futures composing this sequence
6 |
7 | - returns: A new Future that will behave as the first completed future of this sequence
8 | */
9 | public func firstCompleted() -> Future {
10 | let result = Promise()
11 |
12 | forEach { element in
13 | result.mimic(element.future)
14 | }
15 |
16 | result.onCancel {
17 | self.forEach { item in
18 | item.future.cancel()
19 | }
20 | }
21 |
22 | return result.future
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Future.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Abstracts a Future computation so that it's easier to extend SequenceType
4 | public protocol Async {
5 | /// The generic parameter in the Future implementation
6 | associatedtype Value
7 |
8 | /// Accessor to the Future instance
9 | var future: Future { get }
10 | }
11 |
12 | public enum FutureInitializationError: Error {
13 | case closureReturnedNil
14 | }
15 |
16 | /// This class is a read-only Promise.
17 | open class Future: Async {
18 | public typealias Value = T
19 |
20 | public var future: Future {
21 | return self
22 | }
23 |
24 | private let promise: Promise
25 |
26 | init(promise: Promise) {
27 | self.promise = promise
28 | }
29 |
30 | /**
31 | Initializes a new Future and makes it immediately succeed with the given value
32 |
33 | - parameter value: The success value of the Future
34 | */
35 | public convenience init(_ value: T) {
36 | self.init(promise: Promise(value))
37 | }
38 |
39 | /**
40 | Initializes a new Future and makes it succeed (or fail) with the result of the given closure
41 |
42 | - parameter closure: The closure that will be evaluated on a background thread
43 |
44 | The initialized future will succeed if the result of the closure is .Some, and will fail with a FutureInitializationError.ClosureReturnedNil if it's .None. The future will report on the main queue
45 | */
46 | public convenience init(closure: @escaping () -> T?) {
47 | let promise = Promise()
48 |
49 | self.init(promise: promise)
50 |
51 | GCD.background {
52 | closure()
53 | }.main { result in
54 | if let result = result {
55 | promise.succeed(result)
56 | } else {
57 | promise.fail(FutureInitializationError.closureReturnedNil)
58 | }
59 | }
60 | }
61 |
62 | /**
63 | Initializes a new Future and makes it immediately succeed or fail depending on the value
64 |
65 | - parameter value: The success value of the Future, if not .None
66 | - parameter error: The error of the Future, if value is .None
67 | */
68 | public convenience init(value: T?, error: Error) {
69 | self.init(promise: Promise(value: value, error: error))
70 | }
71 |
72 | /**
73 | Initializes a new Future and makes it immediately fail with the given error
74 |
75 | - parameter error: The error of the Future
76 | */
77 | public convenience init(_ error: Error) {
78 | self.init(promise: Promise(error))
79 | }
80 |
81 | /**
82 | Cancels the Future
83 |
84 | Calling this method makes all the listeners get the onCancel callback (but not the onFailure callback)
85 | */
86 | public func cancel() {
87 | promise.cancel()
88 | }
89 |
90 | /**
91 | Adds a listener for the cancel event of this Future
92 |
93 | - parameter cancel: The closure that should be called when the Future is canceled
94 |
95 | - returns: The updated Future
96 | */
97 | @discardableResult
98 | public func onCancel(_ callback: @escaping () -> Void) -> Future {
99 | promise.onCancel(callback)
100 |
101 | return self
102 | }
103 |
104 | /**
105 | Adds a listener for the success event of this Future
106 |
107 | - parameter success: The closure that should be called when the Future succeeds, taking the value as a parameter
108 |
109 | - returns: The updated Future
110 | */
111 | @discardableResult
112 | public func onSuccess(_ callback: @escaping (T) -> Void) -> Future {
113 | promise.onSuccess(callback)
114 |
115 | return self
116 | }
117 |
118 | /**
119 | Adds a listener for the failure event of this Future
120 |
121 | - parameter success: The closure that should be called when the Future fails, taking the error as a parameter
122 |
123 | - returns: The updated Future
124 | */
125 | @discardableResult
126 | public func onFailure(_ callback: @escaping (Error) -> Void) -> Future {
127 | promise.onFailure(callback)
128 |
129 | return self
130 | }
131 |
132 | /**
133 | Adds a listener for both success and failure events of this Future
134 |
135 | - parameter completion: The closure that should be called when the Future completes (succeeds or fails), taking a Result with value .Success in case the Future succeeded and .error in case the Future failed as parameter. If the Future is canceled, the result will be .Cancelled
136 |
137 | - returns: The updated Future
138 | */
139 | @discardableResult
140 | public func onCompletion(_ completion: @escaping (Result) -> Void) -> Future {
141 | promise.onCompletion(completion)
142 |
143 | return self
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/GCD.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Internal struct for easy GCD usage
4 | public struct GCD: GCDQueue {
5 | fileprivate static let mainQueue = GCD(queue: DispatchQueue.main)
6 | fileprivate static let backgroundQueue = GCD(queue: DispatchQueue.global(qos: .default))
7 |
8 | /**
9 | Asynchronously dispatches a closure on the main queue
10 |
11 | - parameter closure: The closure you want to dispatch on the queue
12 |
13 | - returns: The result of the execution of the closure
14 | */
15 | @discardableResult
16 | public static func main(_ closure: @escaping (() -> T)) -> AsyncDispatch {
17 | return mainQueue.async(closure)
18 | }
19 |
20 | /**
21 | Asynchronously dispatches a closure on the default priority background queue
22 |
23 | - parameter closure: The closure you want to dispatch on the queue
24 |
25 | - returns: The result of the execution of the closure
26 | */
27 | @discardableResult
28 | static public func background(_ closure: @escaping (() -> T)) -> AsyncDispatch {
29 | return backgroundQueue.async(closure)
30 | }
31 |
32 | /**
33 | Creates a new serial queue with the given name
34 |
35 | - parameter name: The name for the new queue
36 |
37 | - returns: The newly created GCDQueue
38 | */
39 | public static func serial(_ name: String) -> GCDQueue {
40 | return GCD(queue: DispatchQueue(label: name))
41 | }
42 |
43 | static func delay(_ time: TimeInterval) -> Future<()> {
44 | return delay(time) { () }
45 | }
46 |
47 | @discardableResult
48 | static func delay(_ time: TimeInterval, closure: @escaping () -> T) -> Future {
49 | let result = Promise()
50 |
51 | let time = DispatchTime.now() + Double(Int64(time * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
52 | DispatchQueue.main.asyncAfter(deadline: time, execute: {
53 | result.succeed(closure())
54 | })
55 |
56 | return result.future
57 | }
58 |
59 | /**
60 | Instantiates a new GCD value with a custom dispatch queue
61 |
62 | - parameter queue: The custom dispatch queue you want to use with this GCD value
63 | */
64 | public init(queue: DispatchQueue) {
65 | self.queue = queue
66 | }
67 |
68 | /// The GCD queue associated to this GCD value
69 | public let queue: DispatchQueue
70 | }
71 |
72 | /// An async dispatch operation
73 | open class AsyncDispatch {
74 | /// The inner async operation
75 | private(set) open var future: Future
76 |
77 | init(operation: Future) {
78 | self.future = operation
79 | }
80 |
81 | private func dispatchClosureAsync(_ closure: @escaping (T) -> O, queue: GCDQueue) -> AsyncDispatch {
82 | let innerResult = Promise()
83 | let result = AsyncDispatch(operation: innerResult.future)
84 |
85 | future.onSuccess { value in
86 | queue.async {
87 | innerResult.succeed(closure(value))
88 | }
89 | }
90 |
91 | return result
92 | }
93 |
94 | /**
95 | Chains a closure taking a T input and returning an O output on the main queue
96 |
97 | - parameter closure: The closure you want to dispatch on the main queue
98 |
99 | - returns: An AsyncDispatch object. You can keep chaining async calls on this object
100 | */
101 | @discardableResult
102 | public func main(_ closure: @escaping (T) -> O) -> AsyncDispatch {
103 | return dispatchClosureAsync(closure, queue: GCD.mainQueue)
104 | }
105 |
106 | /**
107 | Chains a closure taking a T input and returning an O output on a background queue
108 |
109 | - parameter closure: The closure you want to dispatch on the background queue
110 |
111 | - returns: An AsyncDispatch object. You can keep chaining async calls on this object
112 | */
113 | @discardableResult
114 | public func background(_ closure: @escaping (T) -> O) -> AsyncDispatch {
115 | return dispatchClosureAsync(closure, queue: GCD.backgroundQueue)
116 | }
117 | }
118 |
119 | /// Abstracts a GCD queue
120 | public protocol GCDQueue {
121 | /// The underlying dispatch_queue_t
122 | var queue: DispatchQueue { get }
123 | }
124 |
125 | extension GCDQueue {
126 |
127 | /**
128 | Dispatches a given closure on the queue asynchronously
129 |
130 | - parameter closure: The closure you want to dispatch on the queue
131 |
132 | - returns: An AsyncDispatch object. You can keep chaining async calls on this object
133 | */
134 | @discardableResult
135 | public func async(_ closure: @escaping () -> T) -> AsyncDispatch {
136 | let innerResult = Promise()
137 | let result = AsyncDispatch(operation: innerResult.future)
138 |
139 | queue.async {
140 | innerResult.succeed(closure())
141 | }
142 |
143 | return result
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | $(MARKETING_VERSION)
19 | CFBundleVersion
20 | $(CURRENT_PROJECT_VERSION)
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/PiedPiper.h:
--------------------------------------------------------------------------------
1 | @import Foundation;
2 |
3 | //! Project version number for PiedPiper.
4 | FOUNDATION_EXPORT double PiedPiperVersionNumber;
5 |
6 | //! Project version string for PiedPiper.
7 | FOUNDATION_EXPORT const unsigned char PiedPiperVersionString[];
8 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Promise.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// This class is a Future computation, where you can attach failure and success callbacks.
4 | open class Promise: Async {
5 | public typealias Value = T
6 |
7 | private var failureListeners: [(Error) -> Void] = []
8 | private var successListeners: [(T) -> Void] = []
9 | private var cancelListeners: [() -> Void] = []
10 | private var error: Error?
11 | private var value: T?
12 | private var canceled = false
13 | private let successLock: ReadWriteLock = PThreadReadWriteLock()
14 | private let failureLock: ReadWriteLock = PThreadReadWriteLock()
15 | private let cancelLock: ReadWriteLock = PThreadReadWriteLock()
16 |
17 | /// The Future associated to this Promise
18 | private weak var _future: Future?
19 | public var future: Future {
20 | if let _future = _future {
21 | return _future
22 | }
23 |
24 | let newFuture = Future(promise: self)
25 | _future = newFuture
26 | return newFuture
27 | }
28 |
29 | /**
30 | Creates a new Promise
31 | */
32 | public init() {}
33 |
34 | convenience init(_ value: T) {
35 | self.init()
36 |
37 | succeed(value)
38 | }
39 |
40 | convenience init(value: T?, error: Error) {
41 | self.init()
42 |
43 | if let value = value {
44 | succeed(value)
45 | } else {
46 | fail(error)
47 | }
48 | }
49 |
50 | convenience init(_ error: Error) {
51 | self.init()
52 |
53 | fail(error)
54 | }
55 |
56 | /**
57 | Mimics the given Future, so that it fails or succeeds when the stamps does so (in addition to its pre-existing behavior)
58 | Moreover, if the mimiced Future is canceled, the Promise will also cancel itself
59 |
60 | - parameter stamp: The Future to mimic
61 |
62 | - returns: The Promise itself
63 | */
64 | @discardableResult
65 | public func mimic(_ stamp: Future) -> Promise {
66 | stamp.onCompletion { result in
67 | switch result {
68 | case .success(let value):
69 | self.succeed(value)
70 | case .error(let error):
71 | self.fail(error)
72 | case .cancelled:
73 | self.cancel()
74 | }
75 | }
76 |
77 | return self
78 | }
79 |
80 | /**
81 | Mimics the given Result, so that it fails or succeeds when the stamps does so (in addition to its pre-existing behavior)
82 | Moreover, if the mimiced Result is canceled, the Promise will also cancel itself
83 |
84 | - parameter stamp: The Result to mimic
85 |
86 | - returns: The Promise itself
87 | */
88 | @discardableResult
89 | public func mimic(_ stamp: Result) -> Promise {
90 | switch stamp {
91 | case .success(let value):
92 | self.succeed(value)
93 | case .error(let error):
94 | self.fail(error)
95 | case .cancelled:
96 | self.cancel()
97 | }
98 |
99 | return self
100 | }
101 |
102 | private func clearListeners() {
103 | successLock.withWriteLock {
104 | successListeners.removeAll()
105 | }
106 |
107 | failureLock.withWriteLock {
108 | failureListeners.removeAll()
109 | }
110 |
111 | cancelLock.withWriteLock {
112 | cancelListeners.removeAll()
113 | }
114 | }
115 |
116 | /**
117 | Makes the Promise succeed with a value
118 |
119 | - parameter value: The value found for the Promise
120 |
121 | Calling this method makes all the listeners get the onSuccess callback
122 | */
123 | public func succeed(_ value: T) {
124 | guard self.error == nil else { return }
125 | guard self.value == nil else { return }
126 | guard self.canceled == false else { return }
127 |
128 | self.value = value
129 |
130 | successLock.withReadLock {
131 | successListeners.forEach { listener in
132 | listener(value)
133 | }
134 | }
135 |
136 | clearListeners()
137 | }
138 |
139 | /**
140 | Makes the Promise fail with an error
141 |
142 | - parameter error: The optional error that caused the Promise to fail
143 |
144 | Calling this method makes all the listeners get the onFailure callback
145 | */
146 | public func fail(_ error: Error) {
147 | guard self.error == nil else { return }
148 | guard self.value == nil else { return }
149 | guard self.canceled == false else { return }
150 |
151 | self.error = error
152 |
153 | failureLock.withReadLock {
154 | failureListeners.forEach { listener in
155 | listener(error)
156 | }
157 | }
158 |
159 | clearListeners()
160 | }
161 |
162 | /**
163 | Cancels the Promise
164 |
165 | Calling this method makes all the listeners get the onCancel callback (but not the onFailure callback)
166 | */
167 | public func cancel() {
168 | guard self.error == nil else { return }
169 | guard self.value == nil else { return }
170 | guard self.canceled == false else { return }
171 |
172 | canceled = true
173 |
174 | cancelLock.withReadLock {
175 | cancelListeners.forEach { listener in
176 | listener()
177 | }
178 | }
179 |
180 | clearListeners()
181 | }
182 |
183 | /**
184 | Adds a listener for the cancel event of this Promise
185 |
186 | - parameter cancel: The closure that should be called when the Promise is canceled
187 |
188 | - returns: The updated Promise
189 | */
190 | @discardableResult
191 | public func onCancel(_ callback: @escaping () -> Void) -> Promise {
192 | if canceled {
193 | callback()
194 | } else {
195 | cancelLock.withWriteLock {
196 | cancelListeners.append(callback)
197 | }
198 | }
199 |
200 | return self
201 | }
202 |
203 | /**
204 | Adds a listener for the success event of this Promise
205 |
206 | - parameter success: The closure that should be called when the Promise succeeds, taking the value as a parameter
207 |
208 | - returns: The updated Promise
209 | */
210 | @discardableResult
211 | public func onSuccess(_ callback: @escaping (T) -> Void) -> Promise {
212 | if let value = value {
213 | callback(value)
214 | } else {
215 | successLock.withWriteLock {
216 | successListeners.append(callback)
217 | }
218 | }
219 |
220 | return self
221 | }
222 |
223 | /**
224 | Adds a listener for the failure event of this Promise
225 |
226 | - parameter success: The closure that should be called when the Promise fails, taking the error as a parameter
227 |
228 | - returns: The updated Promise
229 | */
230 | @discardableResult
231 | public func onFailure(_ callback: @escaping (Error) -> Void) -> Promise {
232 | if let error = error {
233 | callback(error)
234 | } else {
235 | failureLock.withWriteLock {
236 | failureListeners.append(callback)
237 | }
238 | }
239 |
240 | return self
241 | }
242 |
243 | /**
244 | Adds a listener for both success and failure events of this Promise
245 |
246 | - parameter completion: The closure that should be called when the Promise completes (succeeds or fails), taking a result with value .Success in case the Promise succeeded and .error in case the Promise failed as parameter. If the Promise is canceled, the result will be .Cancelled
247 |
248 | - returns: The updated Promise
249 | */
250 | @discardableResult
251 | public func onCompletion(_ completion: @escaping (Result) -> Void) -> Promise {
252 | if let error = error {
253 | completion(.error(error))
254 | } else if let value = value {
255 | completion(.success(value))
256 | } else if canceled {
257 | completion(.cancelled)
258 | } else {
259 | onSuccess { completion(.success($0)) }
260 | onFailure { completion(.error($0)) }
261 | onCancel { completion(.cancelled) }
262 | }
263 |
264 | return self
265 | }
266 | }
267 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/ReadWriteLock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ReadWriteLock.swift
3 | // ReadWriteLock
4 | //
5 | // Created by John Gallagher on 7/17/14.
6 | // Copyright © 2014-2015 Big Nerd Ranch. Licensed under MIT.
7 | //
8 |
9 | import Foundation
10 |
11 | /// Abstracts a Read/Write lock
12 | public protocol ReadWriteLock {
13 | /**
14 | Executes a given closure with a read lock
15 |
16 | - parameter body: The code to execute with a read lock
17 |
18 | - returns: The result of the given code
19 | */
20 | func withReadLock( _ body: () -> T) -> T
21 |
22 | /**
23 | Executes a given closure with a write lock
24 |
25 | - parameter body: The code to execute with a write lock
26 |
27 | - returns: The result of the given code
28 | */
29 | func withWriteLock( _ body: () -> T) -> T
30 | }
31 |
32 | /// An implemenation of ReadWriteLock based on pthread, taken from https://github.com/bignerdranch/Deferred
33 | public final class PThreadReadWriteLock: ReadWriteLock {
34 | private var lock: UnsafeMutablePointer
35 |
36 | /// Instantiates a new read/write lock
37 | public init() {
38 | lock = UnsafeMutablePointer.allocate(capacity: 1)
39 | let status = pthread_rwlock_init(lock, nil)
40 | assert(status == 0)
41 | }
42 |
43 | deinit {
44 | let status = pthread_rwlock_destroy(lock)
45 | assert(status == 0)
46 | lock.deallocate()
47 | }
48 |
49 | public func withReadLock( _ body: () -> T) -> T {
50 | pthread_rwlock_rdlock(lock)
51 |
52 | defer {
53 | pthread_rwlock_unlock(lock)
54 | }
55 |
56 | return body()
57 | }
58 |
59 | public func withWriteLock( _ body: () -> T) -> T {
60 | pthread_rwlock_wrlock(lock)
61 |
62 | defer {
63 | pthread_rwlock_unlock(lock)
64 | }
65 |
66 | return body()
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Result+Filter.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Errors that can arise when filtering Results
4 | public enum ResultFilteringError: Error {
5 | /// When the filter condition is not satisfied
6 | case conditionUnsatisfied
7 | }
8 |
9 | extension Result {
10 | /**
11 | Filters this Result with the given condition
12 |
13 | - parameter condition: The condition you want to apply to the boxed value of this Result
14 |
15 | - returns: A new Result that will behave as this Result w.r.t. cancellation and failure, but will succeed if the boxed value satisfies the given condition, and fail with ResultFilteringError.ConditionUnsatisfied if the condition is not satisfied
16 | */
17 | public func filter(_ condition: (T) -> Bool) -> Result {
18 | return _map { value in
19 | if condition(value) {
20 | return .success(value)
21 | } else {
22 | return .error(ResultFilteringError.conditionUnsatisfied)
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Result+Map.swift:
--------------------------------------------------------------------------------
1 | /// Errors that can arise when mapping Results
2 | public enum ResultMappingError: Error {
3 | /// When the boxed value can't be mapped
4 | case cantMapValue
5 | }
6 |
7 | extension Result {
8 | func _map(_ handler: (T, Promise) -> Void) -> Future {
9 | let mapped = Promise()
10 |
11 | switch self {
12 | case .success(let value):
13 | handler(value, mapped)
14 | case .error(let error):
15 | mapped.fail(error)
16 | case .cancelled:
17 | mapped.cancel()
18 | }
19 |
20 | return mapped.future
21 | }
22 |
23 | func _map(_ handler: (T) -> Result) -> Result {
24 | switch self {
25 | case .success(let value):
26 | return handler(value)
27 | case .error(let error):
28 | return .error(error)
29 | case .cancelled:
30 | return .cancelled
31 | }
32 | }
33 |
34 | /**
35 | Maps this Result using a simple transformation closure
36 |
37 | - parameter handler: The closure to use to map the boxed value of this Result
38 |
39 | - returns: A new Result that will behave as this Result w.r.t. cancellation and failure, but will succeed with a value of type U obtained through the given closure
40 | */
41 | public func map(_ handler: (T) -> U) -> Result {
42 | return _map {
43 | .success(handler($0))
44 | }
45 | }
46 |
47 | /**
48 | Maps this Result using a simple transformation closure
49 |
50 | - parameter handler: The closure to use to map the boxed value of this Result
51 |
52 | - returns: A new Result that will behave as this Result w.r.t. cancellation and failure, but will succeed with a value of type U obtained through the given closure, unless the latter throws. In this case, the new Result will fail
53 | */
54 | public func map(_ handler: (T) throws -> U) -> Result {
55 | return _map { value in
56 | do {
57 | let mappedValue = try handler(value)
58 | return .success(mappedValue)
59 | } catch {
60 | return .error(error)
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Result+flatMap.swift:
--------------------------------------------------------------------------------
1 | extension Result {
2 | /**
3 | Flat maps this Result with the given handler returning a Future
4 |
5 | - parameter handler: The flat mapping handler that takes the boxed value of this Result and returns a Future
6 |
7 | - returns: A Future that will behave as this Result w.r.t. cancellation and failure, but will behave as the future obtained by calling the handler with the boxed value if this Result is .Success
8 | */
9 | public func flatMap(_ handler: (T) -> Future) -> Future {
10 | return _map { value, flatMapped in
11 | flatMapped.mimic(handler(value))
12 | }
13 | }
14 |
15 | /**
16 | Flat maps this Result with the given handler returning another Result
17 |
18 | - parameter handler: The flat mapping handler that takes the boxed value of this Result and returns another Result
19 |
20 | - returns: A new Result that will behave as this Result w.r.t. cancellation and failure, but will behave as the Result obtained by calling the handler with the boxed value if this Result is .Success
21 | */
22 | public func flatMap(_ handler: (T) -> Result) -> Result {
23 | return _map(handler)
24 | }
25 |
26 | /**
27 | Flat maps this Result with the given handler returning an optional U
28 |
29 | - parameter handler: The flat mapping handler that takes the boxed value of this Result and returns an optional U
30 |
31 | - returns: A new Result that will behave as this Result w.r.t. cancellation and failure, but will succeed with a value of type U obtained by calling the handler with the boxed value if this Result is .Success, unless the value is nil, in which case it will fail with a ResultMappingError.CantMapValue error
32 | */
33 | public func flatMap(_ handler: (T) -> U?) -> Result {
34 | return _map { value in
35 | if let mappedValue = handler(value) {
36 | return .success(mappedValue)
37 | } else {
38 | return .error(ResultMappingError.cantMapValue)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/PiedPiper/Result.swift:
--------------------------------------------------------------------------------
1 | /// Typical Result enumeration (aka Either)
2 | public enum Result {
3 | /// The result contains a Success value
4 | case success(T)
5 |
6 | /// The result contains an error
7 | case error(Error)
8 |
9 | /// The result was cancelled
10 | case cancelled
11 |
12 | /// The success value of this result, if any
13 | public var value: T? {
14 | if case .success(let result) = self {
15 | return result
16 | } else {
17 | return nil
18 | }
19 | }
20 |
21 | /// The error of this result, if any
22 | public var error: Error? {
23 | if case .error(let issue) = self {
24 | return issue
25 | } else {
26 | return nil
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/FunctionCompositionTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Quick
3 | import Nimble
4 | import PiedPiper
5 |
6 | class FunctionCompositionTests: QuickSpec {
7 | override func spec() {
8 | describe("Composing functions") {
9 | context("when the first function takes no parameter, and the second does") {
10 | let first: () -> Int = {
11 | 5
12 | }
13 |
14 | let second: (Int) -> String = {
15 | "\($0)"
16 | }
17 |
18 | var composed: ((()) -> String)!
19 |
20 | beforeEach {
21 | composed = first >>> second
22 | }
23 |
24 | it("should return the right value") {
25 | expect(composed(())).to(equal("5"))
26 | }
27 | }
28 |
29 | context("when the first function takes no parameter, and the second does but returns void") {
30 | let first: () -> Int = {
31 | 5
32 | }
33 |
34 | let second: (Int) -> Void = {
35 | print("\($0)")
36 | }
37 |
38 | var composed: ((()) -> Void)!
39 |
40 | beforeEach {
41 | composed = first >>> second
42 | composed(())
43 | }
44 |
45 | it("should swallow the parameter") {
46 | expect(true).to(beTrue())
47 | }
48 | }
49 |
50 | context("when the first function takes a parameter, and the second doesn't") {
51 | let first: (Int) -> Void = { input in
52 | print(input)
53 | }
54 |
55 | let second: () -> String = {
56 | "hello!"
57 | }
58 |
59 | var composed: ((Int) -> String)!
60 |
61 | beforeEach {
62 | composed = first >>> second
63 | }
64 |
65 | it("should return the right value") {
66 | expect(composed(1)).to(equal("hello!"))
67 | }
68 | }
69 |
70 | context("when the first function takes a parameter, and the second doesn't and returns void") {
71 | let first: (Int) -> Void = { input in
72 | print(input)
73 | }
74 |
75 | let second: () -> Void = {
76 | print("hello!")
77 | }
78 |
79 | var composed: ((Int) -> Void)!
80 |
81 | beforeEach {
82 | composed = first >>> second
83 | composed(1)
84 | }
85 |
86 | it("should swallow the parameter") {
87 | expect(true).to(beTrue())
88 | }
89 | }
90 |
91 | context("when both functions take no parameter") {
92 | let first: () -> Void = {
93 | print("hello...")
94 | }
95 |
96 | let second: () -> String = {
97 | "...world!"
98 | }
99 |
100 | var composed: ((()) -> String)!
101 |
102 | beforeEach {
103 | composed = first >>> second
104 | }
105 |
106 | it("should return the right value") {
107 | expect(composed(())).to(equal("...world!"))
108 | }
109 | }
110 |
111 | context("when both functions take no parameter and the second returns void") {
112 | let first: () -> Void = {
113 | print("hello...")
114 | }
115 |
116 | let second: () -> Void = {
117 | print("...world!")
118 | }
119 |
120 | var composed: ((()) -> Void)!
121 |
122 | beforeEach {
123 | composed = first >>> second
124 | composed(())
125 | }
126 |
127 | it("should swallow the parameter") {
128 | expect(true).to(beTrue())
129 | }
130 | }
131 |
132 | context("when both functions take parameters") {
133 | let first: (String) -> String = { input in
134 | "hello, \(input)!"
135 | }
136 |
137 | let second: (String) -> String = { input in
138 | input.uppercased()
139 | }
140 |
141 | var composed: ((String) -> String)!
142 |
143 | beforeEach {
144 | composed = first >>> second
145 | }
146 |
147 | it("should return the right value") {
148 | expect(composed("world")).to(equal("HELLO, WORLD!"))
149 | }
150 | }
151 |
152 | context("when both functions take parameters and the second returns void") {
153 | let first: (Int) -> String = { input in
154 | "\(input)"
155 | }
156 |
157 | let second: (String) -> Void = { input in
158 | print(input)
159 | }
160 |
161 | var composed: ((Int) -> Void)!
162 |
163 | beforeEach {
164 | composed = first >>> second
165 | composed(1)
166 | }
167 |
168 | it("should swallow the parameter") {
169 | expect(true).to(beTrue())
170 | }
171 | }
172 |
173 | context("when the first function returns nil") {
174 | let first: (String) -> Int? = {
175 | Int($0)
176 | }
177 |
178 | let second: (Int) -> String = {
179 | "\($0)"
180 | }
181 |
182 | var composed: ((String) -> String?)!
183 |
184 | beforeEach {
185 | composed = first >>> second
186 | }
187 |
188 | it("should return the result if it's not nil") {
189 | expect(composed("10")).to(equal("10"))
190 | }
191 |
192 | it("should return nil if the first computation is nil") {
193 | expect(composed("hello")).to(beNil())
194 | }
195 | }
196 |
197 | context("when the second function returns nil") {
198 | let first: (Float) -> Int? = {
199 | Int($0)
200 | }
201 |
202 | let second: (Int) -> String? = {
203 | $0 > 0 ? "\($0)" : nil
204 | }
205 |
206 | var composed: ((Float) -> String?)!
207 |
208 | beforeEach {
209 | composed = first >>> second
210 | }
211 |
212 | it("should return the result if it's not nil") {
213 | expect(composed(1.5)).to(equal("1"))
214 | }
215 |
216 | it("should return nil if the second computation is nil") {
217 | expect(composed(-1.0)).to(beNil())
218 | }
219 | }
220 | }
221 |
222 | describe("Composing futures") {
223 | var promise1: Promise!
224 | var promise2: Promise!
225 | var input1: Int!
226 | var input2: String!
227 | var composed: ((Int) -> Future)!
228 | var result: Int!
229 | var error: Error!
230 | var canceled: Bool!
231 | let input = 1
232 |
233 | beforeEach {
234 | result = nil
235 | error = nil
236 | canceled = false
237 |
238 | promise1 = Promise()
239 | promise2 = Promise()
240 |
241 | let first: (Int) -> Future = { input in
242 | input1 = input
243 | return promise1.future
244 | }
245 |
246 | let second: (String) -> Future = { input in
247 | input2 = input
248 | return promise2.future
249 | }
250 |
251 | composed = first >>> second
252 |
253 | composed(input).onSuccess {
254 | result = $0
255 | }.onFailure {
256 | error = $0
257 | }.onCancel {
258 | canceled = true
259 | }
260 | }
261 |
262 | it("should pass the input to the first promise") {
263 | expect(input1).to(equal(input))
264 | }
265 |
266 | context("when the first promise succeeds") {
267 | let firstValue = "test"
268 |
269 | beforeEach {
270 | promise1.succeed(firstValue)
271 | }
272 |
273 | it("should pass the result to the second promise") {
274 | expect(input2).to(equal(firstValue))
275 | }
276 |
277 | context("when the second promise fails") {
278 | beforeEach {
279 | promise2.fail(TestError.anotherError)
280 | }
281 |
282 | it("should fail the composition") {
283 | expect(error).notTo(beNil())
284 | }
285 |
286 | it("should pass the right error") {
287 | expect(error as? TestError).to(equal(TestError.anotherError))
288 | }
289 | }
290 |
291 | context("when the second promise succeeds") {
292 | let expectedResult = 10
293 |
294 | beforeEach {
295 | promise2.succeed(expectedResult)
296 | }
297 |
298 | it("should succeed the composition") {
299 | expect(result).to(equal(expectedResult))
300 | }
301 | }
302 |
303 | context("when the second promise is canceled") {
304 | beforeEach {
305 | promise2.cancel()
306 | }
307 |
308 | it("should cancel the composition") {
309 | expect(canceled).to(beTrue())
310 | }
311 | }
312 | }
313 |
314 | context("when the first promise fails") {
315 | beforeEach {
316 | promise1.fail(TestError.simpleError)
317 | }
318 |
319 | it("should fail the composition") {
320 | expect(error).notTo(beNil())
321 | }
322 |
323 | it("should pass the right error") {
324 | expect(error as? TestError).to(equal(TestError.simpleError))
325 | }
326 | }
327 |
328 | context("when the first promise is canceled") {
329 | beforeEach {
330 | promise1.cancel()
331 | }
332 |
333 | it("should cancel the composition") {
334 | expect(canceled).to(beTrue())
335 | }
336 | }
337 | }
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+AllTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class FutureSequenceAllTests: QuickSpec {
6 | override func spec() {
7 | describe("calling all on a list of Futures") {
8 | var promises: [Promise]!
9 | var resultFuture: Future<()>!
10 | var didSucceed: Bool?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 | var originalPromisesCanceled: [Bool]!
14 |
15 | beforeEach {
16 | let numberOfPromises = 5
17 | originalPromisesCanceled = (0..]!
144 | var resultingFuture: Future<()>!
145 | var didSucceed: Bool?
146 |
147 | beforeEach {
148 | promises = [
149 | Promise(),
150 | Promise(),
151 | Promise(),
152 | Promise(),
153 | Promise()
154 | ]
155 |
156 | didSucceed = nil
157 |
158 | resultingFuture = promises
159 | .map { $0.future }
160 | .all()
161 |
162 | resultingFuture.onSuccess {
163 | didSucceed = true
164 | }
165 |
166 | var arrayOfIndexes = Array(promises.enumerated())
167 |
168 | repeat {
169 | arrayOfIndexes = arrayOfIndexes.shuffle()
170 | } while arrayOfIndexes.map({ $0.0 }) == Array(0..!
9 | var filteredFuture: Future!
10 | var successValue: Int?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 |
14 | beforeEach {
15 | promise = Promise()
16 |
17 | wasCanceled = false
18 | successValue = nil
19 | failureValue = nil
20 | }
21 |
22 | context("when done through a simple closure") {
23 | let filteringClosure: (Int) -> Bool = { num in
24 | num > 0
25 | }
26 |
27 | beforeEach {
28 | filteredFuture = promise.future
29 | .filter(filteringClosure)
30 |
31 | filteredFuture.onCompletion { result in
32 | switch result {
33 | case .success(let value):
34 | successValue = value
35 | case .error(let error):
36 | failureValue = error
37 | case .cancelled:
38 | wasCanceled = true
39 | }
40 | }
41 | }
42 |
43 | context("when the original future fails") {
44 | let error = TestError.simpleError
45 |
46 | beforeEach {
47 | promise.fail(error)
48 | }
49 |
50 | it("should also fail the filtered future") {
51 | expect(failureValue).notTo(beNil())
52 | }
53 |
54 | it("should fail the filtered future with the same error") {
55 | expect(failureValue as? TestError).to(equal(error))
56 | }
57 |
58 | it("should not succeed the filtered future") {
59 | expect(successValue).to(beNil())
60 | }
61 |
62 | it("should not cancel the filtered future") {
63 | expect(wasCanceled).to(beFalse())
64 | }
65 | }
66 |
67 | context("when the original future is canceled") {
68 | beforeEach {
69 | promise.cancel()
70 | }
71 |
72 | it("should also cancel the filtered future") {
73 | expect(wasCanceled).to(beTrue())
74 | }
75 |
76 | it("should not succeed the filtered future") {
77 | expect(successValue).to(beNil())
78 | }
79 |
80 | it("should not fail the filtered future") {
81 | expect(failureValue).to(beNil())
82 | }
83 | }
84 |
85 | context("when the original future succeeds") {
86 | context("when the success value satisfies the condition") {
87 | let result = 20
88 |
89 | beforeEach {
90 | promise.succeed(result)
91 | }
92 |
93 | it("should also succeed the filtered future") {
94 | expect(successValue).notTo(beNil())
95 | }
96 |
97 | it("should succeed the filtered future with the original value") {
98 | expect(successValue).to(equal(result))
99 | }
100 |
101 | it("should not fail the filtered future") {
102 | expect(failureValue).to(beNil())
103 | }
104 |
105 | it("should not cancel the filtered future") {
106 | expect(wasCanceled).to(beFalse())
107 | }
108 | }
109 |
110 | context("when the success value doesn't satisfy the condition") {
111 | let result = -20
112 |
113 | beforeEach {
114 | promise.succeed(result)
115 | }
116 |
117 | it("should not succeed the filtered future") {
118 | expect(successValue).to(beNil())
119 | }
120 |
121 | it("should fail the filtered future") {
122 | expect(failureValue).notTo(beNil())
123 | }
124 |
125 | it("should fail the filtered future with the right error") {
126 | expect(failureValue as? FutureFilteringError).to(equal(FutureFilteringError.conditionUnsatisfied))
127 | }
128 |
129 | it("should not cancel the filtered future") {
130 | expect(wasCanceled).to(beFalse())
131 | }
132 | }
133 | }
134 | }
135 |
136 | context("when done through a closure that returns a Future") {
137 | let filteringClosure: (Int) -> Future = { num in
138 | if num < 0 {
139 | return Future(TestError.simpleError)
140 | } else if num == 0 {
141 | let result = Promise()
142 | result.cancel()
143 | return result.future
144 | } else if num < 100 {
145 | return Future(true)
146 | } else {
147 | return Future(false)
148 | }
149 | }
150 |
151 | beforeEach {
152 | filteredFuture = promise.future
153 | .filter(filteringClosure)
154 |
155 | filteredFuture.onCompletion { result in
156 | switch result {
157 | case .success(let value):
158 | successValue = value
159 | case .error(let error):
160 | failureValue = error
161 | case .cancelled:
162 | wasCanceled = true
163 | }
164 | }
165 | }
166 |
167 | context("when the original future fails") {
168 | let error = TestError.simpleError
169 |
170 | beforeEach {
171 | promise.fail(error)
172 | }
173 |
174 | it("should also fail the filtered future") {
175 | expect(failureValue).notTo(beNil())
176 | }
177 |
178 | it("should fail the filtered future with the same error") {
179 | expect(failureValue as? TestError).to(equal(error))
180 | }
181 |
182 | it("should not succeed the filtered future") {
183 | expect(successValue).to(beNil())
184 | }
185 |
186 | it("should not cancel the filtered future") {
187 | expect(wasCanceled).to(beFalse())
188 | }
189 | }
190 |
191 | context("when the original future is canceled") {
192 | beforeEach {
193 | promise.cancel()
194 | }
195 |
196 | it("should also cancel the filtered future") {
197 | expect(wasCanceled).to(beTrue())
198 | }
199 |
200 | it("should not succeed the filtered future") {
201 | expect(successValue).to(beNil())
202 | }
203 |
204 | it("should not fail the filtered future") {
205 | expect(failureValue).to(beNil())
206 | }
207 | }
208 |
209 | context("when the original future succeeds") {
210 | context("when the success value returns a Future that satisfies the condition") {
211 | let result = 20
212 |
213 | beforeEach {
214 | promise.succeed(result)
215 | }
216 |
217 | it("should also succeed the filtered future") {
218 | expect(successValue).notTo(beNil())
219 | }
220 |
221 | it("should succeed the filtered future with the original value") {
222 | expect(successValue).to(equal(result))
223 | }
224 |
225 | it("should not fail the filtered future") {
226 | expect(failureValue).to(beNil())
227 | }
228 |
229 | it("should not cancel the filtered future") {
230 | expect(wasCanceled).to(beFalse())
231 | }
232 | }
233 |
234 | context("when the success value returns a Future that doesn't satisfy the condition") {
235 | let result = 1208
236 |
237 | beforeEach {
238 | promise.succeed(result)
239 | }
240 |
241 | it("should not succeed the filtered future") {
242 | expect(successValue).to(beNil())
243 | }
244 |
245 | it("should fail the filtered future") {
246 | expect(failureValue).notTo(beNil())
247 | }
248 |
249 | it("should fail the filtered future with the right error") {
250 | expect(failureValue as? FutureFilteringError).to(equal(FutureFilteringError.conditionUnsatisfied))
251 | }
252 |
253 | it("should not cancel the filtered future") {
254 | expect(wasCanceled).to(beFalse())
255 | }
256 | }
257 |
258 | context("when the success value returns a Future that fails") {
259 | let result = -20
260 |
261 | beforeEach {
262 | promise.succeed(result)
263 | }
264 |
265 | it("should not succeed the filtered future") {
266 | expect(successValue).to(beNil())
267 | }
268 |
269 | it("should fail the filtered future") {
270 | expect(failureValue).notTo(beNil())
271 | }
272 |
273 | it("should fail the filtered future with the right error") {
274 | expect(failureValue as? TestError).to(equal(TestError.simpleError))
275 | }
276 |
277 | it("should not cancel the filtered future") {
278 | expect(wasCanceled).to(beFalse())
279 | }
280 | }
281 |
282 | context("when the success value returns a Future that is canceled") {
283 | let result = 0
284 |
285 | beforeEach {
286 | promise.succeed(result)
287 | }
288 |
289 | it("should not succeed the filtered future") {
290 | expect(successValue).to(beNil())
291 | }
292 |
293 | it("should not fail the filtered future") {
294 | expect(failureValue).to(beNil())
295 | }
296 |
297 | it("should cancel the filtered future") {
298 | expect(wasCanceled).to(beTrue())
299 | }
300 | }
301 | }
302 | }
303 | }
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+FirstCompletedTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class FutureSequenceFirstCompletedTests: QuickSpec {
6 | override func spec() {
7 | describe("calling firstCompleted on a list of Futures") {
8 | var promises: [Promise]!
9 | var resultFuture: Future!
10 | var successValue: Int?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 | var originalPromisesCanceled: [Bool]!
14 |
15 | beforeEach {
16 | let numberOfPromises = 5
17 | originalPromisesCanceled = (0..!
9 | var mappedFuture: Future!
10 | var successValue: Int?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 |
14 | beforeEach {
15 | promise = Promise()
16 |
17 | wasCanceled = false
18 | successValue = nil
19 | failureValue = nil
20 | }
21 |
22 | context("when done through a closure that can return nil") {
23 | let mappingClosure: (String) -> Int? = { str in
24 | if str == "nil" {
25 | return nil
26 | } else {
27 | return 1
28 | }
29 | }
30 |
31 | beforeEach {
32 | mappedFuture = promise.future
33 | .flatMap(mappingClosure)
34 |
35 | mappedFuture.onCompletion { result in
36 | switch result {
37 | case .success(let value):
38 | successValue = value
39 | case .error(let error):
40 | failureValue = error
41 | case .cancelled:
42 | wasCanceled = true
43 | }
44 | }
45 | }
46 |
47 | context("when the original future fails") {
48 | let error = TestError.simpleError
49 |
50 | beforeEach {
51 | promise.fail(error)
52 | }
53 |
54 | it("should also fail the mapped future") {
55 | expect(failureValue).notTo(beNil())
56 | }
57 |
58 | it("should fail the mapped future with the same error") {
59 | expect(failureValue as? TestError).to(equal(error))
60 | }
61 |
62 | it("should not succeed the mapped future") {
63 | expect(successValue).to(beNil())
64 | }
65 |
66 | it("should not cancel the mapped future") {
67 | expect(wasCanceled).to(beFalse())
68 | }
69 | }
70 |
71 | context("when the original future is canceled") {
72 | beforeEach {
73 | promise.cancel()
74 | }
75 |
76 | it("should also cancel the mapped future") {
77 | expect(wasCanceled).to(beTrue())
78 | }
79 |
80 | it("should not succeed the mapped future") {
81 | expect(successValue).to(beNil())
82 | }
83 |
84 | it("should not fail the mapped future") {
85 | expect(failureValue).to(beNil())
86 | }
87 | }
88 |
89 | context("when the original future succeeds") {
90 | context("when the closure doesn't return nil") {
91 | let result = "Eureka!"
92 |
93 | beforeEach {
94 | promise.succeed(result)
95 | }
96 |
97 | it("should also succeed the mapped future") {
98 | expect(successValue).notTo(beNil())
99 | }
100 |
101 | it("should succeed the mapped future with the mapped value") {
102 | expect(successValue).to(equal(mappingClosure(result)))
103 | }
104 |
105 | it("should not fail the mapped future") {
106 | expect(failureValue).to(beNil())
107 | }
108 |
109 | it("should not cancel the mapped future") {
110 | expect(wasCanceled).to(beFalse())
111 | }
112 | }
113 |
114 | context("when the closure returns nil") {
115 | let result = "nil"
116 |
117 | beforeEach {
118 | promise.succeed(result)
119 | }
120 |
121 | it("should not succeed the mapped future") {
122 | expect(successValue).to(beNil())
123 | }
124 |
125 | it("should fail the mapped future") {
126 | expect(failureValue).notTo(beNil())
127 | }
128 |
129 | it("should fail the mapped future with the right error") {
130 | expect(failureValue as? FutureMappingError).to(equal(FutureMappingError.cantMapValue))
131 | }
132 |
133 | it("should not cancel the mapped future") {
134 | expect(wasCanceled).to(beFalse())
135 | }
136 | }
137 | }
138 | }
139 |
140 | context("when done through a closure that returns a Result") {
141 | let mappingClosure: (String) -> Result = { str in
142 | if str == "cancel" {
143 | return Result.cancelled
144 | } else if str == "failure" {
145 | return Result.error(TestError.simpleError)
146 | } else {
147 | return Result.success(1)
148 | }
149 | }
150 |
151 | beforeEach {
152 | mappedFuture = promise.future
153 | .flatMap(mappingClosure)
154 |
155 | mappedFuture.onCompletion { result in
156 | switch result {
157 | case .success(let value):
158 | successValue = value
159 | case .error(let error):
160 | failureValue = error
161 | case .cancelled:
162 | wasCanceled = true
163 | }
164 | }
165 | }
166 |
167 | context("when the original future fails") {
168 | let error = TestError.simpleError
169 |
170 | beforeEach {
171 | promise.fail(error)
172 | }
173 |
174 | it("should also fail the mapped future") {
175 | expect(failureValue).notTo(beNil())
176 | }
177 |
178 | it("should fail the mapped future with the same error") {
179 | expect(failureValue as? TestError).to(equal(error))
180 | }
181 |
182 | it("should not succeed the mapped future") {
183 | expect(successValue).to(beNil())
184 | }
185 |
186 | it("should not cancel the mapped future") {
187 | expect(wasCanceled).to(beFalse())
188 | }
189 | }
190 |
191 | context("when the original future is canceled") {
192 | beforeEach {
193 | promise.cancel()
194 | }
195 |
196 | it("should also cancel the mapped future") {
197 | expect(wasCanceled).to(beTrue())
198 | }
199 |
200 | it("should not succeed the mapped future") {
201 | expect(successValue).to(beNil())
202 | }
203 |
204 | it("should not fail the mapped future") {
205 | expect(failureValue).to(beNil())
206 | }
207 | }
208 |
209 | context("when the original future succeeds") {
210 | context("when the closure returns a success") {
211 | let result = "Eureka!"
212 |
213 | beforeEach {
214 | promise.succeed(result)
215 | }
216 |
217 | it("should also succeed the mapped future") {
218 | expect(successValue).notTo(beNil())
219 | }
220 |
221 | it("should succeed the mapped future with the right value") {
222 | expect(successValue).to(equal(1))
223 | }
224 |
225 | it("should not fail the mapped future") {
226 | expect(failureValue).to(beNil())
227 | }
228 |
229 | it("should not cancel the mapped future") {
230 | expect(wasCanceled).to(beFalse())
231 | }
232 | }
233 |
234 | context("when the closure returns a failure") {
235 | let result = "failure"
236 |
237 | beforeEach {
238 | promise.succeed(result)
239 | }
240 |
241 | it("should not succeed the mapped future") {
242 | expect(successValue).to(beNil())
243 | }
244 |
245 | it("should fail the mapped future") {
246 | expect(failureValue).notTo(beNil())
247 | }
248 |
249 | it("should fail the mapped future with the right error") {
250 | expect(failureValue as? TestError).to(equal(TestError.simpleError))
251 | }
252 |
253 | it("should not cancel the mapped future") {
254 | expect(wasCanceled).to(beFalse())
255 | }
256 | }
257 |
258 | context("when the closure returns a cancelled result") {
259 | let result = "cancel"
260 |
261 | beforeEach {
262 | promise.succeed(result)
263 | }
264 |
265 | it("should not succeed the mapped future") {
266 | expect(successValue).to(beNil())
267 | }
268 |
269 | it("should not fail the mapped future") {
270 | expect(failureValue).to(beNil())
271 | }
272 |
273 | it("should cancel the mapped future") {
274 | expect(wasCanceled).to(beTrue())
275 | }
276 | }
277 | }
278 | }
279 |
280 | context("when done through a closure that returns a Future") {
281 | let mappingClosure: (String) -> Future = { str in
282 | let result: Future
283 |
284 | if str == "cancel" {
285 | let intermediate = Promise()
286 | intermediate.cancel()
287 | result = intermediate.future
288 | } else if str == "failure" {
289 | result = Future(TestError.simpleError)
290 | } else {
291 | result = Future(1)
292 | }
293 |
294 | return result
295 | }
296 |
297 | beforeEach {
298 | mappedFuture = promise.future
299 | .flatMap(mappingClosure)
300 |
301 | mappedFuture.onCompletion { result in
302 | switch result {
303 | case .success(let value):
304 | successValue = value
305 | case .error(let error):
306 | failureValue = error
307 | case .cancelled:
308 | wasCanceled = true
309 | }
310 | }
311 | }
312 |
313 | context("when the original future fails") {
314 | let error = TestError.simpleError
315 |
316 | beforeEach {
317 | promise.fail(error)
318 | }
319 |
320 | it("should also fail the mapped future") {
321 | expect(failureValue).notTo(beNil())
322 | }
323 |
324 | it("should fail the mapped future with the same error") {
325 | expect(failureValue as? TestError).to(equal(error))
326 | }
327 |
328 | it("should not succeed the mapped future") {
329 | expect(successValue).to(beNil())
330 | }
331 |
332 | it("should not cancel the mapped future") {
333 | expect(wasCanceled).to(beFalse())
334 | }
335 | }
336 |
337 | context("when the original future is canceled") {
338 | beforeEach {
339 | promise.cancel()
340 | }
341 |
342 | it("should also cancel the mapped future") {
343 | expect(wasCanceled).to(beTrue())
344 | }
345 |
346 | it("should not succeed the mapped future") {
347 | expect(successValue).to(beNil())
348 | }
349 |
350 | it("should not fail the mapped future") {
351 | expect(failureValue).to(beNil())
352 | }
353 | }
354 |
355 | context("when the original future succeeds") {
356 | context("when the closure returns a success") {
357 | let result = "Eureka!"
358 |
359 | beforeEach {
360 | promise.succeed(result)
361 | }
362 |
363 | it("should also succeed the mapped future") {
364 | expect(successValue).notTo(beNil())
365 | }
366 |
367 | it("should succeed the mapped future with the right value") {
368 | expect(successValue).to(equal(1))
369 | }
370 |
371 | it("should not fail the mapped future") {
372 | expect(failureValue).to(beNil())
373 | }
374 |
375 | it("should not cancel the mapped future") {
376 | expect(wasCanceled).to(beFalse())
377 | }
378 | }
379 |
380 | context("when the closure returns a failure") {
381 | let result = "failure"
382 |
383 | beforeEach {
384 | promise.succeed(result)
385 | }
386 |
387 | it("should not succeed the mapped future") {
388 | expect(successValue).to(beNil())
389 | }
390 |
391 | it("should fail the mapped future") {
392 | expect(failureValue).notTo(beNil())
393 | }
394 |
395 | it("should fail the mapped future with the right error") {
396 | expect(failureValue as? TestError).to(equal(TestError.simpleError))
397 | }
398 |
399 | it("should not cancel the mapped future") {
400 | expect(wasCanceled).to(beFalse())
401 | }
402 | }
403 |
404 | context("when the closure returns a cancelled future") {
405 | let result = "cancel"
406 |
407 | beforeEach {
408 | promise.succeed(result)
409 | }
410 |
411 | it("should not succeed the mapped future") {
412 | expect(successValue).to(beNil())
413 | }
414 |
415 | it("should not fail the mapped future") {
416 | expect(failureValue).to(beNil())
417 | }
418 |
419 | it("should cancel the mapped future") {
420 | expect(wasCanceled).to(beTrue())
421 | }
422 | }
423 | }
424 | }
425 | }
426 | }
427 | }
428 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+MapTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class FutureMapTests: QuickSpec {
6 | override func spec() {
7 | describe("Mapping a Future") {
8 | var promise: Promise!
9 | var mappedFuture: Future!
10 | var successValue: Int?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 |
14 | beforeEach {
15 | promise = Promise()
16 |
17 | wasCanceled = false
18 | successValue = nil
19 | failureValue = nil
20 | }
21 |
22 | context("when done through a simple closure") {
23 | let mappingClosure: (String) -> Int = { str in
24 | return 1
25 | }
26 |
27 | beforeEach {
28 | mappedFuture = promise.future
29 | .map(mappingClosure)
30 |
31 | mappedFuture.onCompletion { result in
32 | switch result {
33 | case .success(let value):
34 | successValue = value
35 | case .error(let error):
36 | failureValue = error
37 | case .cancelled:
38 | wasCanceled = true
39 | }
40 | }
41 | }
42 |
43 | context("when the original future fails") {
44 | let error = TestError.simpleError
45 |
46 | beforeEach {
47 | promise.fail(error)
48 | }
49 |
50 | it("should also fail the mapped future") {
51 | expect(failureValue).notTo(beNil())
52 | }
53 |
54 | it("should fail the mapped future with the same error") {
55 | expect(failureValue as? TestError).to(equal(error))
56 | }
57 |
58 | it("should not succeed the mapped future") {
59 | expect(successValue).to(beNil())
60 | }
61 |
62 | it("should not cancel the mapped future") {
63 | expect(wasCanceled).to(beFalse())
64 | }
65 | }
66 |
67 | context("when the original future is canceled") {
68 | beforeEach {
69 | promise.cancel()
70 | }
71 |
72 | it("should also cancel the mapped future") {
73 | expect(wasCanceled).to(beTrue())
74 | }
75 |
76 | it("should not succeed the mapped future") {
77 | expect(successValue).to(beNil())
78 | }
79 |
80 | it("should not fail the mapped future") {
81 | expect(failureValue).to(beNil())
82 | }
83 | }
84 |
85 | context("when the original future succeeds") {
86 | let result = "Eureka!"
87 |
88 | beforeEach {
89 | promise.succeed(result)
90 | }
91 |
92 | it("should also succeed the mapped future") {
93 | expect(successValue).notTo(beNil())
94 | }
95 |
96 | it("should succeed the mapped future with the mapped value") {
97 | expect(successValue).to(equal(mappingClosure(result)))
98 | }
99 |
100 | it("should not fail the mapped future") {
101 | expect(failureValue).to(beNil())
102 | }
103 |
104 | it("should not cancel the mapped future") {
105 | expect(wasCanceled).to(beFalse())
106 | }
107 | }
108 | }
109 |
110 | context("when done through a closure that can throw") {
111 | let mappingClosure: (String) throws -> Int = { str in
112 | if str == "throw" {
113 | throw TestError.anotherError
114 | } else {
115 | return 1
116 | }
117 | }
118 |
119 | beforeEach {
120 | mappedFuture = promise.future
121 | .map(mappingClosure)
122 |
123 | mappedFuture.onCompletion { result in
124 | switch result {
125 | case .success(let value):
126 | successValue = value
127 | case .error(let error):
128 | failureValue = error
129 | case .cancelled:
130 | wasCanceled = true
131 | }
132 | }
133 | }
134 |
135 | context("when the original future fails") {
136 | let error = TestError.simpleError
137 |
138 | beforeEach {
139 | promise.fail(error)
140 | }
141 |
142 | it("should also fail the mapped future") {
143 | expect(failureValue).notTo(beNil())
144 | }
145 |
146 | it("should fail the mapped future with the same error") {
147 | expect(failureValue as? TestError).to(equal(error))
148 | }
149 |
150 | it("should not succeed the mapped future") {
151 | expect(successValue).to(beNil())
152 | }
153 |
154 | it("should not cancel the mapped future") {
155 | expect(wasCanceled).to(beFalse())
156 | }
157 | }
158 |
159 | context("when the original future is canceled") {
160 | beforeEach {
161 | promise.cancel()
162 | }
163 |
164 | it("should also cancel the mapped future") {
165 | expect(wasCanceled).to(beTrue())
166 | }
167 |
168 | it("should not succeed the mapped future") {
169 | expect(successValue).to(beNil())
170 | }
171 |
172 | it("should not fail the mapped future") {
173 | expect(failureValue).to(beNil())
174 | }
175 | }
176 |
177 | context("when the original future succeeds") {
178 | context("when the closure doesn't throw") {
179 | let result = "Eureka!"
180 |
181 | beforeEach {
182 | promise.succeed(result)
183 | }
184 |
185 | it("should also succeed the mapped future") {
186 | expect(successValue).notTo(beNil())
187 | }
188 |
189 | it("should succeed the mapped future with the mapped value") {
190 | expect(successValue).to(equal(try! mappingClosure(result)))
191 | }
192 |
193 | it("should not fail the mapped future") {
194 | expect(failureValue).to(beNil())
195 | }
196 |
197 | it("should not cancel the mapped future") {
198 | expect(wasCanceled).to(beFalse())
199 | }
200 | }
201 |
202 | context("when the closure throws") {
203 | let result = "throw"
204 |
205 | beforeEach {
206 | promise.succeed(result)
207 | }
208 |
209 | it("should not succeed the mapped future") {
210 | expect(successValue).to(beNil())
211 | }
212 |
213 | it("should fail the mapped future") {
214 | expect(failureValue).notTo(beNil())
215 | }
216 |
217 | it("should fail the mapped future with the right error") {
218 | expect(failureValue as? TestError).to(equal(TestError.anotherError))
219 | }
220 |
221 | it("should not cancel the mapped future") {
222 | expect(wasCanceled).to(beFalse())
223 | }
224 | }
225 | }
226 | }
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+MergeTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class FutureSequenceMergeTests: QuickSpec {
6 | override func spec() {
7 | describe("Merging a list of Futures") {
8 | var promises: [Promise]!
9 | var mergedFuture: Future<[Int]>!
10 | var successValue: [Int]?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 | var originalPromisesCanceled: [Bool]!
14 |
15 | beforeEach {
16 | let numberOfPromises = 5
17 | originalPromisesCanceled = (0..]!
151 | var mergedFuture: Future<[String]>!
152 | var successValue: [String]?
153 | var expectedResult: [String]!
154 |
155 | beforeEach {
156 | promises = [
157 | Promise(),
158 | Promise(),
159 | Promise(),
160 | Promise(),
161 | Promise()
162 | ]
163 |
164 | successValue = nil
165 |
166 | mergedFuture = promises
167 | .map { $0.future }
168 | .mergeAll()
169 |
170 | mergedFuture.onSuccess {
171 | successValue = $0
172 | }
173 |
174 | expectedResult = Array(0..]!
198 | var mergedFuture: Future<[Int]>!
199 | var successValue: [Int]?
200 | var failureValue: Error?
201 | var wasCanceled: Bool!
202 | var originalPromisesCanceled: [Bool]!
203 |
204 | let numberOfPromises = 5
205 |
206 | beforeEach {
207 | originalPromisesCanceled = (0.. Self {
7 | if count < 2 { return self }
8 |
9 | for i in startIndex ..< endIndex - 1 {
10 | let j = Int.random(in: 0..]!
24 | var reducedFuture: Future!
25 | var successValue: Int?
26 | var failureValue: Error?
27 | var wasCanceled: Bool!
28 | var originalPromisesCanceled: [Bool]!
29 |
30 | beforeEach {
31 | let numberOfPromises = 5
32 | originalPromisesCanceled = (0..]!
167 | var reducedFuture: Future!
168 | var successValue: String?
169 | var expectedResult: String!
170 |
171 | beforeEach {
172 | promises = [
173 | Promise(),
174 | Promise(),
175 | Promise(),
176 | Promise(),
177 | Promise()
178 | ]
179 |
180 | successValue = nil
181 |
182 | reducedFuture = promises
183 | .map { $0.future }
184 | .reduce("BEGIN-", combine: +)
185 |
186 | reducedFuture.onSuccess {
187 | successValue = $0
188 | }
189 |
190 | let sequenceOfIndexes = Array(0..?
10 | var retryCount: Int!
11 | let futureClosure: () -> Future = {
12 | lastPromise = Promise()
13 | retryCount = retryCount + 1
14 | return lastPromise!.future
15 | }
16 | var successSentinel: Int?
17 | var failureSentinel: Error?
18 | var cancelSentinel: Bool!
19 |
20 | beforeEach {
21 | lastPromise = nil
22 | retryCount = 0
23 |
24 | successSentinel = nil
25 | failureSentinel = nil
26 | cancelSentinel = false
27 | }
28 |
29 | context("when retrying less than 0 times") {
30 | beforeEach {
31 | retry(-1, every: 0, futureClosure: futureClosure)
32 | .onSuccess {
33 | successSentinel = $0
34 | }
35 | .onFailure {
36 | failureSentinel = $0
37 | }
38 | .onCancel {
39 | cancelSentinel = true
40 | }
41 | }
42 |
43 | it("should call the closure once") {
44 | expect(retryCount).to(equal(1))
45 | }
46 |
47 | context("when the future succeeds") {
48 | let value = 2
49 |
50 | beforeEach {
51 | lastPromise?.succeed(value)
52 | }
53 |
54 | it("should not retry") {
55 | expect(retryCount).to(equal(1))
56 | }
57 |
58 | it("should succeed the result") {
59 | expect(successSentinel).notTo(beNil())
60 | }
61 |
62 | it("should succeed with the right value") {
63 | expect(successSentinel).to(equal(value))
64 | }
65 | }
66 |
67 | context("when the future fails") {
68 | let error = TestError.anotherError
69 |
70 | beforeEach {
71 | lastPromise?.fail(error)
72 | }
73 |
74 | it("should not retry") {
75 | expect(retryCount).to(equal(1))
76 | }
77 |
78 | it("should fail the result") {
79 | expect(failureSentinel).notTo(beNil())
80 | }
81 |
82 | it("should fail with the right error") {
83 | expect(failureSentinel as? TestError).to(equal(error))
84 | }
85 | }
86 |
87 | context("when the future is canceled") {
88 | beforeEach {
89 | lastPromise?.cancel()
90 | }
91 |
92 | it("should not retry") {
93 | expect(retryCount).to(equal(1))
94 | }
95 |
96 | it("should cancel the result") {
97 | expect(cancelSentinel).to(beTrue())
98 | }
99 | }
100 | }
101 |
102 | context("when retrying 0 times") {
103 | beforeEach {
104 | retry(0, every: 0, futureClosure: futureClosure)
105 | .onSuccess {
106 | successSentinel = $0
107 | }
108 | .onFailure {
109 | failureSentinel = $0
110 | }
111 | .onCancel {
112 | cancelSentinel = true
113 | }
114 | }
115 |
116 | it("should call the closure once") {
117 | expect(retryCount).to(equal(1))
118 | }
119 |
120 | context("when the future succeeds") {
121 | let value = 2
122 |
123 | beforeEach {
124 | lastPromise?.succeed(value)
125 | }
126 |
127 | it("should not retry") {
128 | expect(retryCount).to(equal(1))
129 | }
130 |
131 | it("should succeed the result") {
132 | expect(successSentinel).notTo(beNil())
133 | }
134 |
135 | it("should succeed with the right value") {
136 | expect(successSentinel).to(equal(value))
137 | }
138 | }
139 |
140 | context("when the future fails") {
141 | let error = TestError.anotherError
142 |
143 | beforeEach {
144 | lastPromise?.fail(error)
145 | }
146 |
147 | it("should not retry") {
148 | expect(retryCount).to(equal(1))
149 | }
150 |
151 | it("should fail the result") {
152 | expect(failureSentinel).notTo(beNil())
153 | }
154 |
155 | it("should fail with the right error") {
156 | expect(failureSentinel as? TestError).to(equal(error))
157 | }
158 | }
159 |
160 | context("when the future is canceled") {
161 | beforeEach {
162 | lastPromise?.cancel()
163 | }
164 |
165 | it("should not retry") {
166 | expect(retryCount).to(equal(1))
167 | }
168 |
169 | it("should cancel the result") {
170 | expect(cancelSentinel).to(beTrue())
171 | }
172 | }
173 | }
174 |
175 | context("when retrying 1 time") {
176 | beforeEach {
177 | retry(1, every: 0.2, futureClosure: futureClosure)
178 | .onSuccess {
179 | successSentinel = $0
180 | }
181 | .onFailure {
182 | failureSentinel = $0
183 | }
184 | .onCancel {
185 | cancelSentinel = true
186 | }
187 | }
188 |
189 | it("should call the closure once") {
190 | expect(retryCount).to(equal(1))
191 | }
192 |
193 | context("when the future succeeds") {
194 | let value = 2
195 |
196 | beforeEach {
197 | lastPromise?.succeed(value)
198 | }
199 |
200 | it("should not retry") {
201 | expect(retryCount).to(equal(1))
202 | }
203 |
204 | it("should succeed the result") {
205 | expect(successSentinel).notTo(beNil())
206 | }
207 |
208 | it("should succeed with the right value") {
209 | expect(successSentinel).to(equal(value))
210 | }
211 | }
212 |
213 | context("when the future fails") {
214 | let error = TestError.anotherError
215 |
216 | beforeEach {
217 | lastPromise?.fail(error)
218 | }
219 |
220 | it("should not fail the result") {
221 | expect(failureSentinel).to(beNil())
222 | }
223 |
224 | // FIXME: Failing for some reason :(
225 | xit("should retry") {
226 | expect(retryCount).toEventually(equal(2), timeout: 0.25)
227 | }
228 | }
229 |
230 | context("when the future is canceled") {
231 | beforeEach {
232 | lastPromise?.cancel()
233 | }
234 |
235 | it("should not retry") {
236 | expect(retryCount).to(equal(1))
237 | }
238 |
239 | it("should cancel the result") {
240 | expect(cancelSentinel).to(beTrue())
241 | }
242 | }
243 | }
244 | }
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+SnoozeTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class FutureSnoozeTests: QuickSpec {
6 | override func spec() {
7 | describe("Snoozing a Future") {
8 | var sut: Promise!
9 | var result: Future!
10 | var failSentinel: Error?
11 | var successSentinel: Int?
12 | var cancelSentinel: Bool?
13 |
14 | beforeEach {
15 | sut = Promise()
16 |
17 | cancelSentinel = false
18 | failSentinel = nil
19 | successSentinel = nil
20 |
21 | result = sut.future.snooze(0.5)
22 |
23 | result
24 | .onSuccess { successSentinel = $0 }
25 | .onCancel { cancelSentinel = true }
26 | .onFailure { failSentinel = $0 }
27 | }
28 |
29 | context("when the original promise fails") {
30 | let error = TestError.anotherError
31 |
32 | beforeEach {
33 | sut.fail(error)
34 | }
35 |
36 | it("should not immediately fail the snoozed future") {
37 | expect(failSentinel).to(beNil())
38 | }
39 |
40 | it("should eventually fail the snoozed future") {
41 | expect(failSentinel).toEventuallyNot(beNil(), timeout: 0.8)
42 | }
43 |
44 | it("should fail with the same error") {
45 | expect(failSentinel as? TestError).toEventually(equal(error), timeout: 0.8)
46 | }
47 | }
48 |
49 | context("when the original promise is canceled") {
50 | beforeEach {
51 | sut.cancel()
52 | }
53 |
54 | it("should not immediately cancel the snoozed future") {
55 | expect(cancelSentinel).notTo(beTrue())
56 | }
57 |
58 | it("should cancel the snoozed future later") {
59 | expect(cancelSentinel).toEventually(beTrue(), timeout: 0.8)
60 | }
61 | }
62 |
63 | context("when the original promise succeeds") {
64 | let value = 3
65 |
66 | beforeEach {
67 | sut.succeed(value)
68 | }
69 |
70 | it("should not immediately succeed the snoozed future") {
71 | expect(successSentinel).to(beNil())
72 | }
73 |
74 | it("should eventually succeed the snoozed future") {
75 | expect(successSentinel).toEventuallyNot(beNil(), timeout: 0.8)
76 | }
77 |
78 | it("should succeed with the same error") {
79 | expect(successSentinel).toEventually(equal(value), timeout: 0.8)
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+TimeoutTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class FutureTimeoutTests: QuickSpec {
6 | override func spec() {
7 | describe("Timing out a Future") {
8 | var sut: Promise!
9 | var result: Future!
10 | var failSentinel: Error?
11 | var successSentinel: Int?
12 | var cancelSentinel: Bool?
13 |
14 | beforeEach {
15 | sut = Promise()
16 |
17 | cancelSentinel = false
18 | failSentinel = nil
19 | successSentinel = nil
20 |
21 | result = sut.future.timeout(after: 0.5)
22 |
23 | result
24 | .onSuccess { successSentinel = $0 }
25 | .onCancel { cancelSentinel = true }
26 | .onFailure { failSentinel = $0 }
27 | }
28 |
29 | context("when the original promise fails") {
30 | let error = TestError.anotherError
31 |
32 | beforeEach {
33 | sut.fail(error)
34 | }
35 |
36 | it("should immediately fail the result future") {
37 | expect(failSentinel).notTo(beNil())
38 | }
39 |
40 | it("should fail with the same error") {
41 | expect(failSentinel as? TestError).to(equal(error))
42 | }
43 | }
44 |
45 | context("when the original promise succeeds") {
46 | let value = 3
47 |
48 | beforeEach {
49 | sut.succeed(value)
50 | }
51 |
52 | it("should immediately succeed the result future") {
53 | expect(successSentinel).notTo(beNil())
54 | }
55 |
56 | it("should succeed with the same error") {
57 | expect(successSentinel).to(equal(value))
58 | }
59 | }
60 |
61 | context("when the original promise is canceled") {
62 | beforeEach {
63 | sut.cancel()
64 | }
65 |
66 | it("should immediately cancel the result future") {
67 | expect(cancelSentinel).to(beTrue())
68 | }
69 | }
70 |
71 | context("when the promise doesn't succeed nor fail") {
72 | it("should eventually fail the future") {
73 | expect(failSentinel).toEventuallyNot(beNil(), timeout: 0.6)
74 | }
75 |
76 | it("should fail with the right error") {
77 | expect(failSentinel as? FutureError).toEventually(equal(FutureError.timeout), timeout: 0.6)
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Future+TraverseTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class SequenceTraverseTests: QuickSpec {
6 | override func spec() {
7 | describe("Traversing a list of items") {
8 | var promises: [Promise]!
9 | var traversedFuture: Future<[Int]>!
10 | var successValue: [Int]?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 | var valuesToTraverse: [Int]!
14 | var originalPromisesCanceled: [Bool]!
15 |
16 | beforeEach {
17 | valuesToTraverse = Array(0...5)
18 | originalPromisesCanceled = (0..]!
153 | var traversedFuture: Future<[String]>!
154 | var successValue: [String]?
155 | var expectedResult: [String]!
156 | var valuesToTraverse: [String]!
157 |
158 | beforeEach {
159 | valuesToTraverse = (0...5).map {
160 | "\($0)"
161 | }
162 |
163 | promises = valuesToTraverse.map { _ in
164 | Promise()
165 | }
166 |
167 | successValue = nil
168 | traversedFuture = valuesToTraverse
169 | .traverse { idx in
170 | promises[Int(idx)!].future
171 | }
172 |
173 | traversedFuture.onSuccess {
174 | successValue = $0
175 | }
176 |
177 | expectedResult = Array(0..!
9 | var zippedFuture: Future<(String, Int)>!
10 | var successValue: (String, Int)?
11 | var failureValue: Error?
12 | var wasCanceled: Bool!
13 |
14 | beforeEach {
15 | promise = Promise()
16 |
17 | wasCanceled = false
18 | successValue = nil
19 | failureValue = nil
20 | }
21 |
22 | context("when done with another Future") {
23 | var other: Promise!
24 |
25 | beforeEach {
26 | other = Promise()
27 |
28 | zippedFuture = promise.future.zip(other.future)
29 |
30 | zippedFuture.onCompletion { result in
31 | switch result {
32 | case .success(let value):
33 | successValue = value
34 | case .error(let error):
35 | failureValue = error
36 | case .cancelled:
37 | wasCanceled = true
38 | }
39 | }
40 | }
41 |
42 | context("when the first future fails") {
43 | let error = TestError.anotherError
44 |
45 | beforeEach {
46 | promise.fail(error)
47 | }
48 |
49 | it("should fail the zipped future") {
50 | expect(failureValue).notTo(beNil())
51 | }
52 |
53 | it("should fail with the right error") {
54 | expect(failureValue as? TestError).to(equal(error))
55 | }
56 |
57 | it("should not succeed the zipped future") {
58 | expect(successValue).to(beNil())
59 | }
60 |
61 | it("should not cancel the zipped future") {
62 | expect(wasCanceled).to(beFalse())
63 | }
64 | }
65 |
66 | context("when the first future is canceled") {
67 | beforeEach {
68 | promise.cancel()
69 | }
70 |
71 | it("should not fail the zipped future") {
72 | expect(failureValue).to(beNil())
73 | }
74 |
75 | it("should not succeed the zipped future") {
76 | expect(successValue).to(beNil())
77 | }
78 |
79 | it("should cancel the zipped future") {
80 | expect(wasCanceled).to(beTrue())
81 | }
82 | }
83 |
84 | context("when the first future succeeds") {
85 | let firstResult = "yes"
86 |
87 | beforeEach {
88 | promise.succeed(firstResult)
89 | }
90 |
91 | context("when the second future fails") {
92 | let error = TestError.simpleError
93 |
94 | beforeEach {
95 | other.fail(error)
96 | }
97 |
98 | it("should fail the zipped future") {
99 | expect(failureValue).notTo(beNil())
100 | }
101 |
102 | it("should fail with the right error") {
103 | expect(failureValue as? TestError).to(equal(error))
104 | }
105 |
106 | it("should not succeed the zipped future") {
107 | expect(successValue).to(beNil())
108 | }
109 |
110 | it("should not cancel the zipped future") {
111 | expect(wasCanceled).to(beFalse())
112 | }
113 | }
114 |
115 | context("when the second future is canceled") {
116 | beforeEach {
117 | other.cancel()
118 | }
119 |
120 | it("should not fail the zipped future") {
121 | expect(failureValue).to(beNil())
122 | }
123 |
124 | it("should not succeed the zipped future") {
125 | expect(successValue).to(beNil())
126 | }
127 |
128 | it("should cancel the zipped future") {
129 | expect(wasCanceled).to(beTrue())
130 | }
131 | }
132 |
133 | context("when the second future succeeds") {
134 | let secondResult = 10
135 |
136 | beforeEach {
137 | other.succeed(secondResult)
138 | }
139 |
140 | it("should not fail the zipped future") {
141 | expect(failureValue).to(beNil())
142 | }
143 |
144 | it("should succeed the zipped future") {
145 | expect(successValue).notTo(beNil())
146 | }
147 |
148 | it("should succeed with the right value") {
149 | expect(successValue?.0).to(equal(firstResult))
150 | expect(successValue?.1).to(equal(secondResult))
151 | }
152 |
153 | it("should not cancel the zipped future") {
154 | expect(wasCanceled).to(beFalse())
155 | }
156 | }
157 | }
158 | }
159 |
160 | context("when done with a Result") {
161 | var other: Result!
162 |
163 | context("when the result is success") {
164 | let otherResult = 10
165 |
166 | beforeEach {
167 | other = Result.success(otherResult)
168 |
169 | zippedFuture = promise.future.zip(other)
170 |
171 | zippedFuture.onCompletion { result in
172 | switch result {
173 | case .success(let value):
174 | successValue = value
175 | case .error(let error):
176 | failureValue = error
177 | case .cancelled:
178 | wasCanceled = true
179 | }
180 | }
181 | }
182 |
183 | context("when the future fails") {
184 | let error = TestError.anotherError
185 |
186 | beforeEach {
187 | promise.fail(error)
188 | }
189 |
190 | it("should fail the zipped future") {
191 | expect(failureValue).notTo(beNil())
192 | }
193 |
194 | it("should fail with the right error") {
195 | expect(failureValue as? TestError).to(equal(error))
196 | }
197 |
198 | it("should not succeed the zipped future") {
199 | expect(successValue).to(beNil())
200 | }
201 |
202 | it("should not cancel the zipped future") {
203 | expect(wasCanceled).to(beFalse())
204 | }
205 | }
206 |
207 | context("when the future is canceled") {
208 | beforeEach {
209 | promise.cancel()
210 | }
211 |
212 | it("should not fail the zipped future") {
213 | expect(failureValue).to(beNil())
214 | }
215 |
216 | it("should not succeed the zipped future") {
217 | expect(successValue).to(beNil())
218 | }
219 |
220 | it("should cancel the zipped future") {
221 | expect(wasCanceled).to(beTrue())
222 | }
223 | }
224 |
225 | context("when the future succeeds") {
226 | let firstResult = "yay"
227 |
228 | beforeEach {
229 | promise.succeed(firstResult)
230 | }
231 |
232 | it("should not fail the zipped future") {
233 | expect(failureValue).to(beNil())
234 | }
235 |
236 | it("should succeed the zipped future") {
237 | expect(successValue).notTo(beNil())
238 | }
239 |
240 | it("should succeed with the right value") {
241 | expect(successValue?.0).to(equal(firstResult))
242 | expect(successValue?.1).to(equal(otherResult))
243 | }
244 |
245 | it("should not cancel the zipped future") {
246 | expect(wasCanceled).to(beFalse())
247 | }
248 | }
249 | }
250 |
251 | context("when the result is error") {
252 | let error = TestError.simpleError
253 |
254 | beforeEach {
255 | other = Result.error(error)
256 |
257 | zippedFuture = promise.future.zip(other)
258 |
259 | zippedFuture.onCompletion { result in
260 | switch result {
261 | case .success(let value):
262 | successValue = value
263 | case .error(let error):
264 | failureValue = error
265 | case .cancelled:
266 | wasCanceled = true
267 | }
268 | }
269 | }
270 |
271 | it("should immediately fail the zipped future") {
272 | expect(failureValue).notTo(beNil())
273 | }
274 |
275 | it("should immediately fail with the right error") {
276 | expect(failureValue as? TestError).to(equal(error))
277 | }
278 |
279 | it("should not succeed the zipped future") {
280 | expect(successValue).to(beNil())
281 | }
282 |
283 | it("should not cancel the zipped future") {
284 | expect(wasCanceled).to(beFalse())
285 | }
286 |
287 | context("when the future fails") {
288 | let anotherError = TestError.anotherError
289 |
290 | beforeEach {
291 | promise.fail(anotherError)
292 | }
293 |
294 | it("should fail the zipped future") {
295 | expect(failureValue).notTo(beNil())
296 | }
297 |
298 | it("should fail with the right error") {
299 | expect(failureValue as? TestError).to(equal(error))
300 | }
301 |
302 | it("should not succeed the zipped future") {
303 | expect(successValue).to(beNil())
304 | }
305 |
306 | it("should not cancel the zipped future") {
307 | expect(wasCanceled).to(beFalse())
308 | }
309 | }
310 |
311 | context("when the future is canceled") {
312 | beforeEach {
313 | promise.cancel()
314 | }
315 |
316 | it("should fail the zipped future") {
317 | expect(failureValue).notTo(beNil())
318 | }
319 |
320 | it("should fail with the right error") {
321 | expect(failureValue as? TestError).to(equal(error))
322 | }
323 |
324 | it("should not succeed the zipped future") {
325 | expect(successValue).to(beNil())
326 | }
327 |
328 | it("should not cancel the zipped future") {
329 | expect(wasCanceled).to(beFalse())
330 | }
331 | }
332 |
333 | context("when the future succeeds") {
334 | beforeEach {
335 | promise.succeed("ops")
336 | }
337 |
338 | it("should fail the zipped future") {
339 | expect(failureValue).notTo(beNil())
340 | }
341 |
342 | it("should fail with the right error") {
343 | expect(failureValue as? TestError).to(equal(error))
344 | }
345 |
346 | it("should not succeed the zipped future") {
347 | expect(successValue).to(beNil())
348 | }
349 |
350 | it("should not cancel the zipped future") {
351 | expect(wasCanceled).to(beFalse())
352 | }
353 | }
354 | }
355 |
356 | context("when the result is cancelled") {
357 | beforeEach {
358 | other = Result.cancelled
359 |
360 | zippedFuture = promise.future.zip(other)
361 |
362 | zippedFuture.onCompletion { result in
363 | switch result {
364 | case .success(let value):
365 | successValue = value
366 | case .error(let error):
367 | failureValue = error
368 | case .cancelled:
369 | wasCanceled = true
370 | }
371 | }
372 | }
373 |
374 | it("should not fail the zipped future") {
375 | expect(failureValue).to(beNil())
376 | }
377 |
378 | it("should not succeed the zipped future") {
379 | expect(successValue).to(beNil())
380 | }
381 |
382 | it("should immediately cancel the zipped future") {
383 | expect(wasCanceled).to(beTrue())
384 | }
385 |
386 | context("when the future fails") {
387 | let error = TestError.anotherError
388 |
389 | beforeEach {
390 | promise.fail(error)
391 | }
392 |
393 | it("should not fail the zipped future") {
394 | expect(failureValue).to(beNil())
395 | }
396 |
397 | it("should not succeed the zipped future") {
398 | expect(successValue).to(beNil())
399 | }
400 |
401 | it("should cancel the zipped future") {
402 | expect(wasCanceled).to(beTrue())
403 | }
404 | }
405 |
406 | context("when the future is canceled") {
407 | beforeEach {
408 | promise.cancel()
409 | }
410 |
411 | it("should not fail the zipped future") {
412 | expect(failureValue).to(beNil())
413 | }
414 |
415 | it("should not succeed the zipped future") {
416 | expect(successValue).to(beNil())
417 | }
418 |
419 | it("should cancel the zipped future") {
420 | expect(wasCanceled).to(beTrue())
421 | }
422 | }
423 |
424 | context("when the future succeeds") {
425 | beforeEach {
426 | promise.succeed(":(")
427 | }
428 |
429 | it("should not fail the zipped future") {
430 | expect(failureValue).to(beNil())
431 | }
432 |
433 | it("should not succeed the zipped future") {
434 | expect(successValue).to(beNil())
435 | }
436 |
437 | it("should cancel the zipped future") {
438 | expect(wasCanceled).to(beTrue())
439 | }
440 | }
441 | }
442 | }
443 | }
444 | }
445 | }
446 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/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 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Result+FilterTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class ResultFilterTests: QuickSpec {
6 | override func spec() {
7 | describe("Filtering a Result") {
8 | var original: Result!
9 | var filteredResult: Result!
10 |
11 | context("when done through a simple closure") {
12 | let filteringClosure: (Int) -> Bool = { num in
13 | num > 0
14 | }
15 |
16 | context("when the original result is error") {
17 | let error = TestError.simpleError
18 |
19 | beforeEach {
20 | original = .error(error)
21 |
22 | filteredResult = original.filter(filteringClosure)
23 | }
24 |
25 | it("should also fail the filtered result") {
26 | var didFail = false
27 |
28 | if case .some(.error) = filteredResult {
29 | didFail = true
30 | }
31 |
32 | expect(didFail).to(beTrue())
33 | }
34 |
35 | it("should fail the filtered result with the same error") {
36 | var actualError: Error?
37 |
38 | if case .some(.error(let error)) = filteredResult {
39 | actualError = error
40 | }
41 |
42 | expect(actualError as? TestError).to(equal(error))
43 | }
44 | }
45 |
46 | context("when the original result is canceled") {
47 | beforeEach {
48 | original = .cancelled
49 |
50 | filteredResult = original.filter(filteringClosure)
51 | }
52 |
53 | it("should also cancel the filtered result") {
54 | var wasCanceled = false
55 |
56 | if case .some(.cancelled) = filteredResult {
57 | wasCanceled = true
58 | }
59 |
60 | expect(wasCanceled).to(beTrue())
61 | }
62 | }
63 |
64 | context("when the original result is success") {
65 | context("when the success value satisfies the condition") {
66 | let result = 20
67 |
68 | beforeEach {
69 | original = .success(result)
70 | filteredResult = original.filter(filteringClosure)
71 | }
72 |
73 | it("should also succeed the filtered result") {
74 | var didSucceed = false
75 |
76 | if case .some(.success) = filteredResult {
77 | didSucceed = true
78 | }
79 |
80 | expect(didSucceed).to(beTrue())
81 | }
82 |
83 | it("should succeed the filtered result with the original value") {
84 | var successValue: Int?
85 |
86 | if case .some(.success(let value)) = filteredResult {
87 | successValue = value
88 | }
89 |
90 | expect(successValue).to(equal(result))
91 | }
92 | }
93 |
94 | context("when the success value doesn't satisfy the condition") {
95 | let result = -20
96 |
97 | beforeEach {
98 | original = .success(result)
99 | filteredResult = original.filter(filteringClosure)
100 | }
101 |
102 | it("should fail the filtered result") {
103 | var didFail = false
104 |
105 | if case .some(.error) = filteredResult {
106 | didFail = true
107 | }
108 |
109 | expect(didFail).to(beTrue())
110 | }
111 |
112 | it("should fail the filtered result with the right error") {
113 | var error: Error?
114 |
115 | if case .some(.error(let err)) = filteredResult {
116 | error = err
117 | }
118 |
119 | expect(error as? ResultFilteringError).to(equal(ResultFilteringError.conditionUnsatisfied))
120 | }
121 | }
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/Result+MapTests.swift:
--------------------------------------------------------------------------------
1 | import Quick
2 | import Nimble
3 | import PiedPiper
4 |
5 | class ResultMapTests: QuickSpec {
6 | override func spec() {
7 | describe("Mapping a Result") {
8 | var result: Result!
9 | var mappedResult: Result!
10 |
11 | context("when done through a simple closure") {
12 | let mappingClosure: (String) -> Int = { str in
13 | return 1
14 | }
15 |
16 | context("when the original Result is an error") {
17 | let error = TestError.simpleError
18 |
19 | beforeEach {
20 | result = .error(error)
21 | mappedResult = result.map(mappingClosure)
22 | }
23 |
24 | it("should also fail the mapped result") {
25 | var sentinel = false
26 |
27 | if case .some(.error) = mappedResult {
28 | sentinel = true
29 | }
30 |
31 | expect(sentinel).to(beTrue())
32 | }
33 |
34 | it("should fail the mapped result with the same error") {
35 | var failureValue: Error?
36 |
37 | if case .some(.error(let error)) = mappedResult {
38 | failureValue = error
39 | }
40 |
41 | expect(failureValue as? TestError).to(equal(error))
42 | }
43 | }
44 |
45 | context("when the original Result is canceled") {
46 | beforeEach {
47 | result = .cancelled
48 | mappedResult = result.map(mappingClosure)
49 | }
50 |
51 | it("should also cancel the mapped Result") {
52 | var wasCanceled = false
53 |
54 | if case .some(.cancelled) = mappedResult {
55 | wasCanceled = true
56 | }
57 |
58 | expect(wasCanceled).to(beTrue())
59 | }
60 | }
61 |
62 | context("when the original Result is .Success") {
63 | let value = "Eureka!"
64 |
65 | beforeEach {
66 | result = .success(value)
67 | mappedResult = result.map(mappingClosure)
68 | }
69 |
70 | it("should also succeed the mapped Result") {
71 | var successValue: Int?
72 |
73 | if case .some(.success(let value)) = mappedResult {
74 | successValue = value
75 | }
76 |
77 | expect(successValue).notTo(beNil())
78 | }
79 |
80 | it("should succeed the mapped future with the mapped value") {
81 | var successValue: Int?
82 |
83 | if case .some(.success(let value)) = mappedResult {
84 | successValue = value
85 | }
86 |
87 | expect(successValue).to(equal(mappingClosure(value)))
88 | }
89 | }
90 | }
91 |
92 | context("when done through a closure that can throw") {
93 | let mappingClosure: (String) throws -> Int = { str in
94 | if str == "throw" {
95 | throw TestError.anotherError
96 | } else {
97 | return 1
98 | }
99 | }
100 |
101 | context("when the original Result is an error") {
102 | let error = TestError.simpleError
103 |
104 | beforeEach {
105 | result = .error(error)
106 | mappedResult = result.map(mappingClosure)
107 | }
108 |
109 | it("should also fail the mapped result") {
110 | var sentinel = false
111 |
112 | if case .some(.error) = mappedResult {
113 | sentinel = true
114 | }
115 |
116 | expect(sentinel).to(beTrue())
117 | }
118 |
119 | it("should fail the mapped result with the same error") {
120 | var failureValue: Error?
121 |
122 | if case .some(.error(let error)) = mappedResult {
123 | failureValue = error
124 | }
125 |
126 | expect(failureValue as? TestError).to(equal(error))
127 | }
128 | }
129 |
130 | context("when the original Result is canceled") {
131 | beforeEach {
132 | result = .cancelled
133 | mappedResult = result.map(mappingClosure)
134 | }
135 |
136 | it("should also cancel the mapped Result") {
137 | var wasCanceled = false
138 |
139 | if case .some(.cancelled) = mappedResult {
140 | wasCanceled = true
141 | }
142 |
143 | expect(wasCanceled).to(beTrue())
144 | }
145 | }
146 |
147 | context("when the original Result is .Success") {
148 | context("when the closure doesn't throw") {
149 | let value = "Eureka!"
150 |
151 | beforeEach {
152 | result = .success(value)
153 | mappedResult = result.map(mappingClosure)
154 | }
155 |
156 | it("should also succeed the mapped Result") {
157 | var successValue: Int?
158 |
159 | if case .some(.success(let value)) = mappedResult {
160 | successValue = value
161 | }
162 |
163 | expect(successValue).notTo(beNil())
164 | }
165 |
166 | it("should succeed the mapped Result with the mapped value") {
167 | var successValue: Int?
168 |
169 | if case .some(.success(let value)) = mappedResult {
170 | successValue = value
171 | }
172 |
173 | expect(successValue).to(equal(try! mappingClosure(value)))
174 | }
175 | }
176 |
177 | context("when the closure throws") {
178 | let value = "throw"
179 |
180 | beforeEach {
181 | result = .success(value)
182 | mappedResult = result.map(mappingClosure)
183 | }
184 |
185 | it("should fail the mapped Result") {
186 | var failureValue: Error?
187 |
188 | if case .some(.error(let error)) = mappedResult {
189 | failureValue = error
190 | }
191 |
192 | expect(failureValue).notTo(beNil())
193 | }
194 |
195 | it("should fail the mapped future with the right error") {
196 | var failureValue: Error?
197 |
198 | if case .some(.error(let error)) = mappedResult {
199 | failureValue = error
200 | }
201 |
202 | expect(failureValue as? TestError).to(equal(TestError.anotherError))
203 | }
204 | }
205 | }
206 | }
207 | }
208 | }
209 | }
210 |
--------------------------------------------------------------------------------
/Tests/PiedPiperTests/ResultTests.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Quick
3 | import Nimble
4 | import PiedPiper
5 |
6 | class ResultTests: QuickSpec {
7 | override func spec() {
8 | describe("Result") {
9 | var result: Result!
10 |
11 | context("the error property") {
12 | var error: Error?
13 |
14 | context("when the result is a success") {
15 | beforeEach {
16 | result = .success("Hi!")
17 | error = result.error
18 | }
19 |
20 | it("should be nil") {
21 | expect(error).to(beNil())
22 | }
23 | }
24 |
25 | context("when the result is cancelled") {
26 | beforeEach {
27 | result = .cancelled
28 | error = result.error
29 | }
30 |
31 | it("should be nil") {
32 | expect(error).to(beNil())
33 | }
34 | }
35 |
36 | context("when the result is a failure") {
37 | beforeEach {
38 | result = .error(TestError.simpleError)
39 | error = result.error
40 | }
41 |
42 | it("should not be nil") {
43 | expect(error).notTo(beNil())
44 | }
45 |
46 | it("should be the right error") {
47 | expect(error as? TestError).to(equal(TestError.simpleError))
48 | }
49 | }
50 | }
51 |
52 | context("the value property") {
53 | var value: String?
54 |
55 | context("when the result is a success") {
56 | let expected = "Hi!"
57 |
58 | beforeEach {
59 | result = .success(expected)
60 | value = result.value
61 | }
62 |
63 | it("should not be nil") {
64 | expect(value).notTo(beNil())
65 | }
66 |
67 | it("should be the right value") {
68 | expect(value).to(equal(expected))
69 | }
70 | }
71 |
72 | context("when the result is cancelled") {
73 | beforeEach {
74 | result = .cancelled
75 | value = result.value
76 | }
77 |
78 | it("should be nil") {
79 | expect(value).to(beNil())
80 | }
81 | }
82 |
83 | context("when the result is a failure") {
84 | beforeEach {
85 | result = .error(TestError.anotherError)
86 | value = result.value
87 | }
88 |
89 | it("should be nil") {
90 | expect(value).to(beNil())
91 | }
92 | }
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # https://github.com/KrauseFx/fastlane/tree/master/docs
2 | # All available actions: https://github.com/KrauseFx/fastlane/blob/master/docs/Actions.md
3 |
4 | fastlane_version "2.140"
5 |
6 | desc "Runs all the tests"
7 | lane :test do
8 | ENV["FASTLANE_XCODE_LIST_TIMEOUT"] = "30"
9 | clear_derived_data
10 | spm(command: "test", configuration: "release")
11 | end
12 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ================
3 | # Installation
4 |
5 | Make sure you have the latest version of the Xcode command line tools installed:
6 |
7 | ```
8 | xcode-select --install
9 | ```
10 |
11 | Install _fastlane_ using
12 | ```
13 | [sudo] gem install fastlane -NV
14 | ```
15 | or alternatively using `brew cask install fastlane`
16 |
17 | # Available Actions
18 | ### test
19 | ```
20 | fastlane test
21 | ```
22 | Runs all the tests
23 |
24 | ----
25 |
26 | This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run.
27 | More information about fastlane can be found on [fastlane.tools](https://fastlane.tools).
28 | The documentation of fastlane can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
29 |
--------------------------------------------------------------------------------
/fastlane/Scanfile:
--------------------------------------------------------------------------------
1 | # https://github.com/fastlane/scan#scanfile
2 |
3 | clean true
4 |
--------------------------------------------------------------------------------
/travis/bootstrap-if-needed.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cmp -s Cartfile.resolved Carthage/Cartfile.resolved || travis/bootstrap.sh
4 |
--------------------------------------------------------------------------------
/travis/bootstrap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | bundle exec carthage bootstrap --platform ios
4 | cp Cartfile.resolved Carthage
5 |
--------------------------------------------------------------------------------