Add PromiseKit dependency

- Added PromiseKit dependency
This commit is contained in:
2018-11-15 22:08:00 -04:00
parent 2689d86c18
commit be7b6b5881
541 changed files with 46282 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
#import <Foundation/NSNotification.h>
#import <PromiseKit/AnyPromise.h>
/**
To import the `NSNotificationCenter` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `NSNotificationCenter` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
#import <PromiseKit/PromiseKit.h>
*/
@interface NSNotificationCenter (PromiseKit)
/**
Observe the named notification once.
[NSNotificationCenter once:UIKeyboardWillShowNotification].then(^(id note, id userInfo){
UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
CGFloat duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] floatValue];
return [UIView promiseWithDuration:duration delay:0.0 options:(curve << 16) animations:^{
}];
});
@warning *Important* Promises only resolve once. If you need your block to execute more than once then use `-addObserverForName:object:queue:usingBlock:`.
@param notificationName The name of the notification for which to register the observer.
@return A promise that fulfills with two parameters:
1. The NSNotification object.
2. The NSNotifications userInfo property.
*/
+ (AnyPromise *)once:(NSString *)notificationName NS_REFINED_FOR_SWIFT;
@end

View File

@@ -0,0 +1,18 @@
#import <Foundation/NSOperation.h>
#import <Foundation/NSThread.h>
#import "PMKFoundation.h"
@implementation NSNotificationCenter (PromiseKit)
+ (AnyPromise *)once:(NSString *)name {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
__block id identifier;
identifier = [[NSNotificationCenter defaultCenter] addObserverForName:name object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[[NSNotificationCenter defaultCenter] removeObserver:identifier name:name object:nil];
identifier = nil;
resolve(PMKManifold(note, note.userInfo));
}];
}];
}
@end

View File

@@ -0,0 +1,33 @@
import Foundation
#if !PMKCocoaPods
import PromiseKit
#endif
/**
To import the `NSNotificationCenter` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `NSNotificationCenter` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
import PromiseKit
*/
extension NotificationCenter {
/// Observe the named notification once
public func observe(once name: Notification.Name, object: Any? = nil) -> Guarantee<Notification> {
let (promise, fulfill) = Guarantee<Notification>.pending()
#if os(Linux) && ((swift(>=4.0) && !swift(>=4.0.1)) || (swift(>=3.0) && !swift(>=3.2.1)))
let id = addObserver(forName: name, object: object, queue: nil, usingBlock: fulfill)
#else
let id = addObserver(forName: name, object: object, queue: nil, using: fulfill)
#endif
promise.done { _ in self.removeObserver(id) }
return promise
}
}

View File

@@ -0,0 +1,57 @@
import Foundation
#if !PMKCocoaPods
import PromiseKit
#endif
/**
To import the `NSObject` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `NSObject` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
import PromiseKit
*/
extension NSObject {
/**
- Returns: A promise that resolves when the provided keyPath changes.
- Warning: *Important* The promise must not outlive the object under observation.
- SeeAlso: Apples KVO documentation.
*/
public func observe(_: PMKNamespacer, keyPath: String) -> Guarantee<Any?> {
return Guarantee { KVOProxy(observee: self, keyPath: keyPath, resolve: $0) }
}
}
private class KVOProxy: NSObject {
var retainCycle: KVOProxy?
let fulfill: (Any?) -> Void
@discardableResult
init(observee: NSObject, keyPath: String, resolve: @escaping (Any?) -> Void) {
fulfill = resolve
super.init()
observee.addObserver(self, forKeyPath: keyPath, options: NSKeyValueObservingOptions.new, context: pointer)
retainCycle = self
}
fileprivate override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let change = change, context == pointer {
defer { retainCycle = nil }
fulfill(change[NSKeyValueChangeKey.newKey])
if let object = object as? NSObject, let keyPath = keyPath {
object.removeObserver(self, forKeyPath: keyPath)
}
}
}
private lazy var pointer: UnsafeMutableRawPointer = {
return Unmanaged<KVOProxy>.passUnretained(self).toOpaque()
}()
}

