├── .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 | --------------------------------------------------------------------------------