From d803b537c32d16b1789e620471e2077dca4b9a7e Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Fri, 3 Nov 2017 19:00:18 +0900 Subject: [PATCH 01/18] Replace callbacks in `Gist` with `Promise`s --- Package.resolved | 9 +++++++++ Package.swift | 5 +++-- Sources/TweetupKit/Gist.swift | 16 +++++++++------- Sources/TweetupKit/Speaker.swift | 2 +- Tests/TweetupKitTests/GistTests.swift | 4 ++-- 5 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Package.resolved b/Package.resolved index b819ded..69921cf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -9,6 +9,15 @@ "revision": "2489c960e177e9413e52dabcedaa36e5f0705fd1", "version": "2.0.0-beta" } + }, + { + "package": "PromiseK", + "repositoryURL": "https://github.com/koher/PromiseK.git", + "state": { + "branch": null, + "revision": "8c339c6a27b5b834dec65a66e790d736c3739c9a", + "version": "3.0.0-alpha.4" + } } ] }, diff --git a/Package.swift b/Package.swift index f91fc9d..4b692cf 100644 --- a/Package.swift +++ b/Package.swift @@ -8,10 +8,11 @@ let package = Package( .library(name: "TweetupKit", targets: ["TweetupKit"]), ], dependencies: [ + .package(url: "https://github.com/koher/PromiseK.git", from: "3.0.0-alpha"), .package(url: "https://github.com/swift-tweets/OAuthSwift.git", from: "2.0.0-beta"), ], targets: [ - .target(name: "TweetupKit", dependencies: ["OAuthSwift"]), - .testTarget(name: "TweetupKitTests", dependencies: ["TweetupKit", "OAuthSwift"]), + .target(name: "TweetupKit", dependencies: ["PromiseK", "OAuthSwift"]), + .testTarget(name: "TweetupKitTests", dependencies: ["TweetupKit", "PromiseK", "OAuthSwift"]), ] ) diff --git a/Sources/TweetupKit/Gist.swift b/Sources/TweetupKit/Gist.swift index 04c197c..afa8db9 100644 --- a/Sources/TweetupKit/Gist.swift +++ b/Sources/TweetupKit/Gist.swift @@ -1,7 +1,8 @@ import Foundation +import PromiseK internal struct Gist { - static func createGist(description: String, code: Code, accessToken: String, callback: @escaping (() throws -> String) -> ()) { + static func createGist(description: String, code: Code, accessToken: String) -> Promise<() throws -> String> { let session = URLSession(configuration: .ephemeral, delegate: nil, delegateQueue: .current) var request = URLRequest(url: URL(string: "https://api.github.com/gists")!) request.httpMethod = "POST" @@ -17,23 +18,24 @@ internal struct Gist { "files": files ] let data = try! JSONSerialization.data(withJSONObject: json) - let task = session.uploadTask(with: request, from: data) { responseData, response, error in - callback { - if let error = error { throw error } + return Promise { fulfill in + let task = session.uploadTask(with: request, from: data) { responseData, response, error in + if let error = error { fulfill { throw error }; return } guard let response = response as? HTTPURLResponse else { fatalError("Never reaches here.") } let responseJson: [String: Any] = try! JSONSerialization.jsonObject(with: responseData!) as! [String: Any] // never fails guard response.statusCode == 201 else { - throw APIError(response: response, json: responseJson) + fulfill { throw APIError(response: response, json: responseJson) } + return } guard let id = responseJson["id"] as? String else { fatalError("Never reaches here.") } - return id + fulfill { id } } + task.resume() } - task.resume() } } diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index 68159c6..e6ed647 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -140,7 +140,7 @@ public struct Speaker { return } - Gist.createGist(description: tweet.body, code: code, accessToken: githubToken) { getId in + Gist.createGist(description: tweet.body, code: code, accessToken: githubToken).get { getId in callback { let id = try getId() return try Tweet(body: "\(tweet.body)\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id)))) diff --git a/Tests/TweetupKitTests/GistTests.swift b/Tests/TweetupKitTests/GistTests.swift index 9a71d0f..213f25c 100644 --- a/Tests/TweetupKitTests/GistTests.swift +++ b/Tests/TweetupKitTests/GistTests.swift @@ -27,7 +27,7 @@ class GistTests: XCTestCase { do { let expectation = self.expectation(description: "") - Gist.createGist(description: "", code: Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"), accessToken: accessToken) { getId in + Gist.createGist(description: "", code: Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"), accessToken: accessToken).get { getId in defer { expectation.fulfill() } @@ -45,7 +45,7 @@ class GistTests: XCTestCase { do { // illegal access token let expectation = self.expectation(description: "") - Gist.createGist(description: "", code: Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"), accessToken: "") { getId in + Gist.createGist(description: "", code: Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"), accessToken: "").get { getId in defer { expectation.fulfill() } From 8ddc669aee69dbf62a6c65be295217b923e8bfab Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Fri, 3 Nov 2017 18:58:07 +0900 Subject: [PATCH 02/18] Update for Swift 4 --- Package.resolved | 16 ++ Package.swift | 11 +- Sources/{ => TweetupKit}/APIError.swift | 0 .../{ => TweetupKit}/ArrayExtensions.swift | 0 .../ArraySliceExtensions.swift | 0 Sources/{ => TweetupKit}/Async.swift | 0 Sources/{ => TweetupKit}/Code.swift | 0 Sources/{ => TweetupKit}/CodeRenderer.swift | 0 Sources/{ => TweetupKit}/Gist.swift | 0 Sources/{ => TweetupKit}/Image.swift | 0 Sources/{ => TweetupKit}/Language.swift | 0 .../NSRegularExpressionExtensions.swift | 0 .../NSTextCheckingResultExtensions.swift | 2 +- .../{ => TweetupKit}/OAuthCredential.swift | 0 Sources/{ => TweetupKit}/Parser.swift | 12 +- Sources/{ => TweetupKit}/Speaker.swift | 0 .../{ => TweetupKit}/StringExtensions.swift | 0 Sources/{ => TweetupKit}/Tweet.swift | 0 Sources/{ => TweetupKit}/Twitter.swift | 0 Tests/TweetupKitTests/ParserTests.swift | 138 +++++++++--------- Tests/TweetupKitTests/TweetTests.swift | 8 +- 21 files changed, 106 insertions(+), 81 deletions(-) create mode 100644 Package.resolved rename Sources/{ => TweetupKit}/APIError.swift (100%) rename Sources/{ => TweetupKit}/ArrayExtensions.swift (100%) rename Sources/{ => TweetupKit}/ArraySliceExtensions.swift (100%) rename Sources/{ => TweetupKit}/Async.swift (100%) rename Sources/{ => TweetupKit}/Code.swift (100%) rename Sources/{ => TweetupKit}/CodeRenderer.swift (100%) rename Sources/{ => TweetupKit}/Gist.swift (100%) rename Sources/{ => TweetupKit}/Image.swift (100%) rename Sources/{ => TweetupKit}/Language.swift (100%) rename Sources/{ => TweetupKit}/NSRegularExpressionExtensions.swift (100%) rename Sources/{ => TweetupKit}/NSTextCheckingResultExtensions.swift (84%) rename Sources/{ => TweetupKit}/OAuthCredential.swift (100%) rename Sources/{ => TweetupKit}/Parser.swift (93%) rename Sources/{ => TweetupKit}/Speaker.swift (100%) rename Sources/{ => TweetupKit}/StringExtensions.swift (100%) rename Sources/{ => TweetupKit}/Tweet.swift (100%) rename Sources/{ => TweetupKit}/Twitter.swift (100%) diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..b819ded --- /dev/null +++ b/Package.resolved @@ -0,0 +1,16 @@ +{ + "object": { + "pins": [ + { + "package": "OAuthSwift", + "repositoryURL": "https://github.com/swift-tweets/OAuthSwift.git", + "state": { + "branch": null, + "revision": "2489c960e177e9413e52dabcedaa36e5f0705fd1", + "version": "2.0.0-beta" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index ca8695b..f91fc9d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,17 @@ +// swift-tools-version:4.0 + import PackageDescription let package = Package( name: "TweetupKit", + products: [ + .library(name: "TweetupKit", targets: ["TweetupKit"]), + ], dependencies: [ - .Package(url: "https://github.com/swift-tweets/OAuthSwift.git", "2.0.0-beta") + .package(url: "https://github.com/swift-tweets/OAuthSwift.git", from: "2.0.0-beta"), + ], + targets: [ + .target(name: "TweetupKit", dependencies: ["OAuthSwift"]), + .testTarget(name: "TweetupKitTests", dependencies: ["TweetupKit", "OAuthSwift"]), ] ) diff --git a/Sources/APIError.swift b/Sources/TweetupKit/APIError.swift similarity index 100% rename from Sources/APIError.swift rename to Sources/TweetupKit/APIError.swift diff --git a/Sources/ArrayExtensions.swift b/Sources/TweetupKit/ArrayExtensions.swift similarity index 100% rename from Sources/ArrayExtensions.swift rename to Sources/TweetupKit/ArrayExtensions.swift diff --git a/Sources/ArraySliceExtensions.swift b/Sources/TweetupKit/ArraySliceExtensions.swift similarity index 100% rename from Sources/ArraySliceExtensions.swift rename to Sources/TweetupKit/ArraySliceExtensions.swift diff --git a/Sources/Async.swift b/Sources/TweetupKit/Async.swift similarity index 100% rename from Sources/Async.swift rename to Sources/TweetupKit/Async.swift diff --git a/Sources/Code.swift b/Sources/TweetupKit/Code.swift similarity index 100% rename from Sources/Code.swift rename to Sources/TweetupKit/Code.swift diff --git a/Sources/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift similarity index 100% rename from Sources/CodeRenderer.swift rename to Sources/TweetupKit/CodeRenderer.swift diff --git a/Sources/Gist.swift b/Sources/TweetupKit/Gist.swift similarity index 100% rename from Sources/Gist.swift rename to Sources/TweetupKit/Gist.swift diff --git a/Sources/Image.swift b/Sources/TweetupKit/Image.swift similarity index 100% rename from Sources/Image.swift rename to Sources/TweetupKit/Image.swift diff --git a/Sources/Language.swift b/Sources/TweetupKit/Language.swift similarity index 100% rename from Sources/Language.swift rename to Sources/TweetupKit/Language.swift diff --git a/Sources/NSRegularExpressionExtensions.swift b/Sources/TweetupKit/NSRegularExpressionExtensions.swift similarity index 100% rename from Sources/NSRegularExpressionExtensions.swift rename to Sources/TweetupKit/NSRegularExpressionExtensions.swift diff --git a/Sources/NSTextCheckingResultExtensions.swift b/Sources/TweetupKit/NSTextCheckingResultExtensions.swift similarity index 84% rename from Sources/NSTextCheckingResultExtensions.swift rename to Sources/TweetupKit/NSTextCheckingResultExtensions.swift index a5335ad..fd9b4a8 100644 --- a/Sources/NSTextCheckingResultExtensions.swift +++ b/Sources/TweetupKit/NSTextCheckingResultExtensions.swift @@ -2,7 +2,7 @@ import Foundation extension NSTextCheckingResult { internal func validRangeAt(_ index: Int) -> NSRange? { - let range = rangeAt(index) + let range = self.range(at: index) guard range.location != NSNotFound else { return nil } diff --git a/Sources/OAuthCredential.swift b/Sources/TweetupKit/OAuthCredential.swift similarity index 100% rename from Sources/OAuthCredential.swift rename to Sources/TweetupKit/OAuthCredential.swift diff --git a/Sources/Parser.swift b/Sources/TweetupKit/Parser.swift similarity index 93% rename from Sources/Parser.swift rename to Sources/TweetupKit/Parser.swift index 7de83c7..6fdb8be 100644 --- a/Sources/Parser.swift +++ b/Sources/TweetupKit/Parser.swift @@ -36,7 +36,7 @@ extension Tweet { internal static func containsHashTag(body: String, hashTag: String) -> Bool { return Tweet.hashTagInTweetPattern.matches(in: body).map { - (body as NSString).substring(with: $0.rangeAt(2)) + (body as NSString).substring(with: $0.range(at: 2)) }.contains(hashTag) } @@ -58,17 +58,17 @@ extension Tweet { } internal static func matchingAttachments(in string: String, pattern: NSRegularExpression ,initializer: (String, NSTextCheckingResult) throws -> T) throws -> [(NSRange, T)] { - return try pattern.matches(in: string).map { ($0.rangeAt(0), try initializer(string, $0)) } + return try pattern.matches(in: string).map { ($0.range(at: 0), try initializer(string, $0)) } } } extension Code { fileprivate init(string: String, matchingResult: NSTextCheckingResult) throws { let nsString = string as NSString - let language = Language(identifier: nsString.substring(with: matchingResult.rangeAt(1))) + let language = Language(identifier: nsString.substring(with: matchingResult.range(at: 1))) let fileName: String do { - let range = matchingResult.rangeAt(3) + let range = matchingResult.range(at: 3) if (range.location == NSNotFound) { guard let filenameExtension = language.filenameExtension else { throw TweetParseError.codeWithoutFileName(string) @@ -81,7 +81,7 @@ extension Code { self.init( language: language, fileName: fileName, - body: nsString.substring(with: matchingResult.rangeAt(4)) + body: nsString.substring(with: matchingResult.range(at: 4)) ) } } @@ -101,7 +101,7 @@ extension Image { } self.init( - alternativeText: string.substring(with: matchingResult.rangeAt(1)), + alternativeText: string.substring(with: matchingResult.range(at: 1)), source: source ) } diff --git a/Sources/Speaker.swift b/Sources/TweetupKit/Speaker.swift similarity index 100% rename from Sources/Speaker.swift rename to Sources/TweetupKit/Speaker.swift diff --git a/Sources/StringExtensions.swift b/Sources/TweetupKit/StringExtensions.swift similarity index 100% rename from Sources/StringExtensions.swift rename to Sources/TweetupKit/StringExtensions.swift diff --git a/Sources/Tweet.swift b/Sources/TweetupKit/Tweet.swift similarity index 100% rename from Sources/Tweet.swift rename to Sources/TweetupKit/Tweet.swift diff --git a/Sources/Twitter.swift b/Sources/TweetupKit/Twitter.swift similarity index 100% rename from Sources/Twitter.swift rename to Sources/TweetupKit/Twitter.swift diff --git a/Tests/TweetupKitTests/ParserTests.swift b/Tests/TweetupKitTests/ParserTests.swift index 95447c6..9b93b86 100644 --- a/Tests/TweetupKitTests/ParserTests.swift +++ b/Tests/TweetupKitTests/ParserTests.swift @@ -275,12 +275,12 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 6) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "```swift:hello.swift\nprint(\"Hello world!\")\n```") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), ":hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "print(\"Hello world!\")\n") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "print(\"Hello world!\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "```swift:hello.swift\nprint(\"Hello world!\")\n```") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), ":hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "print(\"Hello world!\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "print(\"Hello world!\")\n") } do { @@ -291,12 +291,12 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 6) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "```swift:hello.swift\nlet s = \"Hello world!\"\nprint(s)\n```") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), ":hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "let s = \"Hello world!\"\nprint(s)\n") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "print(s)\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "```swift:hello.swift\nlet s = \"Hello world!\"\nprint(s)\n```") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), ":hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "let s = \"Hello world!\"\nprint(s)\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "print(s)\n") } do { @@ -307,12 +307,12 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 6) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), ":hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), ":hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") } do { @@ -323,12 +323,12 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 6) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "```swift:hello.swift\nprint(\"Hello world!\")\n```") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), ":hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "print(\"Hello world!\")\n") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "print(\"Hello world!\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "```swift:hello.swift\nprint(\"Hello world!\")\n```") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), ":hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "print(\"Hello world!\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "print(\"Hello world!\")\n") } do { @@ -339,12 +339,12 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 6) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "```swift:hello.swift\nprint(\"Hello world!\")\n```") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), ":hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "print(\"Hello world!\")\n") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "print(\"Hello world!\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "```swift:hello.swift\nprint(\"Hello world!\")\n```") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), ":hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "print(\"Hello world!\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "print(\"Hello world!\")\n") } do { @@ -355,12 +355,12 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 6) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), ":hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "hello.swift") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), ":hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "hello.swift") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") } } @@ -373,10 +373,10 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 8) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "![](path/to/image.png)") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "path/to/image.png") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(7)), "path/to/image.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "![](path/to/image.png)") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "path/to/image.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 7)), "path/to/image.png") } do { @@ -387,10 +387,10 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 8) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "![alternative text](path/to/image.png)") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "alternative text") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "path/to/image.png") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(7)), "path/to/image.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "![alternative text](path/to/image.png)") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "alternative text") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "path/to/image.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 7)), "path/to/image.png") } do { // twitter @@ -401,11 +401,11 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 8) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "![alternative text](twitter:471592142565957632)") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "alternative text") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "twitter:471592142565957632") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(3)), "twitter:471592142565957632") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(4)), "471592142565957632") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "![alternative text](twitter:471592142565957632)") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "alternative text") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "twitter:471592142565957632") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 3)), "twitter:471592142565957632") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 4)), "471592142565957632") } do { // gist @@ -416,11 +416,11 @@ class ParserTests: XCTestCase { let result = results[0] XCTAssertEqual(result.numberOfRanges, 8) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "![alternative text](gist:aa5a315d61ae9438b18d)") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "alternative text") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "gist:aa5a315d61ae9438b18d") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(5)), "gist:aa5a315d61ae9438b18d") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(6)), "aa5a315d61ae9438b18d") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "![alternative text](gist:aa5a315d61ae9438b18d)") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "alternative text") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "gist:aa5a315d61ae9438b18d") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 5)), "gist:aa5a315d61ae9438b18d") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 6)), "aa5a315d61ae9438b18d") } do { @@ -432,19 +432,19 @@ class ParserTests: XCTestCase { do { let result = results[0] XCTAssertEqual(result.numberOfRanges, 8) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "![alternative text 1](path/to/image1.png)") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "alternative text 1") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "path/to/image1.png") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(7)), "path/to/image1.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "![alternative text 1](path/to/image1.png)") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "alternative text 1") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "path/to/image1.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 7)), "path/to/image1.png") } do { let result = results[1] XCTAssertEqual(result.numberOfRanges, 8) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(0)), "![alternative text 2](path/to/image2.png)") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(1)), "alternative text 2") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "path/to/image2.png") - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(7)), "path/to/image2.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 0)), "![alternative text 2](path/to/image2.png)") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 1)), "alternative text 2") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "path/to/image2.png") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 7)), "path/to/image2.png") } } @@ -462,7 +462,7 @@ class ParserTests: XCTestCase { let results = Tweet.hashTagPattern.matches(in: string) XCTAssertEqual(results.count, 1) - XCTAssertEqual((string as NSString).substring(with: results[0].rangeAt(0)), "#abc") + XCTAssertEqual((string as NSString).substring(with: results[0].range(at: 0)), "#abc") } do { @@ -490,7 +490,7 @@ class ParserTests: XCTestCase { do { let result = results[0] XCTAssertEqual(result.numberOfRanges, 4) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#swtws") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "#swtws") } } @@ -503,7 +503,7 @@ class ParserTests: XCTestCase { do { let result = results[0] XCTAssertEqual(result.numberOfRanges, 4) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#swtws") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "#swtws") } } @@ -516,7 +516,7 @@ class ParserTests: XCTestCase { do { let result = results[0] XCTAssertEqual(result.numberOfRanges, 4) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#swtws") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "#swtws") } } @@ -529,19 +529,19 @@ class ParserTests: XCTestCase { do { let result = results[0] XCTAssertEqual(result.numberOfRanges, 4) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#abc") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "#abc") } do { let result = results[1] XCTAssertEqual(result.numberOfRanges, 4) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#def") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "#def") } do { let result = results[2] XCTAssertEqual(result.numberOfRanges, 4) - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#ghi") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "#ghi") } } } diff --git a/Tests/TweetupKitTests/TweetTests.swift b/Tests/TweetupKitTests/TweetTests.swift index 1a02eb4..407873f 100644 --- a/Tests/TweetupKitTests/TweetTests.swift +++ b/Tests/TweetupKitTests/TweetTests.swift @@ -153,12 +153,12 @@ class TweetTests: XCTestCase { do { let result = results[0] - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "http://qaleido.space") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "http://qaleido.space") } do { let result = results[1] - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "https://swift-tweets.github.io/?foo=bar&baz=qux#tweeters") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "https://swift-tweets.github.io/?foo=bar&baz=qux#tweeters") } } @@ -170,12 +170,12 @@ class TweetTests: XCTestCase { do { let result = results[0] - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "Swift.org") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "Swift.org") } do { let result = results[1] - XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "https://swift.org/compiler-stdlib/#compiler-architecture") + XCTAssertEqual((string as NSString).substring(with: result.range(at: 2)), "https://swift.org/compiler-stdlib/#compiler-architecture") } } } From bfe5579b17c7eaacd4bc350f36302a25e4e52285 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Fri, 3 Nov 2017 19:14:26 +0900 Subject: [PATCH 03/18] Replace callbacks in `Twitter` with `Promise`s --- Sources/TweetupKit/Speaker.swift | 4 +-- Sources/TweetupKit/Twitter.swift | 43 +++++++++++++----------- Tests/TweetupKitTests/TwitterTests.swift | 8 ++--- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index e6ed647..98ad611 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -70,7 +70,7 @@ public struct Speaker { } else { mediaId = nil } - Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential) { getId in + Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential).get { getId in callback { try getId() } @@ -101,7 +101,7 @@ public struct Speaker { do { let imagePath = Speaker.imagePath(path, from: baseDirectoryPath) - Twitter.upload(media: try Data(contentsOf: URL(fileURLWithPath: imagePath)), credential: twitterCredential) { getId in + Twitter.upload(media: try Data(contentsOf: URL(fileURLWithPath: imagePath)), credential: twitterCredential).get { getId in callback { let id = try getId() return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .twitter(id)))) diff --git a/Sources/TweetupKit/Twitter.swift b/Sources/TweetupKit/Twitter.swift index 56e2e4b..086985f 100644 --- a/Sources/TweetupKit/Twitter.swift +++ b/Sources/TweetupKit/Twitter.swift @@ -1,8 +1,9 @@ import OAuthSwift import Foundation +import PromiseK internal struct Twitter { - static func update(status: String, mediaId: String? = nil, credential: OAuthCredential, callback: @escaping (() throws -> (String, String)) -> ()) { + static func update(status: String, mediaId: String? = nil, credential: OAuthCredential) -> Promise<() throws -> (String, String)> { let client = OAuthSwiftClient(credential: credential) client.sessionFactory.queue = { .current } @@ -12,30 +13,34 @@ internal struct Twitter { if let mediaId = mediaId { parameters["media_ids"] = mediaId } - - _ = client.post( - "https://api.twitter.com/1.1/statuses/update.json", - parameters: parameters, - callback: callback - ) { response in - let json = try! JSONSerialization.jsonObject(with: response.data) as! [String: Any] // `!` never fails - return (json["id_str"] as! String, (json["user"] as! [String: Any])["screen_name"] as! String) // `!` never fails + + return Promise<() throws -> (String, String)> { fulfill in + _ = client.post( + "https://api.twitter.com/1.1/statuses/update.json", + parameters: parameters, + callback: fulfill as! (() throws -> (String, String)) -> () // `as!`f as a workaround + ) { response in + let json = try! JSONSerialization.jsonObject(with: response.data) as! [String: Any] // `!` never fails + return (json["id_str"] as! String, (json["user"] as! [String: Any])["screen_name"] as! String) // `!` never fails + } } } - static func upload(media: Data, credential: OAuthCredential, callback: @escaping (() throws -> String) -> ()) { + static func upload(media: Data, credential: OAuthCredential) -> Promise<() throws -> String> { let client = OAuthSwiftClient(credential: credential) client.sessionFactory.queue = { .current } - _ = client.post( - "https://upload.twitter.com/1.1/media/upload.json", - parameters: [ - "media_data": media.base64EncodedString() - ], - callback: callback - ) { response in - let json = try! JSONSerialization.jsonObject(with: response.data, options: []) as! [String: Any] - return json["media_id_string"] as! String // `!` never fails + return Promise<() throws -> String> { fulfill in + _ = client.post( + "https://upload.twitter.com/1.1/media/upload.json", + parameters: [ + "media_data": media.base64EncodedString() + ], + callback: fulfill as! (() throws -> String) -> () + ) { response in + let json = try! JSONSerialization.jsonObject(with: response.data, options: []) as! [String: Any] + return json["media_id_string"] as! String // `!` never fails + } } } } diff --git a/Tests/TweetupKitTests/TwitterTests.swift b/Tests/TweetupKitTests/TwitterTests.swift index 8bdaa1e..737af5a 100644 --- a/Tests/TweetupKitTests/TwitterTests.swift +++ b/Tests/TweetupKitTests/TwitterTests.swift @@ -27,7 +27,7 @@ class TwitterTests: XCTestCase { do { let expectation = self.expectation(description: "") - Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", credential: credential) { getId in + Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", credential: credential).get { getId in defer { expectation.fulfill() } @@ -46,10 +46,10 @@ class TwitterTests: XCTestCase { let expectation = self.expectation(description: "") let data = try! Data(contentsOf: URL(fileURLWithPath: imagePath)) - Twitter.upload(media: data, credential: credential) { getMediaId in + Twitter.upload(media: data, credential: credential).get { getMediaId in do { let mediaId = try getMediaId() - Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", mediaId: mediaId, credential: credential) { getId in + Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", mediaId: mediaId, credential: credential).get { getId in defer { expectation.fulfill() } @@ -75,7 +75,7 @@ class TwitterTests: XCTestCase { let expectation = self.expectation(description: "") let data = try! Data(contentsOf: URL(fileURLWithPath: imagePath)) - Twitter.upload(media: data, credential: credential) { getId in + Twitter.upload(media: data, credential: credential).get { getId in defer { expectation.fulfill() } From c1253955b40aedf39aed751099ce35880eeaa038 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Fri, 3 Nov 2017 20:02:15 +0900 Subject: [PATCH 04/18] Replace callbacks in `CodeRenderer` with `Promise`s --- Sources/TweetupKit/CodeRenderer.swift | 70 +++++++++------------------ Sources/TweetupKit/Speaker.swift | 2 +- 2 files changed, 25 insertions(+), 47 deletions(-) diff --git a/Sources/TweetupKit/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift index 83a16ac..69eab32 100644 --- a/Sources/TweetupKit/CodeRenderer.swift +++ b/Sources/TweetupKit/CodeRenderer.swift @@ -1,18 +1,22 @@ import WebKit import Foundation +import PromiseK internal class CodeRenderer: NSObject { private var webView: WebView! - fileprivate var loading = true - private var getImage: (() throws -> CGImage)? = nil - private var completions: [(() throws -> CGImage) -> ()] = [] - private var zelf: CodeRenderer? // not to released during the async operation + private var fulfill: (() throws -> CGImage) -> () + var image: Promise<() throws -> CGImage> - fileprivate static let height: CGFloat = 736 + private static let height: CGFloat = 736 init(url: String) { + var _fulfill: ((() throws -> CGImage) -> ())! + image = Promise<() throws -> CGImage> { fulfill in + _fulfill = fulfill as! (() throws -> CGImage) -> () + } + fulfill = _fulfill + super.init() - zelf = self DispatchQueue.main.async { self.webView = WebView(frame: NSRect(x: 0, y: 0, width: 640, height: CodeRenderer.height)) @@ -22,44 +26,21 @@ internal class CodeRenderer: NSObject { } } - func image(completion: @escaping (() throws -> CGImage) -> ()) { - DispatchQueue.main.async { - if let getImage = self.getImage { - completion { - try getImage() - } + func writeImage(to path: String) -> Promise<() throws -> ()> { + return image.map { getImage in + let image = try getImage() + let url = URL(fileURLWithPath: path) + guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else { + throw CodeRendererError.writingFailed } - self.completions.append(completion) - } - } - - func writeImage(to path: String, completion: @escaping (() throws -> ()) -> ()) { - image { getImage in - completion { - let image = try getImage() - let url = URL(fileURLWithPath: path) - guard let destination = CGImageDestinationCreateWithURL(url as CFURL, kUTTypePNG, 1, nil) else { - throw CodeRendererError.writingFailed - } - - CGImageDestinationAddImage(destination, image, nil) - - guard CGImageDestinationFinalize(destination) else { - throw CodeRendererError.writingFailed - } + CGImageDestinationAddImage(destination, image, nil) + + guard CGImageDestinationFinalize(destination) else { + throw CodeRendererError.writingFailed } } } - - fileprivate func resolve(getImage: @escaping (() throws -> CGImage)) { - for completion in completions { - completion(getImage) - } - completions.removeAll() - self.getImage = getImage - self.zelf = nil - } } extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread @@ -71,7 +52,7 @@ extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread let files = document.getElementsByClassName("blob-file-content")! guard files.length > 0 else { - resolve(getImage: { throw CodeRendererError.illegalResponse } ) + fulfill { throw CodeRendererError.illegalResponse } return } let code = files.item(0) as! DOMElement @@ -96,7 +77,7 @@ extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread context.draw(imageRep.cgImage!, in: targetRect) let provider: CGDataProvider = CGDataProvider(data: Data(bytes: pixels) as CFData)! - resolve(getImage: { + fulfill { CGImage( width: width, height: height, @@ -110,14 +91,11 @@ extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread shouldInterpolate: false, intent: .defaultIntent )! - }) - - loading = false + } } func webView(_ sender: WebView, didFailLoadWithError error: Error, for frame: WebFrame) { - resolve(getImage: { throw error }) - loading = false + fulfill { throw error } } } diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index 98ad611..a8d8d52 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -169,7 +169,7 @@ public struct Speaker { let url = "https://gist.github.com/\(id)" let imagePath = outputDirectoryPath.appendingPathComponent("\(id).png") let codeRenderer = CodeRenderer(url: url) - codeRenderer.writeImage(to: Speaker.imagePath(imagePath, from: self.baseDirectoryPath)) { getVoid in + codeRenderer.writeImage(to: Speaker.imagePath(imagePath, from: self.baseDirectoryPath)).get { getVoid in callback { try getVoid() return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .local(imagePath)))) From 79ee12f971626127c0a46a424ff2fbc304b2a6b1 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Fri, 3 Nov 2017 21:49:53 +0900 Subject: [PATCH 05/18] Replace callbacks in `Speaker` with `Promise`s --- Sources/TweetupKit/Async.swift | 55 ++++++++++ Sources/TweetupKit/Speaker.swift | 134 ++++++++--------------- Tests/TweetupKitTests/SpeakerTests.swift | 26 ++--- 3 files changed, 116 insertions(+), 99 deletions(-) diff --git a/Sources/TweetupKit/Async.swift b/Sources/TweetupKit/Async.swift index b009b58..649a218 100644 --- a/Sources/TweetupKit/Async.swift +++ b/Sources/TweetupKit/Async.swift @@ -1,4 +1,5 @@ import Foundation +import PromiseK public func sync(operation: (@escaping (T, @escaping (() throws -> R) -> ()) -> ())) -> (T) throws -> R { return { value in @@ -27,6 +28,30 @@ public func sync(operation: (@escaping (T, @escaping (() throws -> R) -> ( } } +internal func repeated(operation: @escaping (T) -> Promise<() throws -> R>, interval: TimeInterval? = nil) -> ([T]) -> Promise<() throws -> [R]> { + return { values in + _repeat(operation: operation, for: values[...], interval: interval) + } +} + +private func _repeat(operation: @escaping (T) -> Promise<() throws -> R>, for values: ArraySlice, interval: TimeInterval?, results: [R] = []) -> Promise<() throws -> [R]> { + let (headOrNil, tail) = values.headAndTail + guard let head = headOrNil else { + return Promise { results } + } + + let waitingOperation: (T) -> Promise<() throws -> R> + if let interval = interval, values.count > 1 { + waitingOperation = waiting(operation: operation, with: interval) + } else { + waitingOperation = operation + } + + return waitingOperation(head).flatMap { result in + _repeat(operation: operation, for: tail, interval: interval, results: results + [try result()]) + } +} + internal func repeated(operation: (@escaping (T, @escaping (() throws -> R) -> ()) -> ()), interval: TimeInterval? = nil) -> ([T], @escaping (() throws -> [R]) -> ()) -> () { return { values, callback in _repeat(operation: operation, for: values[0..(_ operation1: @escaping (T, @escaping (() throws } } +internal func waiting(operation: @escaping (T) -> Promise<() throws -> R>, with interval: TimeInterval) -> (T) -> Promise<() throws -> R> { + let wait: () -> Promise<() throws -> ()> = { + Promise<() throws -> ()> { fulfill in + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(interval * 1000.0))) { + fulfill { () } + } + } + } + return { value in + join(operation, wait)(value, ()).map { getValue in + let (value, _) = try getValue() + return value + } + } +} + internal func waiting(operation: @escaping (T, @escaping (() throws -> R) -> ()) -> (), with interval: TimeInterval) -> (T, @escaping (() throws -> R) -> ()) -> () { let wait: ((), @escaping (() throws -> ()) -> ()) -> () = { _, completion in DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(interval * 1000.0))) { @@ -93,6 +134,20 @@ internal func waiting(operation: @escaping (T, @escaping (() throws -> R) } } +internal func join(_ operation1: @escaping (T1) -> Promise<() throws -> R1>, _ operation2: @escaping (T2) -> Promise<() throws -> R2>) -> (T1, T2) -> Promise<() throws -> (R1, R2)> { + return { value1, value2 in + let promise1 = operation1(value1) + let promise2 = operation2(value2) + + let results: Promise<() throws -> (R1, R2)> = promise1.flatMap { getResult1 in + promise2.map { getResult2 in + ( try getResul1(), try getResult2()) + } + } + return results + } +} + internal func join(_ operation1: @escaping (T1, @escaping (() throws -> R1) -> ()) -> (), _ operation2: @escaping (T2, @escaping (() throws -> R2) -> ()) -> ()) -> ((T1, T2), @escaping (() throws -> (R1, R2)) -> ()) -> () { return { values, completion in let (value1, value2) = values diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index a8d8d52..9a520ee 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -1,4 +1,5 @@ import Foundation +import PromiseK public struct Speaker { public let twitterCredential: OAuthCredential? @@ -15,41 +16,33 @@ public struct Speaker { self.outputDirectoryPath = outputDirectoryPath } - public func talk(title: String, tweets: [Tweet], interval: TimeInterval?, callback: @escaping (() throws -> URL) -> ()) { - post(tweets: tweets, with: interval) { getIds in - do { - let ids = try getIds() - assert(ids.count == tweets.count) - fatalError("Unimplemented.") -// for (idAndScreenName, tweet) in zip(ids, tweets) { -// let (id, screenName) = idAndScreenName -// // TODO -// fatalError("Unimplemented.") -// } - } catch let error { - callback { - throw error - } - } + public func talk(title: String, tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> URL> { + return post(tweets: tweets, with: interval).map { getIds in + let ids = try getIds() + assert(ids.count == tweets.count) + fatalError("Unimplemented.") +// for (idAndScreenName, tweet) in zip(ids, tweets) { +// let (id, screenName) = idAndScreenName +// // TODO +// fatalError("Unimplemented.") +// } } } - public func post(tweets: [Tweet], with interval: TimeInterval?, callback: @escaping (() throws -> ([(String, String)])) -> ()) { - repeated(operation: post, interval: interval)(tweets, callback) + public func post(tweets: [Tweet], with interval: TimeInterval?) -> Promise<() throws -> [(String, String)]> { + return repeated(operation: post, interval: interval)(tweets) } - public func post(tweet: Tweet, callback: @escaping (() throws -> (String, String)) -> ()) { + public func post(tweet: Tweet) -> Promise<() throws -> (String, String)> { guard let twitterCredential = twitterCredential else { - callback { - throw SpeakerError.noTwitterCredential - } - return + return Promise { throw SpeakerError.noTwitterCredential } } - let resolve = flatten(flatten(resolveCode, resolveGist), resolveImage) - resolve(tweet) { getTweet in - do { - let tweet = try getTweet() + return resolveCode(of: tweet) + .flatMap { self.resolveGist(of: try $0()) } + .flatMap { self.resolveImage(of: try $0()) } + .flatMap { (getTweet: () throws -> Tweet) in + let tweet: Tweet = try getTweet() let status = tweet.body let mediaId: String? if let attachment = tweet.attachment { @@ -70,47 +63,32 @@ public struct Speaker { } else { mediaId = nil } - Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential).get { getId in - callback { - try getId() - } - } - } catch let error { - callback { throw error } + return Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential) + }.map { getId in + try getId() } - } } - public func resolveImages(of tweets: [Tweet], callback: @escaping (() throws -> [Tweet]) -> ()) { - repeated(operation: resolveImage)(tweets, callback) + public func resolveImages(of tweets: [Tweet]) -> Promise<() throws -> [Tweet]> { + return repeated(operation: resolveImage)(tweets) } - public func resolveImage(of tweet: Tweet, callback: @escaping (() throws -> Tweet) -> ()) { + public func resolveImage(of tweet: Tweet) -> Promise<() throws -> Tweet> { guard case let .some(.image(image)) = tweet.attachment, case let .local(path) = image.source else { - callback { - tweet - } - return + return Promise { tweet } } guard let twitterCredential = twitterCredential else { - callback { - throw SpeakerError.noTwitterCredential - } - return + return Promise { throw SpeakerError.noTwitterCredential } } do { let imagePath = Speaker.imagePath(path, from: baseDirectoryPath) - Twitter.upload(media: try Data(contentsOf: URL(fileURLWithPath: imagePath)), credential: twitterCredential).get { getId in - callback { - let id = try getId() - return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .twitter(id)))) - } + return Twitter.upload(media: try Data(contentsOf: URL(fileURLWithPath: imagePath)), credential: twitterCredential).map { getId in + let id = try getId() + return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .twitter(id)))) } } catch let error { - callback { - throw error - } + return Promise { throw error } } } @@ -122,58 +100,42 @@ public struct Speaker { } } - public func resolveCodes(of tweets: [Tweet], callback: @escaping (() throws -> [Tweet]) -> ()) { - repeated(operation: resolveCode)(tweets, callback) + public func resolveCodes(of tweets: [Tweet]) -> Promise<() throws -> [Tweet]> { + return repeated(operation: resolveCode)(tweets) } - public func resolveCode(of tweet: Tweet, callback: @escaping (() throws -> Tweet) -> ()) { + public func resolveCode(of tweet: Tweet) -> Promise<() throws -> Tweet> { guard case let .some(.code(code)) = tweet.attachment else { - callback { - tweet - } - return + return Promise { tweet } } guard let githubToken = githubToken else { - callback { - throw SpeakerError.noGithubToken - } - return + return Promise { throw SpeakerError.noGithubToken } } - Gist.createGist(description: tweet.body, code: code, accessToken: githubToken).get { getId in - callback { - let id = try getId() - return try Tweet(body: "\(tweet.body)\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id)))) - } + return Gist.createGist(description: tweet.body, code: code, accessToken: githubToken).map { getId in + let id = try getId() + return try Tweet(body: "\(tweet.body)\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id)))) } } - public func resolveGists(of tweets: [Tweet], callback: @escaping (() throws -> [Tweet]) -> ()) { - repeated(operation: resolveGist)(tweets, callback) + public func resolveGists(of tweets: [Tweet]) -> Promise<() throws -> [Tweet]> { + return repeated(operation: resolveGist)(tweets) } - public func resolveGist(of tweet: Tweet, callback: @escaping (() throws -> Tweet) -> ()) { + public func resolveGist(of tweet: Tweet) -> Promise<() throws -> Tweet> { guard case let .some(.image(image)) = tweet.attachment, case let .gist(id) = image.source else { - callback { - tweet - } - return + return Promise { tweet } } guard let outputDirectoryPath = outputDirectoryPath else { - callback { - throw SpeakerError.noOutputDirectoryPath - } - return + return Promise { throw SpeakerError.noOutputDirectoryPath } } let url = "https://gist.github.com/\(id)" let imagePath = outputDirectoryPath.appendingPathComponent("\(id).png") let codeRenderer = CodeRenderer(url: url) - codeRenderer.writeImage(to: Speaker.imagePath(imagePath, from: self.baseDirectoryPath)).get { getVoid in - callback { - try getVoid() - return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .local(imagePath)))) - } + return codeRenderer.writeImage(to: Speaker.imagePath(imagePath, from: self.baseDirectoryPath)).map { getVoid in + try getVoid() + return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .local(imagePath)))) } } } diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift index 4439dbf..24479de 100644 --- a/Tests/TweetupKitTests/SpeakerTests.swift +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -34,7 +34,7 @@ class SpeakerTests: XCTestCase { let string = "Twinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n---\n\nUp above the world so high,\nLike a diamond in the sky. \(start)\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n![](\(imagePath))" // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, with: 5.0) { getIds in + speaker.post(tweets: tweets, with: 5.0).get { getIds in defer { expectation.fulfill() } @@ -66,7 +66,7 @@ class SpeakerTests: XCTestCase { let string = "Twinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n---\n\nUp above the world so high,\nLike a diamond in the sky. \(start)\n\n![](illegal/path/to/image.png)\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```" // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, with: 10.0) { getIds in + speaker.post(tweets: tweets, with: 10.0).get { getIds in defer { expectation.fulfill() } @@ -94,7 +94,7 @@ class SpeakerTests: XCTestCase { let string = "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\n---\n\nUp above the world so high,\nLike a diamond in the sky.\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](\(imagePath))\n\n---\n\nWhen the blazing sun is gone,\nWhen he nothing shines upon,\n\n![alternative text 1](\(imagePath))\n\n---\n\nThen you show your little light,\nTwinkle, twinkle, all the night.\n\n![alternative text 2](\(imagePath))\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![alternative text 3](\(imagePath))\n\n" let tweets = try! Tweet.tweets(from: string) - speaker.resolveImages(of: tweets) { getTweets in + speaker.resolveImages(of: tweets).get { getTweets in defer { expectation.fulfill() } @@ -172,7 +172,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") - speaker.resolveImage(of: tweet) { getTweet in + speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -191,7 +191,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .local(imagePath)))) - speaker.resolveImage(of: tweet) { getTweet in + speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -218,7 +218,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") - speaker.resolveImage(of: tweet) { getTweet in + speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -237,7 +237,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .local(imagePath)))) - speaker.resolveImage(of: tweet) { getTweet in + speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -261,7 +261,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .local("image.png")))) - speaker.resolveImage(of: tweet) { getTweet in + speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -292,7 +292,7 @@ class SpeakerTests: XCTestCase { let string = "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\n---\n\nUp above the world so high,\nLike a diamond in the sky.\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](path/to/image.png)\n\n---\n\nWhen the blazing sun is gone,\nWhen he nothing shines upon,\n\n```swift:hello1.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nThen you show your little light,\nTwinkle, twinkle, all the night.\n\n```swift:hello2.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n```swift:hello3.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n" let tweets = try! Tweet.tweets(from: string) - speaker.resolveCodes(of: tweets) { getTweets in + speaker.resolveCodes(of: tweets).get { getTweets in defer { expectation.fulfill() } @@ -370,7 +370,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") - speaker.resolveCode(of: tweet) { getTweet in + speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -389,7 +389,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .code(Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"))) - speaker.resolveCode(of: tweet) { getTweet in + speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -416,7 +416,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") - speaker.resolveCode(of: tweet) { getTweet in + speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() } @@ -435,7 +435,7 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .code(Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"))) - speaker.resolveCode(of: tweet) { getTweet in + speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() } From fedaaa5aae907f1f0a9af7b3b34f0702c5b0ce06 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 4 Nov 2017 00:54:53 +0900 Subject: [PATCH 06/18] Remove callback versions from Async.swift Also fixed `join`. --- Sources/TweetupKit/Async.swift | 132 +------------------------ Tests/TweetupKitTests/AsyncTests.swift | 29 +++--- 2 files changed, 21 insertions(+), 140 deletions(-) diff --git a/Sources/TweetupKit/Async.swift b/Sources/TweetupKit/Async.swift index 649a218..0ea91e5 100644 --- a/Sources/TweetupKit/Async.swift +++ b/Sources/TweetupKit/Async.swift @@ -52,54 +52,6 @@ private func _repeat(operation: @escaping (T) -> Promise<() throws -> R>, } } -internal func repeated(operation: (@escaping (T, @escaping (() throws -> R) -> ()) -> ()), interval: TimeInterval? = nil) -> ([T], @escaping (() throws -> [R]) -> ()) -> () { - return { values, callback in - _repeat(operation: operation, for: values[0..(operation: @escaping (T, @escaping (() throws -> R) -> ()) -> (), for values: ArraySlice, interval: TimeInterval?, results: [R] = [], callback: @escaping (() throws -> [R]) -> ()) { - let (headOrNil, tail) = values.headAndTail - guard let head = headOrNil else { - callback { results } - return - } - - let waitingOperation: (T, @escaping (() throws -> R) -> ()) -> () - if let interval = interval, values.count > 1 { - waitingOperation = waiting(operation: operation, with: interval) - } else { - waitingOperation = operation - } - - waitingOperation(head) { result in - do { - _repeat(operation: operation, for: tail, interval: interval, results: results + [try result()], callback: callback) - } catch let error { - callback { throw error } - } - } -} - -internal func flatten(_ operation1: @escaping (T, @escaping (() throws -> U) -> ()) -> (), _ operation2: @escaping (U, @escaping (() throws -> R) -> ()) -> ()) -> (T, @escaping (() throws -> R) -> ()) -> () { - return { value, callback in - operation1(value) { getValue in - do { - let value = try getValue() - operation2(value) { getValue in - callback { - try getValue() - } - } - } catch let error { - callback { - throw error - } - } - } - } -} - internal func waiting(operation: @escaping (T) -> Promise<() throws -> R>, with interval: TimeInterval) -> (T) -> Promise<() throws -> R> { let wait: () -> Promise<() throws -> ()> = { Promise<() throws -> ()> { fulfill in @@ -116,93 +68,17 @@ internal func waiting(operation: @escaping (T) -> Promise<() throws -> R>, } } -internal func waiting(operation: @escaping (T, @escaping (() throws -> R) -> ()) -> (), with interval: TimeInterval) -> (T, @escaping (() throws -> R) -> ()) -> () { - let wait: ((), @escaping (() throws -> ()) -> ()) -> () = { _, completion in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(interval * 1000.0))) { - completion { - () - } - } - } - return { value, completion in - join(operation, wait)((value, ())) { getValue in - completion { - let (value, _) = try getValue() - return value - } - } - } -} - internal func join(_ operation1: @escaping (T1) -> Promise<() throws -> R1>, _ operation2: @escaping (T2) -> Promise<() throws -> R2>) -> (T1, T2) -> Promise<() throws -> (R1, R2)> { return { value1, value2 in let promise1 = operation1(value1) let promise2 = operation2(value2) - let results: Promise<() throws -> (R1, R2)> = promise1.flatMap { getResult1 in - promise2.map { getResult2 in - ( try getResul1(), try getResult2()) + let results: Promise<() throws -> (R1, R2)> = promise1.flatMap { getResult1 throws -> Promise<() throws -> (R1, R2)> in + let result1 = try getResult1() + return promise2.map { getResult2 in + ( result1, try getResult2()) } } return results } } - -internal func join(_ operation1: @escaping (T1, @escaping (() throws -> R1) -> ()) -> (), _ operation2: @escaping (T2, @escaping (() throws -> R2) -> ()) -> ()) -> ((T1, T2), @escaping (() throws -> (R1, R2)) -> ()) -> () { - return { values, completion in - let (value1, value2) = values - var result1: R1? - var result2: R2? - var hasThrownError = false - - operation1(value1) { getValue in - do { - let result = try getValue() - DispatchQueue.main.async { - guard let result2 = result2 else { - result1 = result - return - } - completion { - (result, result2) - } - } - } catch let error { - DispatchQueue.main.async { - if hasThrownError { - return - } - hasThrownError = true - completion { - throw error - } - } - } - } - - operation2(value2) { getValue in - do { - let result = try getValue() - DispatchQueue.main.async { - guard let result1 = result1 else { - result2 = result - return - } - completion { - (result1, result) - } - } - } catch let error { - DispatchQueue.main.async { - if hasThrownError { - return - } - hasThrownError = true - completion { - throw error - } - } - } - } - } -} diff --git a/Tests/TweetupKitTests/AsyncTests.swift b/Tests/TweetupKitTests/AsyncTests.swift index 9859486..6654424 100644 --- a/Tests/TweetupKitTests/AsyncTests.swift +++ b/Tests/TweetupKitTests/AsyncTests.swift @@ -2,6 +2,7 @@ import XCTest @testable import TweetupKit import Foundation +import PromiseK class AsyncTests: XCTestCase { func testRepeated() { @@ -10,7 +11,7 @@ class AsyncTests: XCTestCase { let start = Date.timeIntervalSinceReferenceDate - repeated(operation: asyncIncrement, interval: 2.0)(Array(1...5)) { getValues in + repeated(operation: asyncIncrement, interval: 2.0)(Array(1...5)).get { getValues in defer { expectation.fulfill() } @@ -35,7 +36,7 @@ class AsyncTests: XCTestCase { let start = Date.timeIntervalSinceReferenceDate - repeated(operation: asyncTime, interval: 2.0)([(), (), (), (), ()]) { getValues in + repeated(operation: asyncTime, interval: 2.0)([(), (), (), (), ()]).get { getValues in defer { expectation.fulfill() } @@ -44,10 +45,15 @@ class AsyncTests: XCTestCase { let values = try getValues() XCTAssertEqual(values.count, 5) let allowableError: TimeInterval = 0.1 + print(values[0] - start) XCTAssertLessThan(abs(values[0] - start), allowableError) + print(values[1] - (values[0] + 2.0)) XCTAssertLessThan(abs(values[1] - (values[0] + 2.0)), allowableError) + print(values[2] - (values[1] + 2.0)) XCTAssertLessThan(abs(values[2] - (values[1] + 2.0)), allowableError) + print(values[3] - (values[2] + 2.0)) XCTAssertLessThan(abs(values[3] - (values[2] + 2.0)), allowableError) + print(values[4] - (values[3] + 2.0)) XCTAssertLessThan(abs(values[4] - (values[3] + 2.0)), allowableError) } catch let error { XCTFail("\(error)") @@ -67,7 +73,7 @@ class AsyncTests: XCTestCase { let start = Date.timeIntervalSinceReferenceDate - waiting(operation: asyncIncrement, with: 2.0)(42) { getValue in + waiting(operation: asyncIncrement, with: 2.0)(42).get { getValue in defer { expectation.fulfill() } @@ -88,19 +94,18 @@ class AsyncTests: XCTestCase { } } -private func asyncIncrement(value: Int, completion: @escaping (() throws -> Int) -> ()) { - DispatchQueue.main.async { - completion { - value + 1 +private func asyncIncrement(value: Int) -> Promise<() throws -> Int> { + return Promise { fulfill in + DispatchQueue.main.async { + fulfill { value + 1 } } } } - -private func asyncTime(_ value: (), completion: @escaping (() throws -> TimeInterval) -> ()) { - DispatchQueue.main.async { - completion { - Date.timeIntervalSinceReferenceDate +private func asyncTime(_ value: ()) -> Promise<() throws -> TimeInterval> { + return Promise { fulfill in + DispatchQueue.main.async { + fulfill { Date.timeIntervalSinceReferenceDate } } } } From ae9e341f56a4ceadddbf3eabd74463b62d9c121a Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 4 Nov 2017 01:05:54 +0900 Subject: [PATCH 07/18] Simplify Async.swift Replaced `waiting` and `join` based on `(T) -> Promise<() throws -> R>` with `wait` based on `Promise<() throws -> T>`. --- Sources/TweetupKit/Async.swift | 45 ++++++++------------------ Tests/TweetupKitTests/AsyncTests.swift | 5 +-- 2 files changed, 16 insertions(+), 34 deletions(-) diff --git a/Sources/TweetupKit/Async.swift b/Sources/TweetupKit/Async.swift index 0ea91e5..70fc351 100644 --- a/Sources/TweetupKit/Async.swift +++ b/Sources/TweetupKit/Async.swift @@ -40,45 +40,26 @@ private func _repeat(operation: @escaping (T) -> Promise<() throws -> R>, return Promise { results } } - let waitingOperation: (T) -> Promise<() throws -> R> - if let interval = interval, values.count > 1 { - waitingOperation = waiting(operation: operation, with: interval) + let resultPromise: Promise<() throws -> R> + if let interval = interval, !tail.isEmpty { + resultPromise = wait(operation(head), for: interval) } else { - waitingOperation = operation + resultPromise = operation(head) } - return waitingOperation(head).flatMap { result in - _repeat(operation: operation, for: tail, interval: interval, results: results + [try result()]) + return resultPromise.flatMap { getResult in + _repeat(operation: operation, for: tail, interval: interval, results: results + [try getResult()]) } } -internal func waiting(operation: @escaping (T) -> Promise<() throws -> R>, with interval: TimeInterval) -> (T) -> Promise<() throws -> R> { - let wait: () -> Promise<() throws -> ()> = { - Promise<() throws -> ()> { fulfill in - DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(interval * 1000.0))) { - fulfill { () } - } - } - } - return { value in - join(operation, wait)(value, ()).map { getValue in - let (value, _) = try getValue() - return value +internal func wait(_ promise: Promise<() throws -> T>, for interval: TimeInterval) -> Promise<() throws -> T> { + let waiting = Promise<()> { fulfill in + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(Int(interval * 1000.0))) { + fulfill(()) } } -} - -internal func join(_ operation1: @escaping (T1) -> Promise<() throws -> R1>, _ operation2: @escaping (T2) -> Promise<() throws -> R2>) -> (T1, T2) -> Promise<() throws -> (R1, R2)> { - return { value1, value2 in - let promise1 = operation1(value1) - let promise2 = operation2(value2) - - let results: Promise<() throws -> (R1, R2)> = promise1.flatMap { getResult1 throws -> Promise<() throws -> (R1, R2)> in - let result1 = try getResult1() - return promise2.map { getResult2 in - ( result1, try getResult2()) - } - } - return results + return promise.flatMap { getValue in + let value = try getValue() + return waiting.map { _ in value } } } diff --git a/Tests/TweetupKitTests/AsyncTests.swift b/Tests/TweetupKitTests/AsyncTests.swift index 6654424..ed3403b 100644 --- a/Tests/TweetupKitTests/AsyncTests.swift +++ b/Tests/TweetupKitTests/AsyncTests.swift @@ -68,12 +68,13 @@ class AsyncTests: XCTestCase { } } - func testWaiting() { + func testWait() { let expectation = self.expectation(description: "") let start = Date.timeIntervalSinceReferenceDate - waiting(operation: asyncIncrement, with: 2.0)(42).get { getValue in + let promise = TweetupKit.wait(asyncIncrement(value: 42), for: 2.0) + promise.get { getValue in defer { expectation.fulfill() } From 58761d1ff880db230ecaa04ee7eb1db89671a46f Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Wed, 27 Dec 2017 00:54:23 +0900 Subject: [PATCH 08/18] Fix wrong casts --- Sources/TweetupKit/CodeRenderer.swift | 8 ++++---- Sources/TweetupKit/Twitter.swift | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Sources/TweetupKit/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift index 69eab32..11ec645 100644 --- a/Sources/TweetupKit/CodeRenderer.swift +++ b/Sources/TweetupKit/CodeRenderer.swift @@ -4,15 +4,15 @@ import PromiseK internal class CodeRenderer: NSObject { private var webView: WebView! - private var fulfill: (() throws -> CGImage) -> () + private var fulfill: (@escaping () throws -> CGImage) -> () var image: Promise<() throws -> CGImage> private static let height: CGFloat = 736 init(url: String) { - var _fulfill: ((() throws -> CGImage) -> ())! - image = Promise<() throws -> CGImage> { fulfill in - _fulfill = fulfill as! (() throws -> CGImage) -> () + var _fulfill: ((@escaping() throws -> CGImage) -> ())! + image = Promise<() throws -> CGImage> { (fulfill: @escaping (@escaping () throws -> CGImage) -> ()) in + _fulfill = fulfill } fulfill = _fulfill diff --git a/Sources/TweetupKit/Twitter.swift b/Sources/TweetupKit/Twitter.swift index 086985f..f078f86 100644 --- a/Sources/TweetupKit/Twitter.swift +++ b/Sources/TweetupKit/Twitter.swift @@ -14,11 +14,11 @@ internal struct Twitter { parameters["media_ids"] = mediaId } - return Promise<() throws -> (String, String)> { fulfill in + return Promise<() throws -> (String, String)> { (fulfill: @escaping (@escaping () throws -> (String, String)) -> ()) in _ = client.post( "https://api.twitter.com/1.1/statuses/update.json", parameters: parameters, - callback: fulfill as! (() throws -> (String, String)) -> () // `as!`f as a workaround + callback: { value in fulfill(value) } ) { response in let json = try! JSONSerialization.jsonObject(with: response.data) as! [String: Any] // `!` never fails return (json["id_str"] as! String, (json["user"] as! [String: Any])["screen_name"] as! String) // `!` never fails @@ -30,13 +30,13 @@ internal struct Twitter { let client = OAuthSwiftClient(credential: credential) client.sessionFactory.queue = { .current } - return Promise<() throws -> String> { fulfill in + return Promise<() throws -> String> { (fulfill: @escaping (@escaping () throws -> String) -> ()) in _ = client.post( "https://upload.twitter.com/1.1/media/upload.json", parameters: [ "media_data": media.base64EncodedString() ], - callback: fulfill as! (() throws -> String) -> () + callback: fulfill ) { response in let json = try! JSONSerialization.jsonObject(with: response.data, options: []) as! [String: Any] return json["media_id_string"] as! String // `!` never fails @@ -56,7 +56,7 @@ extension OAuthSwiftClient { ) } - fileprivate func post(_ url: String, parameters: OAuthSwift.Parameters, callback: @escaping (() throws -> T) -> (), completion: @escaping (OAuthSwiftResponse) throws -> (T)) -> OAuthSwiftRequestHandle? { + fileprivate func post(_ url: String, parameters: OAuthSwift.Parameters, callback: @escaping (@escaping () throws -> T) -> (), completion: @escaping (OAuthSwiftResponse) throws -> (T)) -> OAuthSwiftRequestHandle? { return post( url, parameters: parameters, From 066a5a32607758ace59328e38acce7095a03b8a1 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Wed, 27 Dec 2017 00:54:59 +0900 Subject: [PATCH 09/18] Improve code to make a `CGImage` --- Sources/TweetupKit/CodeRenderer.swift | 41 ++++++++++++++------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Sources/TweetupKit/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift index 11ec645..cce4433 100644 --- a/Sources/TweetupKit/CodeRenderer.swift +++ b/Sources/TweetupKit/CodeRenderer.swift @@ -69,29 +69,30 @@ extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread let width = Int(codeBox2.size.width) let height = Int(codeBox2.size.height) - var pixels = [UInt8](repeating: 0, count: width * height * 4) let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) - let context = CGContext(data: &pixels, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)! - let targetRect = CGRect(x: -codeBox2.origin.x, y: codeBox2.origin.y - CGFloat(pageBox2.size.height - codeBox2.size.height), width: pageBox2.size.width, height: pageBox2.size.height) - context.draw(imageRep.cgImage!, in: targetRect) - - let provider: CGDataProvider = CGDataProvider(data: Data(bytes: pixels) as CFData)! - fulfill { - CGImage( - width: width, - height: height, - bitsPerComponent: 8, - bitsPerPixel: 32, - bytesPerRow: width * 4, - space: colorSpace, - bitmapInfo: bitmapInfo, - provider: provider, - decode: nil, - shouldInterpolate: false, - intent: .defaultIntent - )! + var data = Data(count: width * height * 4) + data.withUnsafeMutableBytes { (bytes: UnsafeMutablePointer) -> Void in + let context = CGContext(data: bytes, width: width, height: height, bitsPerComponent: 8, bytesPerRow: width * 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)! + let targetRect = CGRect(x: -codeBox2.origin.x, y: codeBox2.origin.y - CGFloat(pageBox2.size.height - codeBox2.size.height), width: pageBox2.size.width, height: pageBox2.size.height) + context.draw(imageRep.cgImage!, in: targetRect) } + + let provider: CGDataProvider = CGDataProvider(data: data as CFData)! + let image = CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: bitmapInfo, + provider: provider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent + )! + fulfill { image } } func webView(_ sender: WebView, didFailLoadWithError error: Error, for frame: WebFrame) { From d8bb7f9abd4e101677ccc3c59ae4784f83e2e1a8 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Thu, 28 Dec 2017 00:56:34 +0900 Subject: [PATCH 10/18] Add a missing space --- Sources/TweetupKit/CodeRenderer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TweetupKit/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift index cce4433..1361f80 100644 --- a/Sources/TweetupKit/CodeRenderer.swift +++ b/Sources/TweetupKit/CodeRenderer.swift @@ -10,7 +10,7 @@ internal class CodeRenderer: NSObject { private static let height: CGFloat = 736 init(url: String) { - var _fulfill: ((@escaping() throws -> CGImage) -> ())! + var _fulfill: ((@escaping () throws -> CGImage) -> ())! image = Promise<() throws -> CGImage> { (fulfill: @escaping (@escaping () throws -> CGImage) -> ()) in _fulfill = fulfill } From 8e48075616795c7ebde3ffe71b7334eb2993e48c Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Thu, 28 Dec 2017 01:06:16 +0900 Subject: [PATCH 11/18] Make `CodeRenderer.image { set }` `private` --- Sources/TweetupKit/CodeRenderer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TweetupKit/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift index 1361f80..66e9a0b 100644 --- a/Sources/TweetupKit/CodeRenderer.swift +++ b/Sources/TweetupKit/CodeRenderer.swift @@ -5,7 +5,7 @@ import PromiseK internal class CodeRenderer: NSObject { private var webView: WebView! private var fulfill: (@escaping () throws -> CGImage) -> () - var image: Promise<() throws -> CGImage> + private(set) var image: Promise<() throws -> CGImage> private static let height: CGFloat = 736 From 76d0bc093631844fe03c64fc3379212d418c54fa Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Thu, 28 Dec 2017 01:07:05 +0900 Subject: [PATCH 12/18] Fix unexpected deallocation of `CoreRenderer` --- Sources/TweetupKit/CodeRenderer.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/TweetupKit/CodeRenderer.swift b/Sources/TweetupKit/CodeRenderer.swift index 66e9a0b..4ccdf00 100644 --- a/Sources/TweetupKit/CodeRenderer.swift +++ b/Sources/TweetupKit/CodeRenderer.swift @@ -3,6 +3,7 @@ import Foundation import PromiseK internal class CodeRenderer: NSObject { + private var zelf: CodeRenderer? private var webView: WebView! private var fulfill: (@escaping () throws -> CGImage) -> () private(set) var image: Promise<() throws -> CGImage> @@ -18,6 +19,8 @@ internal class CodeRenderer: NSObject { super.init() + zelf = self + DispatchQueue.main.async { self.webView = WebView(frame: NSRect(x: 0, y: 0, width: 640, height: CodeRenderer.height)) self.webView.frameLoadDelegate = self @@ -53,6 +56,7 @@ extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread let files = document.getElementsByClassName("blob-file-content")! guard files.length > 0 else { fulfill { throw CodeRendererError.illegalResponse } + zelf = nil return } let code = files.item(0) as! DOMElement @@ -93,10 +97,12 @@ extension CodeRenderer: WebFrameLoadDelegate { // called on the main thread intent: .defaultIntent )! fulfill { image } + zelf = nil } func webView(_ sender: WebView, didFailLoadWithError error: Error, for frame: WebFrame) { fulfill { throw error } + zelf = nil } } From e2afe928fb0bec74c27b3428c8cce7c0287811cd Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Thu, 28 Dec 2017 01:36:34 +0900 Subject: [PATCH 13/18] Update `SpeakerTests` by multiline string literals --- Tests/TweetupKitTests/SpeakerTests.swift | 259 ++++++++++++++++++++--- 1 file changed, 235 insertions(+), 24 deletions(-) diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift index 24479de..dea64fc 100644 --- a/Tests/TweetupKitTests/SpeakerTests.swift +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -32,7 +32,27 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") - let string = "Twinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n---\n\nUp above the world so high,\nLike a diamond in the sky. \(start)\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n![](\(imagePath))" // includes `start` to avoid duplicate tweets + let string = """ + Twinkle, twinkle, little star, + How I wonder what you are! \(start) + + --- + + Up above the world so high, + Like a diamond in the sky. \(start) + + ```swift:hello.swift + let name = "Swift" + print("Hello \\(name)!") + ``` + + --- + + Twinkle, twinkle, little star, + How I wonder what you are! \(start) + + ![](\(imagePath)) + """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) speaker.post(tweets: tweets, with: 5.0).get { getIds in defer { @@ -64,7 +84,27 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") - let string = "Twinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n---\n\nUp above the world so high,\nLike a diamond in the sky. \(start)\n\n![](illegal/path/to/image.png)\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are! \(start)\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```" // includes `start` to avoid duplicate tweets + let string = """ + Twinkle, twinkle, little star, + How I wonder what you are! \(start) + + --- + + Up above the world so high, + Like a diamond in the sky. \(start) + + ![](illegal/path/to/image.png) + + --- + + Twinkle, twinkle, little star, + How I wonder what you are! \(start) + + ```swift:hello.swift + let name = "Swift" + print("Hello \\(name)!") + ``` + """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) speaker.post(tweets: tweets, with: 10.0).get { getIds in defer { @@ -92,7 +132,50 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") - let string = "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\n---\n\nUp above the world so high,\nLike a diamond in the sky.\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](\(imagePath))\n\n---\n\nWhen the blazing sun is gone,\nWhen he nothing shines upon,\n\n![alternative text 1](\(imagePath))\n\n---\n\nThen you show your little light,\nTwinkle, twinkle, all the night.\n\n![alternative text 2](\(imagePath))\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![alternative text 3](\(imagePath))\n\n" + let string = """ + Twinkle, twinkle, little star, + How I wonder what you are! + + --- + + Up above the world so high, + Like a diamond in the sky. + + ```swift:hello.swift + let name = "Swift" + print("Hello \\(name)!") + ``` + + --- + + Twinkle, twinkle, little star, + How I wonder what you are! + + ![](\(imagePath)) + + --- + + When the blazing sun is gone, + When he nothing shines upon, + + ![alternative text 1](\(imagePath)) + + --- + + Then you show your little light, + Twinkle, twinkle, all the night. + + ![alternative text 2](\(imagePath)) + + --- + + Twinkle, twinkle, little star, + How I wonder what you are! + + ![alternative text 3](\(imagePath)) + + + """ let tweets = try! Tweet.tweets(from: string) speaker.resolveImages(of: tweets).get { getTweets in defer { @@ -120,7 +203,10 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!", attachment: .image(Image(alternativeText: "", source: .twitter(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + """, attachment: .image(Image(alternativeText: "", source: .twitter(id))))) } do { @@ -130,7 +216,10 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "When the blazing sun is gone,\nWhen he nothing shines upon,", attachment: .image(Image(alternativeText: "alternative text 1", source: .twitter(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + When the blazing sun is gone, + When he nothing shines upon, + """, attachment: .image(Image(alternativeText: "alternative text 1", source: .twitter(id))))) } do { @@ -140,7 +229,10 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Then you show your little light,\nTwinkle, twinkle, all the night.", attachment: .image(Image(alternativeText: "alternative text 2", source: .twitter(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Then you show your little light, + Twinkle, twinkle, all the night. + """, attachment: .image(Image(alternativeText: "alternative text 2", source: .twitter(id))))) } do { @@ -150,7 +242,10 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!", attachment: .image(Image(alternativeText: "alternative text 3", source: .twitter(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + """, attachment: .image(Image(alternativeText: "alternative text 3", source: .twitter(id))))) } } catch let error { @@ -171,7 +266,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") + let tweet = try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + """) speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -190,7 +288,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .local(imagePath)))) + let tweet = try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .image(Image(alternativeText: "alternative text", source: .local(imagePath)))) speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -201,7 +302,10 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .twitter(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .image(Image(alternativeText: "alternative text", source: .twitter(id))))) } catch let error { XCTFail("\(error)") } @@ -217,7 +321,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") + let tweet = try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + """) speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -236,7 +343,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .local(imagePath)))) + let tweet = try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .image(Image(alternativeText: "alternative text", source: .local(imagePath)))) speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -260,7 +370,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .local("image.png")))) + let tweet = try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .image(Image(alternativeText: "alternative text", source: .local("image.png")))) speaker.resolveImage(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -271,7 +384,10 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .image(Image(alternativeText: "alternative text", source: .twitter(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .image(Image(alternativeText: "alternative text", source: .twitter(id))))) } catch let error { XCTFail("\(error)") } @@ -290,7 +406,59 @@ class SpeakerTests: XCTestCase { let expectation = self.expectation(description: "") - let string = "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\n---\n\nUp above the world so high,\nLike a diamond in the sky.\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](path/to/image.png)\n\n---\n\nWhen the blazing sun is gone,\nWhen he nothing shines upon,\n\n```swift:hello1.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nThen you show your little light,\nTwinkle, twinkle, all the night.\n\n```swift:hello2.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n```swift:hello3.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n\n" + let string = """ + Twinkle, twinkle, little star, + How I wonder what you are! + + --- + + Up above the world so high, + Like a diamond in the sky. + + ```swift:hello.swift + let name = "Swift" + print("Hello \\(name)!") + ``` + + --- + + Twinkle, twinkle, little star, + How I wonder what you are! + + ![](path/to/image.png) + + --- + + When the blazing sun is gone, + When he nothing shines upon, + + ```swift:hello1.swift + let name = "Swift" + print(\"Hello \\(name)!\") + ``` + + --- + + Then you show your little light, + Twinkle, twinkle, all the night. + + ```swift:hello2.swift + let name = "Swift" + print("Hello \\(name)!") + ``` + + --- + + Twinkle, twinkle, little star, + How I wonder what you are! + + ```swift:hello3.swift + let name = "Swift" + print("Hello \\(name)!") + ``` + + + """ let tweets = try! Tweet.tweets(from: string) speaker.resolveCodes(of: tweets).get { getTweets in defer { @@ -313,7 +481,12 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + + https://gist.github.com/\(id) + """, attachment: .image(Image(alternativeText: "", source: .gist(id))))) } do { @@ -328,7 +501,12 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "When the blazing sun is gone,\nWhen he nothing shines upon,\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + When the blazing sun is gone, + When he nothing shines upon, + + https://gist.github.com/\(id) + """, attachment: .image(Image(alternativeText: "", source: .gist(id))))) } do { @@ -338,7 +516,12 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Then you show your little light,\nTwinkle, twinkle, all the night.\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Then you show your little light, + Twinkle, twinkle, all the night. + + https://gist.github.com/\(id) + """, attachment: .image(Image(alternativeText: "", source: .gist(id))))) } do { @@ -348,7 +531,12 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + + https://gist.github.com/\(id) + """, attachment: .image(Image(alternativeText: "", source: .gist(id))))) } } catch let error { @@ -369,7 +557,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") + let tweet = try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + """) speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -388,7 +579,13 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .code(Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"))) + let tweet = try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .code(Code(language: .swift, fileName: "hello.swift", body: """ + let name = "Swift" + print("Hello \\(name)!") + """))) speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -399,7 +596,12 @@ class SpeakerTests: XCTestCase { XCTFail() return } - XCTAssertEqual(result, try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.\n\nhttps://gist.github.com/\(id)", attachment: .image(Image(alternativeText: "", source: .gist(id))))) + XCTAssertEqual(result, try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + + https://gist.github.com/\(id) + """, attachment: .image(Image(alternativeText: "", source: .gist(id))))) } catch let error { XCTFail("\(error)") } @@ -415,7 +617,10 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") + let tweet = try! Tweet(body: """ + Twinkle, twinkle, little star, + How I wonder what you are! + """) speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() @@ -434,7 +639,13 @@ class SpeakerTests: XCTestCase { do { let expectation = self.expectation(description: "") - let tweet = try! Tweet(body: "Up above the world so high,\nLike a diamond in the sky.", attachment: .code(Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"))) + let tweet = try! Tweet(body: """ + Up above the world so high, + Like a diamond in the sky. + """, attachment: .code(Code(language: .swift, fileName: "hello.swift", body: """ + let name = "Swift" + print("Hello \\(name)!") + """))) speaker.resolveCode(of: tweet).get { getTweet in defer { expectation.fulfill() From 82192b10372c75921db76ccf33e7aa3b9f7e0119 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 30 Dec 2017 00:31:43 +0900 Subject: [PATCH 14/18] Improve the interval in `testPostTweets` --- Tests/TweetupKitTests/SpeakerTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift index dea64fc..aebe60a 100644 --- a/Tests/TweetupKitTests/SpeakerTests.swift +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -54,7 +54,7 @@ class SpeakerTests: XCTestCase { ![](\(imagePath)) """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, with: 5.0).get { getIds in + speaker.post(tweets: tweets, with: 10.0).get { getIds in defer { expectation.fulfill() } @@ -70,11 +70,11 @@ class SpeakerTests: XCTestCase { } } - waitForExpectations(timeout: 14.0, handler: nil) + waitForExpectations(timeout: 29.0, handler: nil) let end = Date.timeIntervalSinceReferenceDate - XCTAssertGreaterThan(end - start, 10.0) + XCTAssertGreaterThan(end - start, 20.0) } do { // error duraing posting tweets From 09fd9bdfba8fc8ab4e739baea4ccc6aad67fa178 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 30 Dec 2017 00:40:05 +0900 Subject: [PATCH 15/18] Rename `post(tweets:with:)` to `post(tweets:interval:)` To clarify what arguments mean. --- Sources/TweetupKit/Speaker.swift | 4 ++-- Tests/TweetupKitTests/SpeakerTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index 9a520ee..5c0d617 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -17,7 +17,7 @@ public struct Speaker { } public func talk(title: String, tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> URL> { - return post(tweets: tweets, with: interval).map { getIds in + return post(tweets: tweets, interval: interval).map { getIds in let ids = try getIds() assert(ids.count == tweets.count) fatalError("Unimplemented.") @@ -29,7 +29,7 @@ public struct Speaker { } } - public func post(tweets: [Tweet], with interval: TimeInterval?) -> Promise<() throws -> [(String, String)]> { + public func post(tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> [(String, String)]> { return repeated(operation: post, interval: interval)(tweets) } diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift index aebe60a..4dc209e 100644 --- a/Tests/TweetupKitTests/SpeakerTests.swift +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -54,7 +54,7 @@ class SpeakerTests: XCTestCase { ![](\(imagePath)) """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, with: 10.0).get { getIds in + speaker.post(tweets: tweets, interval: 10.0).get { getIds in defer { expectation.fulfill() } @@ -106,7 +106,7 @@ class SpeakerTests: XCTestCase { ``` """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, with: 10.0).get { getIds in + speaker.post(tweets: tweets, interval: 10.0).get { getIds in defer { expectation.fulfill() } From c179928aebb0d54e791fd2d9b75401fdb2d4d221 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 30 Dec 2017 00:52:24 +0900 Subject: [PATCH 16/18] Add `inReplyToStatusId` to `Twitter.update(...)` --- Sources/TweetupKit/Twitter.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/TweetupKit/Twitter.swift b/Sources/TweetupKit/Twitter.swift index f078f86..567defe 100644 --- a/Sources/TweetupKit/Twitter.swift +++ b/Sources/TweetupKit/Twitter.swift @@ -3,7 +3,7 @@ import Foundation import PromiseK internal struct Twitter { - static func update(status: String, mediaId: String? = nil, credential: OAuthCredential) -> Promise<() throws -> (String, String)> { + static func update(status: String, mediaId: String? = nil, inReplyToStatusId: String? = nil, credential: OAuthCredential) -> Promise<() throws -> (String, String)> { let client = OAuthSwiftClient(credential: credential) client.sessionFactory.queue = { .current } @@ -13,6 +13,9 @@ internal struct Twitter { if let mediaId = mediaId { parameters["media_ids"] = mediaId } + if let inReplyToStatusId = inReplyToStatusId { + parameters["in_reply_to_status_id"] = inReplyToStatusId + } return Promise<() throws -> (String, String)> { (fulfill: @escaping (@escaping () throws -> (String, String)) -> ()) in _ = client.post( From 607f4c1949937a693087802d70e4bba0c3b640a3 Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 30 Dec 2017 01:16:37 +0900 Subject: [PATCH 17/18] Introduce `Speaker.PostResponse` and `Twitter.UpdateResponse` --- Sources/TweetupKit/Speaker.swift | 33 +++++++++++++++++------- Sources/TweetupKit/Twitter.swift | 14 +++++++--- Tests/TweetupKitTests/SpeakerTests.swift | 16 ++++++------ Tests/TweetupKitTests/TwitterTests.swift | 12 ++++----- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index 5c0d617..110817b 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -2,6 +2,20 @@ import Foundation import PromiseK public struct Speaker { + public struct PostResponse { + public let statusId: String + public let screenName: String + + public init(statusId: String, screenName: String) { + self.statusId = statusId + self.screenName = screenName + } + + internal init(_ tweetResponse: Twitter.UpdateResponse) { + self.init(statusId: tweetResponse.statusId, screenName: tweetResponse.screenName) + } + } + public let twitterCredential: OAuthCredential? public let githubToken: String? public let qiitaToken: String? @@ -17,23 +31,22 @@ public struct Speaker { } public func talk(title: String, tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> URL> { - return post(tweets: tweets, interval: interval).map { getIds in - let ids = try getIds() - assert(ids.count == tweets.count) + return post(tweets: tweets, interval: interval).map { getResponses in + let responses = try getResponses() + assert(responses.count == tweets.count) fatalError("Unimplemented.") -// for (idAndScreenName, tweet) in zip(ids, tweets) { -// let (id, screenName) = idAndScreenName +// for (response, tweet) in zip(responses, tweets) { // // TODO // fatalError("Unimplemented.") // } } } - public func post(tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> [(String, String)]> { + public func post(tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> [PostResponse]> { return repeated(operation: post, interval: interval)(tweets) } - public func post(tweet: Tweet) -> Promise<() throws -> (String, String)> { + public func post(tweet: Tweet) -> Promise<() throws -> PostResponse> { guard let twitterCredential = twitterCredential else { return Promise { throw SpeakerError.noTwitterCredential } } @@ -63,9 +76,9 @@ public struct Speaker { } else { mediaId = nil } - return Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential) - }.map { getId in - try getId() + return Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential).map { PostResponse(try $0()) } + }.map { getResponse in + try getResponse() } } diff --git a/Sources/TweetupKit/Twitter.swift b/Sources/TweetupKit/Twitter.swift index 567defe..496d865 100644 --- a/Sources/TweetupKit/Twitter.swift +++ b/Sources/TweetupKit/Twitter.swift @@ -3,7 +3,12 @@ import Foundation import PromiseK internal struct Twitter { - static func update(status: String, mediaId: String? = nil, inReplyToStatusId: String? = nil, credential: OAuthCredential) -> Promise<() throws -> (String, String)> { + struct UpdateResponse { + let statusId: String + let screenName: String + } + + static func update(status: String, mediaId: String? = nil, inReplyToStatusId: String? = nil, credential: OAuthCredential) -> Promise<() throws -> UpdateResponse> { let client = OAuthSwiftClient(credential: credential) client.sessionFactory.queue = { .current } @@ -17,14 +22,17 @@ internal struct Twitter { parameters["in_reply_to_status_id"] = inReplyToStatusId } - return Promise<() throws -> (String, String)> { (fulfill: @escaping (@escaping () throws -> (String, String)) -> ()) in + return Promise<() throws -> UpdateResponse> { (fulfill: @escaping (@escaping () throws -> UpdateResponse) -> ()) in _ = client.post( "https://api.twitter.com/1.1/statuses/update.json", parameters: parameters, callback: { value in fulfill(value) } ) { response in let json = try! JSONSerialization.jsonObject(with: response.data) as! [String: Any] // `!` never fails - return (json["id_str"] as! String, (json["user"] as! [String: Any])["screen_name"] as! String) // `!` never fails + return UpdateResponse( + statusId: json["id_str"] as! String, + screenName: (json["user"] as! [String: Any])["screen_name"] as! String + ) // `!` never fails } } } diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift index 4dc209e..6886139 100644 --- a/Tests/TweetupKitTests/SpeakerTests.swift +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -54,17 +54,17 @@ class SpeakerTests: XCTestCase { ![](\(imagePath)) """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, interval: 10.0).get { getIds in + speaker.post(tweets: tweets, interval: 10.0).get { getResponses in defer { expectation.fulfill() } do { - let ids = try getIds() - XCTAssertEqual(ids.count, 3) + let responses = try getResponses() + XCTAssertEqual(responses.count, 3) let idPattern = try! NSRegularExpression(pattern: "^[0-9]+$") - XCTAssertTrue(idPattern.matches(in: ids[0].0).count == 1) - XCTAssertTrue(idPattern.matches(in: ids[1].0).count == 1) - XCTAssertTrue(idPattern.matches(in: ids[2].0).count == 1) + XCTAssertTrue(idPattern.matches(in: responses[0].statusId).count == 1) + XCTAssertTrue(idPattern.matches(in: responses[1].statusId).count == 1) + XCTAssertTrue(idPattern.matches(in: responses[2].statusId).count == 1) } catch let error { XCTFail("\(error)") } @@ -106,12 +106,12 @@ class SpeakerTests: XCTestCase { ``` """ // includes `start` to avoid duplicate tweets let tweets = try! Tweet.tweets(from: string) - speaker.post(tweets: tweets, interval: 10.0).get { getIds in + speaker.post(tweets: tweets, interval: 10.0).get { getResponses in defer { expectation.fulfill() } do { - _ = try getIds() + _ = try getResponses() XCTFail() } catch let error { print(error) diff --git a/Tests/TweetupKitTests/TwitterTests.swift b/Tests/TweetupKitTests/TwitterTests.swift index 737af5a..5f5d869 100644 --- a/Tests/TweetupKitTests/TwitterTests.swift +++ b/Tests/TweetupKitTests/TwitterTests.swift @@ -27,13 +27,13 @@ class TwitterTests: XCTestCase { do { let expectation = self.expectation(description: "") - Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", credential: credential).get { getId in + Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", credential: credential).get { getResponse in defer { expectation.fulfill() } do { - let (id, _) = try getId() - XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: id).count == 1) + let response = try getResponse() + XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: response.statusId).count == 1) } catch let error { XCTFail("\(error)") } @@ -49,13 +49,13 @@ class TwitterTests: XCTestCase { Twitter.upload(media: data, credential: credential).get { getMediaId in do { let mediaId = try getMediaId() - Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", mediaId: mediaId, credential: credential).get { getId in + Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", mediaId: mediaId, credential: credential).get { getResponse in defer { expectation.fulfill() } do { - let (id, _) = try getId() - XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: id).count == 1) + let response = try getResponse() + XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: response.statusId).count == 1) } catch let error { XCTFail("\(error)") } From becf67a286eacd311e1adce1118a6bf8b9fec2eb Mon Sep 17 00:00:00 2001 From: Yuta Koshizawa Date: Sat, 30 Dec 2017 01:41:48 +0900 Subject: [PATCH 18/18] Implement tweet chains by replies --- Sources/TweetupKit/Async.swift | 18 ++++++++++++------ Sources/TweetupKit/Speaker.swift | 6 +++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Sources/TweetupKit/Async.swift b/Sources/TweetupKit/Async.swift index 70fc351..bacac33 100644 --- a/Sources/TweetupKit/Async.swift +++ b/Sources/TweetupKit/Async.swift @@ -28,27 +28,33 @@ public func sync(operation: (@escaping (T, @escaping (() throws -> R) -> ( } } +internal func repeated(operation: @escaping (T, R1?) -> Promise<() throws -> R2>, convert: @escaping (R2) -> R1, interval: TimeInterval? = nil) -> ([T]) -> Promise<() throws -> [R2]> { + return { values in + _repeat(operation: operation, for: values[...], convert: convert, interval: interval) + } +} + internal func repeated(operation: @escaping (T) -> Promise<() throws -> R>, interval: TimeInterval? = nil) -> ([T]) -> Promise<() throws -> [R]> { return { values in - _repeat(operation: operation, for: values[...], interval: interval) + _repeat(operation: { r, _ in operation(r) }, for: values[...], convert: { $0 }, interval: interval) } } -private func _repeat(operation: @escaping (T) -> Promise<() throws -> R>, for values: ArraySlice, interval: TimeInterval?, results: [R] = []) -> Promise<() throws -> [R]> { +private func _repeat(operation: @escaping (T, R1?) -> Promise<() throws -> R2>, for values: ArraySlice, convert: @escaping (R2) -> R1, interval: TimeInterval?, results: [R2] = []) -> Promise<() throws -> [R2]> { let (headOrNil, tail) = values.headAndTail guard let head = headOrNil else { return Promise { results } } - let resultPromise: Promise<() throws -> R> + let resultPromise: Promise<() throws -> R2> if let interval = interval, !tail.isEmpty { - resultPromise = wait(operation(head), for: interval) + resultPromise = wait(operation(head, results.last.map(convert)), for: interval) } else { - resultPromise = operation(head) + resultPromise = operation(head, results.last.map(convert)) } return resultPromise.flatMap { getResult in - _repeat(operation: operation, for: tail, interval: interval, results: results + [try getResult()]) + _repeat(operation: operation, for: tail, convert: convert, interval: interval, results: results + [try getResult()]) } } diff --git a/Sources/TweetupKit/Speaker.swift b/Sources/TweetupKit/Speaker.swift index 110817b..97bc9fd 100644 --- a/Sources/TweetupKit/Speaker.swift +++ b/Sources/TweetupKit/Speaker.swift @@ -43,10 +43,10 @@ public struct Speaker { } public func post(tweets: [Tweet], interval: TimeInterval?) -> Promise<() throws -> [PostResponse]> { - return repeated(operation: post, interval: interval)(tweets) + return repeated(operation: post, convert: { $0.statusId }, interval: interval)(tweets) } - public func post(tweet: Tweet) -> Promise<() throws -> PostResponse> { + public func post(tweet: Tweet, replyingTo statusId: String?) -> Promise<() throws -> PostResponse> { guard let twitterCredential = twitterCredential else { return Promise { throw SpeakerError.noTwitterCredential } } @@ -76,7 +76,7 @@ public struct Speaker { } else { mediaId = nil } - return Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential).map { PostResponse(try $0()) } + return Twitter.update(status: status, mediaId: mediaId, inReplyToStatusId: statusId, credential: twitterCredential).map { PostResponse(try $0()) } }.map { getResponse in try getResponse() }