View File

@@ -0,0 +1,53 @@
#if TARGET_OS_MAC && !TARGET_OS_EMBEDDED && !TARGET_OS_SIMULATOR
#import <Foundation/NSTask.h>
#import <PromiseKit/AnyPromise.h>
#define PMKTaskErrorLaunchPathKey @"PMKTaskErrorLaunchPathKey"
#define PMKTaskErrorArgumentsKey @"PMKTaskErrorArgumentsKey"
#define PMKTaskErrorStandardOutputKey @"PMKTaskErrorStandardOutputKey"
#define PMKTaskErrorStandardErrorKey @"PMKTaskErrorStandardErrorKey"
#define PMKTaskErrorExitStatusKey @"PMKTaskErrorExitStatusKey"
/**
To import the `NSTask` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `NSTask` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
#import <PromiseKit/PromiseKit.h>
*/
@interface NSTask (PromiseKit)
/**
Launches the receiver and resolves when it exits.
If the task fails the promise is rejected with code `PMKTaskError`, and
`userInfo` keys: `PMKTaskErrorStandardOutputKey`,
`PMKTaskErrorStandardErrorKey` and `PMKTaskErrorExitStatusKey`.
NSTask *task = [NSTask new];
task.launchPath = @"/usr/bin/basename";
task.arguments = @[@"/usr/bin/sleep"];
[task promise].then(^(NSString *stdout){
//…
});
@return A promise that fulfills with three parameters:
1) The stdout interpreted as a UTF8 string.
2) The stderr interpreted as a UTF8 string.
3) The stdout as `NSData`.
*/
- (AnyPromise *)promise NS_REFINED_FOR_SWIFT;
@end
#endif

View File

@@ -0,0 +1,59 @@
#import <Foundation/NSDictionary.h>
#import <Foundation/NSFileHandle.h>
#import <PromiseKit/PromiseKit.h>
#import <Foundation/NSString.h>
#import <Foundation/NSError.h>
#if TARGET_OS_MAC && !TARGET_OS_EMBEDDED && !TARGET_OS_SIMULATOR
#import "NSTask+AnyPromise.h"
@implementation NSTask (PromiseKit)
- (AnyPromise *)promise {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
self.standardOutput = [NSPipe pipe];
self.standardError = [NSPipe pipe];
self.terminationHandler = ^(NSTask *task){
id stdoutData = [[task.standardOutput fileHandleForReading] readDataToEndOfFile];
id stdoutString = [[NSString alloc] initWithData:stdoutData encoding:NSUTF8StringEncoding];
id stderrData = [[task.standardError fileHandleForReading] readDataToEndOfFile];
id stderrString = [[NSString alloc] initWithData:stderrData encoding:NSUTF8StringEncoding];
if (task.terminationReason == NSTaskTerminationReasonExit && self.terminationStatus == 0) {
resolve(PMKManifold(stdoutString, stderrString, stdoutData));
} else {
id cmd = [NSMutableArray arrayWithObject:task.launchPath];
[cmd addObjectsFromArray:task.arguments];
cmd = [cmd componentsJoinedByString:@" "];
id info = @{
NSLocalizedDescriptionKey:[NSString stringWithFormat:@"Failed executing: %@.", cmd],
PMKTaskErrorStandardOutputKey: stdoutString,
PMKTaskErrorStandardErrorKey: stderrString,
PMKTaskErrorExitStatusKey: @(task.terminationStatus),
};
resolve([NSError errorWithDomain:PMKErrorDomain code:PMKTaskError userInfo:info]);
}
};
#if __clang_major__ >= 9
if (@available(macOS 10.13, *)) {
NSError *error = nil;
if (![self launchAndReturnError:&error]) {
resolve(error);
}
} else {
[self launch];
}
#else
[self launch]; // might @throw
#endif
}];
}
@end
#endif

View File

@@ -0,0 +1,79 @@
#import <Foundation/NSDictionary.h>
#import <Foundation/NSURLSession.h>
#import <Foundation/NSURLRequest.h>
#import <PromiseKit/AnyPromise.h>
#define PMKURLErrorFailingURLResponseKey @"PMKURLErrorFailingURLResponseKey"
#define PMKURLErrorFailingDataKey @"PMKURLErrorFailingDataKey"
#define PMKURLErrorFailingStringKey @"PMKURLErrorFailingStringKey"
#define PMKJSONErrorJSONObjectKey @"PMKJSONErrorJSONObjectKey"
/**
Really we shouldnt assume JSON for (application|text)/(x-)javascript,
really we should return a String of Javascript. However in practice
for the apps we write it *will be* JSON. Thus if you actually want
a Javascript String, use the promise variant of our category functions.
*/
#define PMKHTTPURLResponseIsJSON(rsp) [@[@"application/json", @"text/json", @"text/javascript", @"application/x-javascript", @"application/javascript"] containsObject:[rsp MIMEType]]
#define PMKHTTPURLResponseIsImage(rsp) [@[@"image/tiff", @"image/jpeg", @"image/gif", @"image/png", @"image/ico", @"image/x-icon", @"image/bmp", @"image/x-bmp", @"image/x-xbitmap", @"image/x-win-bitmap"] containsObject:[rsp MIMEType]]
#define PMKHTTPURLResponseIsText(rsp) [[rsp MIMEType] hasPrefix:@"text/"]
#define PMKJSONDeserializationOptions ((NSJSONReadingOptions)(NSJSONReadingAllowFragments | NSJSONReadingMutableContainers))
/**
To import the `NSURLSession` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `NSURLConnection` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
#import <PromiseKit/PromiseKit.h>
*/
@interface NSURLSession (PromiseKit)
/**
Creates a task that retrieves the contents of a URL based on the
specified URL request object.
PromiseKit automatically deserializes the raw HTTP data response into the
appropriate rich data type based on the mime type the server provides.
Thus if the response is JSON you will get the deserialized JSON response.
PromiseKit supports decoding into strings, JSON and UIImages.
However if your server does not provide a rich content-type, you will
just get `NSData`. This is rare, but a good example we came across was
downloading files from Dropbox.
PromiseKit goes to quite some lengths to provide good `NSError` objects
for error conditions at all stages of the HTTP to rich-data type
pipeline. We provide the following additional `userInfo` keys as
appropriate:
- `PMKURLErrorFailingDataKey`
- `PMKURLErrorFailingStringKey`
- `PMKURLErrorFailingURLResponseKey`
[[NSURLConnection sharedSession] promiseDataTaskWithRequest:rq].then(^(id response){
// response is probably an NSDictionary deserialized from JSON
});
@param request The URL request.
@return A promise that fulfills with three parameters:
1) The deserialized data response.
2) The `NSHTTPURLResponse`.
3) The raw `NSData` response.
@see https://github.com/mxcl/OMGHTTPURLRQ
*/
- (AnyPromise *)promiseDataTaskWithRequest:(NSURLRequest *)request NS_REFINED_FOR_SWIFT;
@end

View File

@@ -0,0 +1,113 @@
#import <Foundation/NSJSONSerialization.h>
#import <Foundation/NSURLResponse.h>
#import <CoreFoundation/CFString.h>
#import "NSURLSession+AnyPromise.h"
#import <Foundation/NSOperation.h>
#import <Foundation/NSURLError.h>
#import <PromiseKit/PromiseKit.h>
#import <CoreFoundation/CFURL.h>
#import <Foundation/NSThread.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSError.h>
#import <Foundation/NSURL.h>
@implementation NSURLSession (PromiseKit)
- (AnyPromise *)promiseDataTaskWithRequest:(NSURLRequest *)rq {
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
[[self dataTaskWithRequest:rq completionHandler:^(NSData *data, id rsp, NSError *urlError){
assert(![NSThread isMainThread]);
PMKResolver fulfiller = ^(id responseObject){
resolve(PMKManifold(responseObject, rsp, data));
};
PMKResolver rejecter = ^(NSError *error){
id userInfo = error.userInfo.mutableCopy ?: [NSMutableDictionary new];
if (data) userInfo[PMKURLErrorFailingDataKey] = data;
if (rsp) userInfo[PMKURLErrorFailingURLResponseKey] = rsp;
error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo];
resolve(error);
};
NSStringEncoding (^stringEncoding)(void) = ^NSStringEncoding{
id encodingName = [rsp textEncodingName];
if (encodingName) {
CFStringEncoding encoding = CFStringConvertIANACharSetNameToEncoding((CFStringRef)encodingName);
if (encoding != kCFStringEncodingInvalidId)
return CFStringConvertEncodingToNSStringEncoding(encoding);
}
return NSUTF8StringEncoding;
};
if (urlError) {
rejecter(urlError);
} else if (![rsp isKindOfClass:[NSHTTPURLResponse class]]) {
fulfiller(data);
} else if ([rsp statusCode] < 200 || [rsp statusCode] >= 300) {
id info = @{
NSLocalizedDescriptionKey: @"The server returned a bad HTTP response code",
NSURLErrorFailingURLStringErrorKey: rq.URL.absoluteString,
NSURLErrorFailingURLErrorKey: rq.URL
};
id err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorBadServerResponse userInfo:info];
rejecter(err);
} else if (PMKHTTPURLResponseIsJSON(rsp)) {
// work around ever-so-common Rails workaround: https://github.com/rails/rails/issues/1742
if ([rsp expectedContentLength] == 1 && [data isEqualToData:[NSData dataWithBytes:" " length:1]])
return fulfiller(nil);
NSError *err = nil;
id json = [NSJSONSerialization JSONObjectWithData:data options:PMKJSONDeserializationOptions error:&err];
if (!err) {
fulfiller(json);
} else {
id userInfo = err.userInfo.mutableCopy;
if (data) {
NSString *string = [[NSString alloc] initWithData:data encoding:stringEncoding()];
if (string)
userInfo[PMKURLErrorFailingStringKey] = string;
}
long long length = [rsp expectedContentLength];
id bytes = length <= 0 ? @"" : [NSString stringWithFormat:@"%lld bytes", length];
id fmt = @"The server claimed a %@ JSON response, but decoding failed with: %@";
userInfo[NSLocalizedDescriptionKey] = [NSString stringWithFormat:fmt, bytes, userInfo[NSLocalizedDescriptionKey]];
err = [NSError errorWithDomain:err.domain code:err.code userInfo:userInfo];
rejecter(err);
}
#ifdef UIKIT_EXTERN
} else if (PMKHTTPURLResponseIsImage(rsp)) {
UIImage *image = [[UIImage alloc] initWithData:data];
image = [[UIImage alloc] initWithCGImage:[image CGImage] scale:image.scale orientation:image.imageOrientation];
if (image)
fulfiller(image);
else {
id info = @{
NSLocalizedDescriptionKey: @"The server returned invalid image data",
NSURLErrorFailingURLStringErrorKey: rq.URL.absoluteString,
NSURLErrorFailingURLErrorKey: rq.URL
};
id err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:info];
rejecter(err);
}
#endif
} else if (PMKHTTPURLResponseIsText(rsp)) {
id str = [[NSString alloc] initWithData:data encoding:stringEncoding()];
if (str)
fulfiller(str);
else {
id info = @{
NSLocalizedDescriptionKey: @"The server returned invalid string data",
NSURLErrorFailingURLStringErrorKey: rq.URL.absoluteString,
NSURLErrorFailingURLErrorKey: rq.URL
};
id err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:info];
rejecter(err);
}
} else {
fulfiller(data);
}
}] resume];
}];
}
@end

View File

@@ -0,0 +1,241 @@
import Foundation
#if !PMKCocoaPods
import PromiseKit
#endif
/**
To import the `NSURLSession` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `NSURLSession` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
import PromiseKit
*/
extension URLSession {
/**
Example usage:
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap { data, _ in
try JSONSerialization.jsonObject(with: data) as? [String: Any]
}.then { json in
//
}
We recommend the use of [OMGHTTPURLRQ] which allows you to construct correct REST requests:
firstly {
let rq = OMGHTTPURLRQ.POST(url, json: parameters)
URLSession.shared.dataTask(.promise, with: rq)
}.then { data, urlResponse in
//
}
We provide a convenience initializer for `String` specifically for this promise:
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap(String.init).then { string in
// decoded per the string encoding specified by the server
}.then { string in
print("response: string")
}
Other common types can be easily decoded using compactMap also:
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap {
UIImage(data: $0)
}.then {
self.imageView.image = $0
}
Though if you do decode the image this way, we recommend inflating it on a background thread
first as this will improve main thread performance when rendering the image:
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.compactMap(on: QoS.userInitiated) { data, _ in
guard let img = UIImage(data: data) else { return nil }
_ = cgImage?.dataProvider?.data
return img
}.then {
self.imageView.image = $0
}
- Parameter convertible: A URL or URLRequest.
- Returns: A promise that represents the URL request.
- SeeAlso: [OMGHTTPURLRQ]
- Remark: We deliberately dont provide a `URLRequestConvertible` for `String` because in our experience, you should be explicit with this error path to make good apps.
[OMGHTTPURLRQ]: https://github.com/mxcl/OMGHTTPURLRQ
*/
public func dataTask(_: PMKNamespacer, with convertible: URLRequestConvertible) -> Promise<(data: Data, response: URLResponse)> {
return Promise { dataTask(with: convertible.pmkRequest, completionHandler: adapter($0)).resume() }
}
public func uploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, from data: Data) -> Promise<(data: Data, response: URLResponse)> {
return Promise { uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0)).resume() }
}
public func uploadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, fromFile file: URL) -> Promise<(data: Data, response: URLResponse)> {
return Promise { uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0)).resume() }
}
/// - Remark: we force a `to` parameter because Apple deletes the downloaded file immediately after the underyling completion handler returns.
/// - Note: we do not create the destination directory for you, because we move the file with FileManager.moveItem which changes it behavior depending on the directory status of the URL you provide. So create your own directory first!
public func downloadTask(_: PMKNamespacer, with convertible: URLRequestConvertible, to saveLocation: URL) -> Promise<(saveLocation: URL, response: URLResponse)> {
return Promise { seal in
downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
if let error = err {
seal.reject(error)
} else if let rsp = rsp, let tmp = tmp {
do {
try FileManager.default.moveItem(at: tmp, to: saveLocation)
seal.fulfill((saveLocation, rsp))
} catch {
seal.reject(error)
}
} else {
seal.reject(PMKError.invalidCallingConvention)
}
}).resume()
}
}
}
public protocol URLRequestConvertible {
var pmkRequest: URLRequest { get }
}
extension URLRequest: URLRequestConvertible {
public var pmkRequest: URLRequest { return self }
}
extension URL: URLRequestConvertible {
public var pmkRequest: URLRequest { return URLRequest(url: self) }
}
#if !os(Linux)
public extension String {
/**
- Remark: useful when converting a `URLSession` response into a `String`
firstly {
URLSession.shared.dataTask(.promise, with: rq)
}.map(String.init).done {
print($0)
}
*/
init?(data: Data, urlResponse: URLResponse) {
guard let str = String(bytes: data, encoding: urlResponse.stringEncoding ?? .utf8) else {
return nil
}
self.init(str)
}
}
private extension URLResponse {
var stringEncoding: String.Encoding? {
guard let encodingName = textEncodingName else { return nil }
let encoding = CFStringConvertIANACharSetNameToEncoding(encodingName as CFString)
guard encoding != kCFStringEncodingInvalidId else { return nil }
return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(encoding))
}
}
#endif
private func adapter<T, U>(_ seal: Resolver<(data: T, response: U)>) -> (T?, U?, Error?) -> Void {
return { t, u, e in
if let t = t, let u = u {
seal.fulfill((t, u))
} else if let e = e {
seal.reject(e)
} else {
seal.reject(PMKError.invalidCallingConvention)
}
}
}
#if swift(>=3.1)
public enum PMKHTTPError: Error, LocalizedError, CustomStringConvertible {
case badStatusCode(Int, Data, HTTPURLResponse)
public var errorDescription: String? {
func url(_ rsp: URLResponse) -> String {
return rsp.url?.absoluteString ?? "nil"
}
switch self {
case .badStatusCode(401, _, let response):
return "Unauthorized (\(url(response))"
case .badStatusCode(let code, _, let response):
return "Invalid HTTP response (\(code)) for \(url(response))."
}
}
#if swift(>=4.0)
public func decodeResponse<T: Decodable>(_ t: T.Type, decoder: JSONDecoder = JSONDecoder()) -> T? {
switch self {
case .badStatusCode(_, let data, _):
return try? decoder.decode(t, from: data)
}
}
#endif
//TODO rename responseJSON
public var jsonDictionary: Any? {
switch self {
case .badStatusCode(_, let data, _):
return try? JSONSerialization.jsonObject(with: data)
}
}
var responseBodyString: String? {
switch self {
case .badStatusCode(_, let data, _):
return String(data: data, encoding: .utf8)
}
}
public var failureReason: String? {
return responseBodyString
}
public var description: String {
switch self {
case .badStatusCode(let code, let data, let response):
var dict: [String: Any] = [
"Status Code": code,
"Body": String(data: data, encoding: .utf8) ?? "\(data.count) bytes"
]
dict["URL"] = response.url
dict["Headers"] = response.allHeaderFields
return "<NSHTTPResponse> \(NSDictionary(dictionary: dict))" // as NSDictionary makes the output look like NSHTTPURLResponse looks
}
}
}
public extension Promise where T == (data: Data, response: URLResponse) {
func validate() -> Promise<T> {
return map {
guard let response = $0.response as? HTTPURLResponse else { return $0 }
switch response.statusCode {
case 200..<300:
return $0
case let code:
throw PMKHTTPError.badStatusCode(code, $0.data, response)
}
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
#import "NSNotificationCenter+AnyPromise.h"
#import "NSURLSession+AnyPromise.h"
#import "NSTask+AnyPromise.h"

View File

@@ -0,0 +1,190 @@
import Foundation
#if !PMKCocoaPods
import PromiseKit
#endif
#if os(macOS)
/**
To import the `Process` category:
use_frameworks!
pod "PromiseKit/Foundation"
Or `Process` is one of the categories imported by the umbrella pod:
use_frameworks!
pod "PromiseKit"
And then in your sources:
import PromiseKit
*/
extension Process {
/**
Launches the receiver and resolves when it exits.
let proc = Process()
proc.launchPath = "/bin/ls"
proc.arguments = ["/bin"]
proc.launch(.promise).compactMap { std in
String(data: std.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
}.then { stdout in
print(str)
}
*/
public func launch(_: PMKNamespacer) -> Promise<(out: Pipe, err: Pipe)> {
let (stdout, stderr) = (Pipe(), Pipe())
do {
standardOutput = stdout
standardError = stderr
#if swift(>=4.0)
if #available(OSX 10.13, *) {
try run()
} else if let path = launchPath, FileManager.default.isExecutableFile(atPath: path) {
launch()
} else {
throw PMKError.notExecutable(launchPath)
}
#else
guard let path = launchPath, FileManager.default.isExecutableFile(atPath: path) else {
throw PMKError.notExecutable(launchPath)
}
launch()
#endif
} catch {
return Promise(error: error)
}
var q: DispatchQueue {
if #available(macOS 10.10, iOS 8.0, tvOS 9.0, watchOS 2.0, *) {
return DispatchQueue.global(qos: .default)
} else {
return DispatchQueue.global(priority: .default)
}
}
return Promise { seal in
q.async {
self.waitUntilExit()
guard self.terminationReason == .exit, self.terminationStatus == 0 else {
let stdoutData = try? self.readDataFromPipe(stdout)
let stderrData = try? self.readDataFromPipe(stderr)
let stdoutString = stdoutData.flatMap { (data: Data) -> String? in String(data: data, encoding: .utf8) }
let stderrString = stderrData.flatMap { (data: Data) -> String? in String(data: data, encoding: .utf8) }
return seal.reject(PMKError.execution(process: self, standardOutput: stdoutString, standardError: stderrString))
}
seal.fulfill((stdout, stderr))
}
}
}
private func readDataFromPipe(_ pipe: Pipe) throws -> Data {
let handle = pipe.fileHandleForReading
defer { handle.closeFile() }
// Someday, NSFileHandle will probably be updated with throwing equivalents to its read and write methods,
// as NSTask has, to avoid raising exceptions and crashing the app.
// Unfortunately that day has not yet come, so use the underlying BSD calls for now.
let fd = handle.fileDescriptor
let bufsize = 1024 * 8
let buf = UnsafeMutablePointer<UInt8>.allocate(capacity: bufsize)
#if swift(>=4.1)
defer { buf.deallocate() }
#else
defer { buf.deallocate(capacity: bufsize) }
#endif
var data = Data()
while true {
let bytesRead = read(fd, buf, bufsize)
if bytesRead == 0 {
break
}
if bytesRead < 0 {
throw POSIXError.Code(rawValue: errno).map { POSIXError($0) } ?? CocoaError(.fileReadUnknown)
}
data.append(buf, count: bytesRead)
}
return data
}
/**
The error generated by PromiseKits `Process` extension
*/
public enum PMKError {
/// NOT AVAILABLE ON 10.13 and above because Apple provide this error handling themselves
case notExecutable(String?)
case execution(process: Process, standardOutput: String?, standardError: String?)
}
}
extension Process.PMKError: LocalizedError {
public var errorDescription: String? {
switch self {
case .notExecutable(let path?):
return "File not executable: \(path)"
case .notExecutable(nil):
return "No launch path specified"
case .execution(process: let task, standardOutput: _, standardError: _):
return "Failed executing: `\(task)` (\(task.terminationStatus))."
}
}
}
public extension Promise where T == (out: Pipe, err: Pipe) {
func print() -> Promise<T> {
return tap { result in
switch result {
case .fulfilled(let raw):
let stdout = String(data: raw.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
let stderr = String(data: raw.err.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
Swift.print("stdout: `\(stdout ?? "")`")
Swift.print("stderr: `\(stderr ?? "")`")
case .rejected(let err):
Swift.print(err)
}
}
}
}
extension Process {
/// Provided because Foundations is USELESS
open override var description: String {
let launchPath = self.launchPath ?? "$0"
var args = [launchPath]
arguments.flatMap{ args += $0 }
return args.map { arg in
let contains: Bool
#if swift(>=3.2)
contains = arg.contains(" ")
#else
contains = arg.characters.contains(" ")
#endif
if contains {
return "\"\(arg)\""
} else if arg == "" {
return "\"\""
} else {
return arg
}
}.joined(separator: " ")
}
}
#endif

View File

@@ -0,0 +1,26 @@
import Foundation
#if !PMKCocoaPods
import PromiseKit
#endif
/**
- Returns: A promise that resolves when the provided object deallocates
- Important: The promise is not guarenteed to resolve immediately when the provided object is deallocated. So you cannot write code that depends on exact timing.
*/
public func after(life object: NSObject) -> Guarantee<Void> {
var reaper = objc_getAssociatedObject(object, &handle) as? GrimReaper
if reaper == nil {
reaper = GrimReaper()
objc_setAssociatedObject(object, &handle, reaper, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
return reaper!.promise
}
private var handle: UInt8 = 0
private class GrimReaper: NSObject {
deinit {
fulfill(())
}
let (promise, fulfill) = Guarantee<Void>.pending()
}