diff --git a/Package.swift b/Package.swift index 0dbde33..ca8695b 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,8 @@ import PackageDescription let package = Package( - name: "SwiftTweetsKit" + name: "TweetupKit", + dependencies: [ + .Package(url: "https://github.com/swift-tweets/OAuthSwift.git", "2.0.0-beta") + ] ) diff --git a/README.md b/README.md index ff4c144..781a567 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# swift-tweets-kit \ No newline at end of file +# TweetupKit diff --git a/Sources/APIError.swift b/Sources/APIError.swift new file mode 100644 index 0000000..723d304 --- /dev/null +++ b/Sources/APIError.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct APIError: Error { + public let response: HTTPURLResponse + public let json: Any + + public init(response: HTTPURLResponse, json: Any) { + self.response = response + self.json = json + } +} diff --git a/Sources/ArrayExtensions.swift b/Sources/ArrayExtensions.swift new file mode 100644 index 0000000..556661b --- /dev/null +++ b/Sources/ArrayExtensions.swift @@ -0,0 +1,35 @@ +extension Array where Element: Equatable { + internal func separated(by separator: Element) -> [[Element]] { + var separated: [[Element]] = [[]] + for element in self { + if element == separator { + separated.append([]) + } else { + separated[separated.endIndex - 1].append(element) + } + } + + return separated + } +} + +extension Array where Element: Hashable { + internal func trimmingElements(in set: Set) -> [Element] { + var trimmed = [Element]() + var elements = [Element]() + + for element in self { + if set.contains(element) { + if !trimmed.isEmpty { + elements.append(element) + } + } else { + elements.forEach { trimmed.append($0) } + elements = [] + trimmed.append(element) + } + } + + return trimmed + } +} diff --git a/Sources/ArraySliceExtensions.swift b/Sources/ArraySliceExtensions.swift new file mode 100644 index 0000000..cf04342 --- /dev/null +++ b/Sources/ArraySliceExtensions.swift @@ -0,0 +1,8 @@ +extension ArraySlice { + internal var headAndTail: (Element?, ArraySlice) { + guard count > 0 else { + return (nil, []) + } + return (first, self[(startIndex + 1) ..< endIndex]) + } +} diff --git a/Sources/Async.swift b/Sources/Async.swift new file mode 100644 index 0000000..b009b58 --- /dev/null +++ b/Sources/Async.swift @@ -0,0 +1,153 @@ +import Foundation + +public func sync(operation: (@escaping (T, @escaping (() throws -> R) -> ()) -> ())) -> (T) throws -> R { + return { value in + var resultValue: R! + var resultError: Error? + var waiting = true + + operation(value) { getValue in + defer { + waiting = false + } + do { + resultValue = try getValue() + } catch let error { + resultError = error + } + } + let runLoop = RunLoop.current + while waiting && runLoop.run(mode: .defaultRunLoopMode, before: .distantFuture) { } + + if let error = resultError { + throw error + } + + return resultValue + } +} + +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, @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, @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/Sources/Code.swift b/Sources/Code.swift new file mode 100644 index 0000000..219b23c --- /dev/null +++ b/Sources/Code.swift @@ -0,0 +1,17 @@ +public struct Code { + public var language: Language + public var fileName: String + public var body: String +} + +extension Code: CustomStringConvertible { + public var description: String { + return "```\(language.identifier):\(fileName)\n\(body)\n```" + } +} + +extension Code: Equatable { + public static func ==(lhs: Code, rhs: Code) -> Bool { + return lhs.language == rhs.language && lhs.fileName == rhs.fileName && lhs.body == rhs.body + } +} diff --git a/Sources/CodeRenderer.swift b/Sources/CodeRenderer.swift new file mode 100644 index 0000000..7c52ec9 --- /dev/null +++ b/Sources/CodeRenderer.swift @@ -0,0 +1,139 @@ +import WebKit +import Foundation + +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 + + fileprivate static let height: CGFloat = 736 + + init(url: String) { + super.init() + zelf = self + + DispatchQueue.main.async { + self.webView = WebView(frame: NSRect(x: 0, y: 0, width: 414, height: CodeRenderer.height)) + self.webView.frameLoadDelegate = self + self.webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1" + self.webView.mainFrameURL = url + } + } + + func image(completion: @escaping (() throws -> CGImage) -> ()) { + DispatchQueue.main.async { + if let getImage = self.getImage { + completion { + try getImage() + } + } + + 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 + } + } + } + } + + 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 + func webView(_ sender: WebView, didFinishLoadFor frame: WebFrame) { + let document = frame.domDocument! + let body = document.getElementsByTagName("body").item(0)! + let bodyBox = body.boundingBox() + let pageBox = CGRect(origin: bodyBox.origin, size: CGSize(width: bodyBox.width, height: max(bodyBox.size.height, CodeRenderer.height))) + + let files = document.getElementsByClassName("blob-file-content")! + guard files.length > 0 else { + resolve(getImage: { throw CodeRendererError.illegalResponse } ) + return + } + let code = files.item(0) as! DOMElement + let codeBox = code.boundingBox() + + let view = frame.frameView.documentView! + let imageRep = view.bitmapImageRepForCachingDisplay(in: CGRect(origin: .zero, size: pageBox.size))! + + view.cacheDisplay(in: pageBox, to: imageRep) + + let scale: CGFloat = 2.0 + let codeBox2 = codeBox * scale + let pageBox2 = pageBox * scale + + 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)! + resolve(getImage: { + CGImage( + width: width, + height: height, + bitsPerComponent: 8, + bitsPerPixel: 32, + bytesPerRow: width * 4, + space: colorSpace, + bitmapInfo: bitmapInfo, + provider: provider, + decode: nil, + shouldInterpolate: false, + intent: .defaultIntent + )! + }) + + loading = false + } + + func webView(_ sender: WebView, didFailLoadWithError error: Error, for frame: WebFrame) { + resolve(getImage: { throw error }) + loading = false + } +} + +public enum CodeRendererError: Error { + case writingFailed + case illegalResponse +} + +internal func *(rect: CGRect, k: CGFloat) -> CGRect { + return CGRect(origin: rect.origin * k, size: rect.size * k) +} + +internal func *(point: CGPoint, k: CGFloat) -> CGPoint { + return CGPoint(x: point.x * k, y: point.y * k) +} + +internal func *(size: CGSize, k: CGFloat) -> CGSize { + return CGSize(width: size.width * k, height: size.height * k) +} diff --git a/Sources/Gist.swift b/Sources/Gist.swift new file mode 100644 index 0000000..04c197c --- /dev/null +++ b/Sources/Gist.swift @@ -0,0 +1,39 @@ +import Foundation + +internal struct Gist { + static func createGist(description: String, code: Code, accessToken: String, callback: @escaping (() 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" + request.addValue("token \(accessToken)", forHTTPHeaderField: "Authorization") + let files: [String: Any] = [ + code.fileName: [ + "content": code.body + ] + ] + let json: [String: Any] = [ + "description": description, + "public": false, + "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 } + 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) + } + guard let id = responseJson["id"] as? String else { + fatalError("Never reaches here.") + } + + return id + } + } + task.resume() + } +} diff --git a/Sources/Image.swift b/Sources/Image.swift new file mode 100644 index 0000000..8f64da6 --- /dev/null +++ b/Sources/Image.swift @@ -0,0 +1,50 @@ +public struct Image { + public var alternativeText: String + public var source: Source + + public enum Source { + case local(String) + case twitter(String) + case gist(String) + } +} + +extension Image: CustomStringConvertible { + public var description: String { + return "![\(alternativeText)](\(source))" + } +} + +extension Image: Equatable { + public static func ==(lhs: Image, rhs: Image) -> Bool { + return lhs.alternativeText == rhs.alternativeText && lhs.source == rhs.source + } +} + +extension Image.Source: CustomStringConvertible { + public var description: String { + switch self { + case let .local(path): + return path + case let .twitter(id): + return "twitter:\(id)" + case let .gist(id): + return "gist:\(id)" + } + } +} + +extension Image.Source: Equatable { + public static func ==(lhs: Image.Source, rhs: Image.Source) -> Bool { + switch (lhs, rhs) { + case let (.local(path1), .local(path2)): + return path1 == path2 + case let (.twitter(id1), .twitter(id2)): + return id1 == id2 + case let (.gist(id1), .gist(id2)): + return id1 == id2 + case (_, _): + return false + } + } +} diff --git a/Sources/Language.swift b/Sources/Language.swift new file mode 100644 index 0000000..9ba55b1 --- /dev/null +++ b/Sources/Language.swift @@ -0,0 +1,224 @@ +public enum Language { + case c + case cpp + case cSharp + case d + case elixir + case erlang + case go + case java + case javaScript + case haskell + case kotlin + case objectiveC + case ocaml + case perl + case php + case python + case ruby + case rust + case scala + case swift + case typeScript + case other(String) + + public init(identifier: String) { + switch identifier { + case "c": + self = .c + case "cpp", "c++": + self = .cpp + case "c#", "cs", "csharp": + self = .cSharp + case "d": + self = .d + case "ex", "elixir": + self = .elixir + case "erl", "erlang": + self = .erlang + case "go": + self = .go + case "hs", "haskell": + self = .haskell + case "java": + self = .java + case "js", "javascript": + self = .javaScript + case "kt", "kotlin": + self = .kotlin + case "objc", "objectivec", "objective-c": + self = .objectiveC + case "ml", "ocaml": + self = .ocaml + case "pl", "perl": + self = .perl + case "php": + self = .php + case "py", "python": + self = .python + case "rb", "ruby": + self = .ruby + case "rust": + self = .rust + case "scala": + self = .scala + case "swift": + self = .swift + case "ts", "typescript": + self = .typeScript + default: + self = .other(identifier) + } + } + + public var identifier: String { + switch self { + case .c: + return "c" + case .cpp: + return "cpp" + case .cSharp: + return "cs" + case .d: + return "d" + case .elixir: + return "elixir" + case .erlang: + return "erlang" + case .go: + return "go" + case .haskell: + return "haskell" + case .java: + return "java" + case .javaScript: + return "javascript" + case .kotlin: + return "kotlin" + case .objectiveC: + return "objc" + case .ocaml: + return "ocaml" + case .perl: + return "perl" + case .php: + return "php" + case .python: + return "python" + case .ruby: + return "ruby" + case .rust: + return "rust" + case .scala: + return "scala" + case .swift: + return "swift" + case .typeScript: + return "typescript" + case let .other(identifier): + return identifier + } + } + + public var filenameExtension: String? { + switch self { + case .c: + return "c" + case .cpp: + return "cpp" + case .cSharp: + return "cs" + case .d: + return "d" + case .elixir: + return "ex" + case .erlang: + return "erl" + case .go: + return "go" + case .haskell: + return "hs" + case .java: + return "java" + case .javaScript: + return "js" + case .kotlin: + return "kt" + case .objectiveC: + return "m" + case .ocaml: + return "ml" + case .perl: + return "pl" + case .php: + return "php" + case .python: + return "py" + case .ruby: + return "rb" + case .rust: + return "rs" + case .scala: + return "scala" + case .swift: + return "swift" + case .typeScript: + return "ts" + case .other(_): + return nil + } + } +} + +extension Language: Equatable { + public static func ==(lhs: Language, rhs: Language) -> Bool { + switch (lhs, rhs) { + case (.c, .c): + return true + case (.cpp, .cpp): + return true + case (.cSharp, .cSharp): + return true + case (.d, .d): + return true + case (.elixir, .elixir): + return true + case (.erlang, .erlang): + return true + case (.go, .go): + return true + case (.haskell, .haskell): + return true + case (.java, .java): + return true + case (.javaScript, .javaScript): + return true + case (.kotlin, .kotlin): + return true + case (.objectiveC, .objectiveC): + return true + case (.ocaml, .ocaml): + return true + case (.perl, .perl): + return true + case (.php, .php): + return true + case (.python, .python): + return true + case (.ruby, .ruby): + return true + case (.rust, .rust): + return true + case (.scala, .scala): + return true + case (.swift, .swift): + return true + case (.typeScript, .typeScript): + return true + case let (.other(identifier1), .other(identifier2)): + return identifier1 == identifier2 + default: + return false + } + } +} diff --git a/Sources/NSRegularExpressionExtensions.swift b/Sources/NSRegularExpressionExtensions.swift new file mode 100644 index 0000000..3f1b938 --- /dev/null +++ b/Sources/NSRegularExpressionExtensions.swift @@ -0,0 +1,7 @@ +import Foundation + +extension NSRegularExpression { + internal func matches(in string: String) -> [NSTextCheckingResult] { + return matches(in: string, options: [], range: NSMakeRange(0, (string as NSString).length)) + } +} diff --git a/Sources/NSTextCheckingResultExtensions.swift b/Sources/NSTextCheckingResultExtensions.swift new file mode 100644 index 0000000..a5335ad --- /dev/null +++ b/Sources/NSTextCheckingResultExtensions.swift @@ -0,0 +1,11 @@ +import Foundation + +extension NSTextCheckingResult { + internal func validRangeAt(_ index: Int) -> NSRange? { + let range = rangeAt(index) + guard range.location != NSNotFound else { + return nil + } + return range + } +} diff --git a/Sources/OAuthCredential.swift b/Sources/OAuthCredential.swift new file mode 100644 index 0000000..5b375d1 --- /dev/null +++ b/Sources/OAuthCredential.swift @@ -0,0 +1,24 @@ +import Foundation + +public struct OAuthCredential { + public let consumerKey: String + public let consumerSecret: String + public let oauthToken: String + public let oauthTokenSecret: String + + public init(consumerKey: String, consumerSecret: String, oauthToken: String, oauthTokenSecret: String) { + self.consumerKey = consumerKey + self.consumerSecret = consumerSecret + self.oauthToken = oauthToken + self.oauthTokenSecret = oauthTokenSecret + } +} + +extension OAuthCredential: Equatable { + public static func ==(lhs: OAuthCredential, rhs: OAuthCredential) -> Bool { + return lhs.consumerKey == rhs.consumerKey + && lhs.consumerSecret == rhs.consumerSecret + && lhs.oauthToken == rhs.oauthToken + && lhs.oauthTokenSecret == rhs.oauthTokenSecret + } +} diff --git a/Sources/Parser.swift b/Sources/Parser.swift new file mode 100644 index 0000000..7de83c7 --- /dev/null +++ b/Sources/Parser.swift @@ -0,0 +1,115 @@ +import Foundation + +extension Tweet { + internal static let imagePattern = try! NSRegularExpression(pattern: "!\\[([^\\]]*)\\]\\(((twitter:([0-9]+))|(gist:([0-9a-f]+))|([^\\)]*))\\)") + internal static let codePattern = try! NSRegularExpression(pattern: "```([a-z]*)(:(.*))?\\n((.*\\n)*)```") + private static let hashTagPatternString = "#\\w+" + internal static let hashTagPattern = try! NSRegularExpression(pattern: "^\(hashTagPatternString)$") + internal static let hashTagInTweetPattern = try! NSRegularExpression(pattern: "(^|\\s)(\(hashTagPatternString))($|\\s)") + + public static func tweets(from string: String, hashTag: String? = nil) throws -> [Tweet] { + return try string.lines + .separated(by: "---") + .map { lines in lines.trimmingElements(in: [""]).joined(separator: "\n") } + .map { try Tweet(rawString: $0, hashTag: hashTag) } + } + + public init(rawString: String, hashTag: String? = nil) throws { + let attachment = try Tweet.matchedAttachment(in: rawString) + let originalBody = attachment.map { attachment -> String in + (rawString as NSString).replacingCharacters(in: attachment.0, with: "") + }?.trimmingCharacters(in: .whitespacesAndNewlines) ?? rawString + let body = try Tweet.bodyWithHashTag(body: originalBody, hashTag: hashTag) + try self.init(body: body, attachment: attachment?.1) + } + + internal static func bodyWithHashTag(body: String, hashTag: String?) throws -> String { + guard let hashTag = hashTag else { return body } + guard Tweet.hashTagPattern.matches(in: hashTag).count == 1 else { + throw TweetParseError.illegalHashTag(hashTag) + } + guard !Tweet.containsHashTag(body: body, hashTag: hashTag) else { + return body + } + return body + " " + hashTag + } + + internal static func containsHashTag(body: String, hashTag: String) -> Bool { + return Tweet.hashTagInTweetPattern.matches(in: body).map { + (body as NSString).substring(with: $0.rangeAt(2)) + }.contains(hashTag) + } + + internal static func matchedAttachment(in string: String) throws -> (NSRange, Attachment)? { + let codeMatchingResults = try matchingAttachments(in: string, pattern: codePattern, initializer: Code.init) + let imageMatchingResults = try matchingAttachments(in: string, pattern: imagePattern, initializer: Image.init) + let attachments: [(NSRange, Attachment)] = codeMatchingResults.map { ($0.0, .code($0.1)) } + + imageMatchingResults.map { ($0.0, .image($0.1)) } + guard attachments.count <= 1 else { + throw TweetParseError.multipleAttachments(string, attachments.map { $0.1 }) + } + let attachment = attachments.first + if let attachment = attachment { + guard attachment.0.location + attachment.0.length == (string as NSString).length else { + throw TweetParseError.nonTailAttachment(string, attachment.1) + } + } + return attachment + } + + 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)) } + } +} + +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 fileName: String + do { + let range = matchingResult.rangeAt(3) + if (range.location == NSNotFound) { + guard let filenameExtension = language.filenameExtension else { + throw TweetParseError.codeWithoutFileName(string) + } + fileName = "code.\(filenameExtension)" + } else { + fileName = nsString.substring(with: range) + } + } + self.init( + language: language, + fileName: fileName, + body: nsString.substring(with: matchingResult.rangeAt(4)) + ) + } +} + +extension Image { + fileprivate init(string: String, matchingResult: NSTextCheckingResult) { + let string = string as NSString + let source: Source + if let range = matchingResult.validRangeAt(4) { + source = .twitter(string.substring(with: range)) + } else if let range = matchingResult.validRangeAt(6) { + source = .gist(string.substring(with: range)) + } else if let range = matchingResult.validRangeAt(7) { + source = .local(string.substring(with: range)) + } else { + fatalError("Never reaches here.") + } + + self.init( + alternativeText: string.substring(with: matchingResult.rangeAt(1)), + source: source + ) + } +} + +public enum TweetParseError: Error { + case multipleAttachments(String, [Tweet.Attachment]) + case nonTailAttachment(String, Tweet.Attachment) + case codeWithoutFileName(String) + case illegalHashTag(String) +} diff --git a/Sources/Speaker.swift b/Sources/Speaker.swift new file mode 100644 index 0000000..68159c6 --- /dev/null +++ b/Sources/Speaker.swift @@ -0,0 +1,185 @@ +import Foundation + +public struct Speaker { + public let twitterCredential: OAuthCredential? + public let githubToken: String? + public let qiitaToken: String? + public var baseDirectoryPath: String? + public var outputDirectoryPath: String? + + public init(twitterCredential: OAuthCredential? = nil, githubToken: String? = nil, qiitaToken: String? = nil, baseDirectoryPath: String? = nil, outputDirectoryPath: String? = nil) { + self.twitterCredential = twitterCredential + self.githubToken = githubToken + self.qiitaToken = qiitaToken + self.baseDirectoryPath = baseDirectoryPath + 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 post(tweets: [Tweet], with interval: TimeInterval?, callback: @escaping (() throws -> ([(String, String)])) -> ()) { + repeated(operation: post, interval: interval)(tweets, callback) + } + + public func post(tweet: Tweet, callback: @escaping (() throws -> (String, String)) -> ()) { + guard let twitterCredential = twitterCredential else { + callback { + throw SpeakerError.noTwitterCredential + } + return + } + + let resolve = flatten(flatten(resolveCode, resolveGist), resolveImage) + resolve(tweet) { getTweet in + do { + let tweet = try getTweet() + let status = tweet.body + let mediaId: String? + if let attachment = tweet.attachment { + switch attachment { + case let .image(image): + switch image.source { + case let .twitter(id): + mediaId = id + case .gist(_): + // TODO + mediaId = nil + case _: + fatalError("Never reaches here.") + } + case _: + fatalError("Never reaches here.") + } + } else { + mediaId = nil + } + Twitter.update(status: status, mediaId: mediaId, credential: twitterCredential) { getId in + callback { + try getId() + } + } + } catch let error { + callback { throw error } + } + } + } + + public func resolveImages(of tweets: [Tweet], callback: @escaping (() throws -> [Tweet]) -> ()) { + repeated(operation: resolveImage)(tweets, callback) + } + + public func resolveImage(of tweet: Tweet, callback: @escaping (() throws -> Tweet) -> ()) { + guard case let .some(.image(image)) = tweet.attachment, case let .local(path) = image.source else { + callback { + tweet + } + return + } + guard let twitterCredential = twitterCredential else { + callback { + throw SpeakerError.noTwitterCredential + } + return + } + + do { + let imagePath = Speaker.imagePath(path, from: baseDirectoryPath) + Twitter.upload(media: try Data(contentsOf: URL(fileURLWithPath: imagePath)), credential: twitterCredential) { getId in + callback { + 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 + } + } + } + + internal static func imagePath(_ path: String, from: String?) -> String { + if let from = from, !path.hasPrefix("/") { + return from.appendingPathComponent(path) + } else { + return path + } + } + + public func resolveCodes(of tweets: [Tweet], callback: @escaping (() throws -> [Tweet]) -> ()) { + repeated(operation: resolveCode)(tweets, callback) + } + + public func resolveCode(of tweet: Tweet, callback: @escaping (() throws -> Tweet) -> ()) { + guard case let .some(.code(code)) = tweet.attachment else { + callback { + tweet + } + return + } + guard let githubToken = githubToken else { + callback { + throw SpeakerError.noGithubToken + } + return + } + + Gist.createGist(description: tweet.body, code: code, accessToken: githubToken) { 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)))) + } + } + } + + public func resolveGists(of tweets: [Tweet], callback: @escaping (() throws -> [Tweet]) -> ()) { + repeated(operation: resolveGist)(tweets, callback) + } + + public func resolveGist(of tweet: Tweet, callback: @escaping (() throws -> Tweet) -> ()) { + guard case let .some(.image(image)) = tweet.attachment, case let .gist(id) = image.source else { + callback { + tweet + } + return + } + guard let outputDirectoryPath = outputDirectoryPath else { + callback { + throw SpeakerError.noOutputDirectoryPath + } + return + } + + 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 + callback { + try getVoid() + return try Tweet(body: "\(tweet.body)", attachment: .image(Image(alternativeText: image.alternativeText, source: .local(imagePath)))) + } + } + } +} + +public enum SpeakerError: Error { + case noTwitterCredential + case noGithubToken + case noOutputDirectoryPath +} diff --git a/Sources/StringExtensions.swift b/Sources/StringExtensions.swift new file mode 100644 index 0000000..4487a69 --- /dev/null +++ b/Sources/StringExtensions.swift @@ -0,0 +1,23 @@ +import Foundation + +extension String { + internal var lines: [String] { + var lines: [String] = [] + self.enumerateLines() { line, _ in + lines.append(line) + } + return lines + } + + internal func replacingOccurrences(of pattern: NSRegularExpression, with template: String) -> String { + return pattern.stringByReplacingMatches(in: self, options: [], range: NSMakeRange(0, characters.count), withTemplate: template) + } + + internal var deletingLastPathComponent: String { + return (self as NSString).deletingLastPathComponent + } + + internal func appendingPathComponent(_ pathComponent: String) -> String { + return (self as NSString).appendingPathComponent(pathComponent) + } +} diff --git a/Sources/SwiftTweetsKit.swift b/Sources/SwiftTweetsKit.swift deleted file mode 100644 index 27dc075..0000000 --- a/Sources/SwiftTweetsKit.swift +++ /dev/null @@ -1,4 +0,0 @@ -struct SwiftTweetsKit { - - var text = "Hello, World!" -} diff --git a/Sources/Tweet.swift b/Sources/Tweet.swift new file mode 100644 index 0000000..d4c8805 --- /dev/null +++ b/Sources/Tweet.swift @@ -0,0 +1,101 @@ +import Foundation + +public struct Tweet { + internal static let urlPattern = try! NSRegularExpression(pattern: "(^|\\s)(http(s)?://[a-zA-Z0-9~!@#$%&*-_=+\\[\\]|:;',./?]*)($|\\s)") + internal static let urlLength = 23 + internal static let maxLength = 140 + + public let body: String + public let attachment: Attachment? + + public init(body: String, attachment: Attachment? = nil) throws { + guard !body.isEmpty || attachment != nil else { throw TweetInitializationError.empty } + self.body = body + self.attachment = attachment + + let length = self.length + guard length <= Tweet.maxLength else { throw TweetInitializationError.tooLong(self.body, self.attachment, length) } + } + + public var length: Int { + let replaced = NSMutableString(string: body) + let numberOfUrls = Tweet.urlPattern.replaceMatches(in: replaced, options: [], range: NSMakeRange(0, replaced.length), withTemplate: "$1$4") + + let normalized = replaced.precomposedStringWithCanonicalMapping as NSString + + var bodyLength = normalized.length + Tweet.urlLength * numberOfUrls + do { + let buffer = UnsafeMutablePointer.allocate(capacity: normalized.length) + defer { + buffer.deallocate(capacity: normalized.length) + } + normalized.getCharacters(buffer) + var skip = false + let end = normalized.length - 1 + if end > 1 { + for i in 0.. Bool { + return lhs.body == rhs.body && lhs.attachment == rhs.attachment + } +} + +extension Tweet.Attachment: Equatable { + public static func ==(lhs: Tweet.Attachment, rhs: Tweet.Attachment) -> Bool { + switch (lhs, rhs) { + case let (.image(image1), .image(image2)): + return image1 == image2 + case let (.code(code1), .code(code2)): + return code1 == code2 + case (_, _): + return false + } + } +} + +public enum TweetInitializationError: Error { + case empty + case tooLong(String, Tweet.Attachment?, Int) +} diff --git a/Sources/Twitter.swift b/Sources/Twitter.swift new file mode 100644 index 0000000..56e2e4b --- /dev/null +++ b/Sources/Twitter.swift @@ -0,0 +1,80 @@ +import OAuthSwift +import Foundation + +internal struct Twitter { + static func update(status: String, mediaId: String? = nil, credential: OAuthCredential, callback: @escaping (() throws -> (String, String)) -> ()) { + let client = OAuthSwiftClient(credential: credential) + client.sessionFactory.queue = { .current } + + var parameters = [ + "status": status + ] + 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 + } + } + + static func upload(media: Data, credential: OAuthCredential, callback: @escaping (() 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 + } + } +} + +extension OAuthSwiftClient { + fileprivate convenience init(credential: OAuthCredential) { + self.init( + consumerKey: credential.consumerKey, + consumerSecret: credential.consumerSecret, + oauthToken: credential.oauthToken, + oauthTokenSecret: credential.oauthTokenSecret, + version: .oauth1 + ) + } + + fileprivate func post(_ url: String, parameters: OAuthSwift.Parameters, callback: @escaping (() throws -> T) -> (), completion: @escaping (OAuthSwiftResponse) throws -> (T)) -> OAuthSwiftRequestHandle? { + return post( + url, + parameters: parameters, + success: { response in + guard response.response.statusCode == 200 else { + let httpResponse = response.response + callback { + let json = try! JSONSerialization.jsonObject(with: response.data, options: []) // `!` never fails + throw APIError(response: httpResponse, json: json) + } + return + } + + callback { + try completion(response) + } + }, + failure: { error in + callback { + throw error + } + } + ) + + } +} diff --git a/Tests/.gitignore b/Tests/.gitignore new file mode 100644 index 0000000..6c7f906 --- /dev/null +++ b/Tests/.gitignore @@ -0,0 +1,8 @@ +/twitter.json +/twitter-*.json +!/twitter-template.json +/github.json +/github-*.json +!/github-template.json +/*.png +!/image.png diff --git a/Tests/SwiftTweetsKitTests/SwiftTweetsKitTests.swift b/Tests/SwiftTweetsKitTests/SwiftTweetsKitTests.swift deleted file mode 100644 index 7023554..0000000 --- a/Tests/SwiftTweetsKitTests/SwiftTweetsKitTests.swift +++ /dev/null @@ -1,17 +0,0 @@ -import XCTest -@testable import SwiftTweetsKit - -class SwiftTweetsKitTests: XCTestCase { - func testExample() { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - XCTAssertEqual(SwiftTweetsKit().text, "Hello, World!") - } - - - static var allTests : [(String, (SwiftTweetsKitTests) -> () throws -> Void)] { - return [ - ("testExample", testExample), - ] - } -} diff --git a/Tests/TweetupKitTests/ArrayExtensionsTests.swift b/Tests/TweetupKitTests/ArrayExtensionsTests.swift new file mode 100644 index 0000000..75df773 --- /dev/null +++ b/Tests/TweetupKitTests/ArrayExtensionsTests.swift @@ -0,0 +1,24 @@ +import XCTest +@testable import TweetupKit + +class ArrayExtensionsTests: XCTestCase { + func testTrimmingElements() { + do { + let lines = ["", "", "\t", "\n", "a", "", "", "\n", "b", "c", "", "d", "", "", "\r"] + let trimmed = lines.trimmingElements(in: ["", "\t", "\n", "\r"]) + XCTAssertEqual(trimmed, ["a", "", "", "\n", "b", "c", "", "d"]) + } + + do { + let lines = ["Twinkle, twinkle, little star,", "How I wonder what you are!", ""] + let trimmed = lines.trimmingElements(in: [""]) + XCTAssertEqual(trimmed, ["Twinkle, twinkle, little star,", "How I wonder what you are!"]) + } + + do { + let lines = ["", "Up above the world so high,", "Like a diamond in the sky.", "", "```swift:hello.swift", "let name = \"Swift\"", "print(\"Hello \\(name)!\")", "```", ""] + let trimmed = lines.trimmingElements(in: [""]) + XCTAssertEqual(trimmed, ["Up above the world so high,", "Like a diamond in the sky.", "", "```swift:hello.swift", "let name = \"Swift\"", "print(\"Hello \\(name)!\")", "```"]) + } + } +} diff --git a/Tests/TweetupKitTests/AsyncTests.swift b/Tests/TweetupKitTests/AsyncTests.swift new file mode 100644 index 0000000..9859486 --- /dev/null +++ b/Tests/TweetupKitTests/AsyncTests.swift @@ -0,0 +1,106 @@ +import XCTest +@testable import TweetupKit + +import Foundation + +class AsyncTests: XCTestCase { + func testRepeated() { + do { + let expectation = self.expectation(description: "") + + let start = Date.timeIntervalSinceReferenceDate + + repeated(operation: asyncIncrement, interval: 2.0)(Array(1...5)) { getValues in + defer { + expectation.fulfill() + } + + do { + let values = try getValues() + XCTAssertEqual(values, [2, 3, 4 ,5, 6]) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 8.5, handler: nil) + + let end = Date.timeIntervalSinceReferenceDate + + XCTAssertGreaterThan(end - start, 8.0) + } + + do { + let expectation = self.expectation(description: "") + + let start = Date.timeIntervalSinceReferenceDate + + repeated(operation: asyncTime, interval: 2.0)([(), (), (), (), ()]) { getValues in + defer { + expectation.fulfill() + } + + do { + let values = try getValues() + XCTAssertEqual(values.count, 5) + let allowableError: TimeInterval = 0.1 + XCTAssertLessThan(abs(values[0] - start), allowableError) + XCTAssertLessThan(abs(values[1] - (values[0] + 2.0)), allowableError) + XCTAssertLessThan(abs(values[2] - (values[1] + 2.0)), allowableError) + XCTAssertLessThan(abs(values[3] - (values[2] + 2.0)), allowableError) + XCTAssertLessThan(abs(values[4] - (values[3] + 2.0)), allowableError) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 8.5, handler: nil) + + let end = Date.timeIntervalSinceReferenceDate + + XCTAssertGreaterThan(end - start, 8.0) + } + } + + func testWaiting() { + let expectation = self.expectation(description: "") + + let start = Date.timeIntervalSinceReferenceDate + + waiting(operation: asyncIncrement, with: 2.0)(42) { getValue in + defer { + expectation.fulfill() + } + + do { + let value = try getValue() + XCTAssertEqual(value, 43) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 2.5, handler: nil) + + let end = Date.timeIntervalSinceReferenceDate + + XCTAssertGreaterThan(end - start, 2.0) + } +} + +private func asyncIncrement(value: Int, completion: @escaping (() throws -> Int) -> ()) { + DispatchQueue.main.async { + completion { + value + 1 + } + } +} + + +private func asyncTime(_ value: (), completion: @escaping (() throws -> TimeInterval) -> ()) { + DispatchQueue.main.async { + completion { + Date.timeIntervalSinceReferenceDate + } + } +} diff --git a/Tests/TweetupKitTests/CodeTests.swift b/Tests/TweetupKitTests/CodeTests.swift new file mode 100644 index 0000000..13e3655 --- /dev/null +++ b/Tests/TweetupKitTests/CodeTests.swift @@ -0,0 +1,18 @@ +import XCTest +@testable import TweetupKit + +class CodeTests: XCTestCase { + func testDescription() { + do { + let code = Code(language: .swift, fileName: "foo.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)\")") + let result = code.description + XCTAssertEqual(result, "```swift:foo.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)\")\n```") + } + + do { + let code = Code(language: .other("foo"), fileName: "bar.foo", body: "let name = \"Swift\"\nprint(\"Hello \\(name)\")") + let result = code.description + XCTAssertEqual(result, "```foo:bar.foo\nlet name = \"Swift\"\nprint(\"Hello \\(name)\")\n```") + } + } +} diff --git a/Tests/TweetupKitTests/GeneralError.swift b/Tests/TweetupKitTests/GeneralError.swift new file mode 100644 index 0000000..371c698 --- /dev/null +++ b/Tests/TweetupKitTests/GeneralError.swift @@ -0,0 +1,3 @@ +struct GeneralError: Error { + let message: String +} diff --git a/Tests/TweetupKitTests/GistTests.swift b/Tests/TweetupKitTests/GistTests.swift new file mode 100644 index 0000000..9a71d0f --- /dev/null +++ b/Tests/TweetupKitTests/GistTests.swift @@ -0,0 +1,87 @@ +import XCTest +@testable import TweetupKit + +import Foundation + +class GistTests: XCTestCase { + var accessToken: String? + + override func setUp() { + super.setUp() + + do { + self.accessToken = try loadGithubToken() + } catch let error { + XCTFail("\(error)") + } + } + + override func tearDown() { + accessToken = nil + super.tearDown() + } + + func testCreateGist() { + guard let accessToken = accessToken else { return } + + 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 + defer { + expectation.fulfill() + } + do { + let id = try getId() + XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9a-f]+$").matches(in: id.description).count == 1, id.description) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + + 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 + defer { + expectation.fulfill() + } + do { + _ = try getId() + XCTFail() + } catch let error as APIError { + guard let message = (error.json as? [String: Any])?["message"] as? String else { + XCTFail("\(error.json)") + return + } + XCTAssertEqual(message, "Bad credentials") + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + } +} + +func loadGithubToken() throws -> String { + let path = #file.deletingLastPathComponent.deletingLastPathComponent.appendingPathComponent("github.json") + let data: Data + do { + data = try Data(contentsOf: URL(fileURLWithPath: path)) + } catch { + throw GeneralError(message: "Put a file at \(path), which contains tokens of Github for the tests in the format same as github-template.json in the same directory.") + } + + let json: [String: Any] = try JSONSerialization.jsonObject(with: data) as! [String: Any] + + guard let accessToken = json["accessToken"] as? String else { + throw GeneralError(message: "Lack of `accessToken` in \(path).") + } + + return accessToken +} diff --git a/Tests/TweetupKitTests/ImageTests.swift b/Tests/TweetupKitTests/ImageTests.swift new file mode 100644 index 0000000..d489a25 --- /dev/null +++ b/Tests/TweetupKitTests/ImageTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import TweetupKit + +class ImageTests: XCTestCase { + func testDescription() { + do { + let image = Image(alternativeText: "", source: .local("path/to/image.png")) + let result = image.description + XCTAssertEqual(result, "![](path/to/image.png)") + } + + do { + let image = Image(alternativeText: "", source: .twitter("471592142565957632")) + let result = image.description + XCTAssertEqual(result, "![](twitter:471592142565957632)") + } + + do { + let image = Image(alternativeText: "", source: .gist("aa5a315d61ae9438b18d")) + let result = image.description + XCTAssertEqual(result, "![](gist:aa5a315d61ae9438b18d)") + } + + do { + let image = Image(alternativeText: "alternative text", source: .local("path/to/image.png")) + let result = image.description + XCTAssertEqual(result, "![alternative text](path/to/image.png)") + } + } +} diff --git a/Tests/TweetupKitTests/ParserTests.swift b/Tests/TweetupKitTests/ParserTests.swift new file mode 100644 index 0000000..95447c6 --- /dev/null +++ b/Tests/TweetupKitTests/ParserTests.swift @@ -0,0 +1,548 @@ +import XCTest +@testable import TweetupKit + +class ParserTests: XCTestCase { + func testTweets() { + do { + 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)" + let tweets = try! Tweet.tweets(from: string) + XCTAssertEqual(tweets.count, 3) + + do { + let tweet = tweets[0] + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star,\nHow I wonder what you are!") + XCTAssertTrue(tweet.attachment == nil) + } + + do { + let tweet = tweets[1] + XCTAssertEqual(tweet.body, "Up above the world so high,\nLike a diamond in the sky.") + switch tweet.attachment { + case let .some(.code(code)): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "let name = \"Swift\"\nprint(\"Hello \\(name)!\")\n") + default: + XCTFail() + } + } + + do { + let tweet = tweets[2] + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star,\nHow I wonder what you are!") + switch tweet.attachment { + case let .some(.image(image)): + XCTAssertEqual(image.alternativeText, "") + XCTAssertEqual(image.source, .local("path/to/image.png")) + default: + XCTFail() + } + } + } + + do { // without blank lines + let string = "Twinkle, twinkle, little star,\nHow I wonder what you are!\n---\nUp above the world so high,\nLike a diamond in the sky.\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\n---\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](path/to/image.png)" + let tweets = try! Tweet.tweets(from: string) + XCTAssertEqual(tweets.count, 3) + + do { + let tweet = tweets[0] + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star,\nHow I wonder what you are!") + XCTAssertTrue(tweet.attachment == nil) + } + + do { + let tweet = tweets[1] + XCTAssertEqual(tweet.body, "Up above the world so high,\nLike a diamond in the sky.") + switch tweet.attachment { + case let .some(.code(code)): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "let name = \"Swift\"\nprint(\"Hello \\(name)!\")\n") + default: + XCTFail() + } + } + + do { + let tweet = tweets[2] + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star,\nHow I wonder what you are!") + switch tweet.attachment { + case let .some(.image(image)): + XCTAssertEqual(image.alternativeText, "") + XCTAssertEqual(image.source, .local("path/to/image.png")) + default: + XCTFail() + } + } + } + + do { // `TweetInitializationError` + 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---" + do { + _ = try Tweet.tweets(from: string) + XCTFail() + } catch TweetInitializationError.empty { + } catch _ { + XCTFail() + } + } + + do { // `TweetParseError` + let string = "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\n---\n\nUp above the world so high,\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\nLike a diamond in the sky.\n\n---\n\nTwinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](path/to/image.png)" + do { + _ = try Tweet.tweets(from: string) + XCTFail() + } catch let TweetParseError.nonTailAttachment(rawString, attachment) { + XCTAssertEqual(rawString, "Up above the world so high,\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```\nLike a diamond in the sky.") + switch attachment { + case let .code(code): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "let name = \"Swift\"\nprint(\"Hello \\(name)!\")\n") + default: + XCTFail() + } + } catch _ { + XCTFail() + } + } + + do { + 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```unknown\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)" + do { + _ = try Tweet.tweets(from: string) + XCTFail() + } catch let TweetParseError.codeWithoutFileName(rawString) { + XCTAssertEqual(rawString, "Up above the world so high,\nLike a diamond in the sky.\n\n```unknown\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```") + } catch _ { + XCTFail() + } + } + + do { // hash tag + 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, #swtws\nHow I wonder what you are!\n\n![](path/to/image.png)" + let hashTag = "#swtws" + let tweets = try! Tweet.tweets(from: string, hashTag: hashTag) + XCTAssertEqual(tweets.count, 3) + + do { + let tweet = tweets[0] + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star,\nHow I wonder what you are! #swtws") + XCTAssertTrue(tweet.attachment == nil) + } + + do { + let tweet = tweets[1] + XCTAssertEqual(tweet.body, "Up above the world so high,\nLike a diamond in the sky. #swtws") + switch tweet.attachment { + case let .some(.code(code)): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "let name = \"Swift\"\nprint(\"Hello \\(name)!\")\n") + default: + XCTFail() + } + } + + do { + let tweet = tweets[2] + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star, #swtws\nHow I wonder what you are!") + switch tweet.attachment { + case let .some(.image(image)): + XCTAssertEqual(image.alternativeText, "") + XCTAssertEqual(image.source, .local("path/to/image.png")) + default: + XCTFail() + } + } + } + } + + func testInit() { + do { + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky." + let tweet = try! Tweet(rawString: string) + XCTAssertEqual(tweet.body, string) + XCTAssertTrue(tweet.attachment == nil) + } + + do { // with `.code` + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n```swift:hello.swift\nprint(\"Hello world!\")\n```" + let tweet = try! Tweet(rawString: string) + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.") + switch tweet.attachment { + case let .some(.code(code)): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "print(\"Hello world!\")\n") + default: + XCTFail() + } + } + + do { // with `.code` + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```" + let tweet = try! Tweet(rawString: string) + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.") + switch tweet.attachment { + case let .some(.code(code)): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "print(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n") + default: + XCTFail() + } + } + + do { // with non-tail `.code` + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n```swift:hello.swift\nprint(\"Hello world!\")\n```\nTwinkle, twinkle, little star, How I wonder what you are!" + do { + _ = try Tweet(rawString: string) + XCTFail() + } catch let TweetParseError.nonTailAttachment(rawString, .code(code)) { + XCTAssertEqual(rawString, string) + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "print(\"Hello world!\")\n") + } catch _ { + XCTFail() + } + } + + do { // with `.image` + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n![alternative text](path/to/image.png)" + let tweet = try! Tweet(rawString: string) + XCTAssertEqual(tweet.body, "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.") + switch tweet.attachment { + case let .some(.image(image)): + XCTAssertEqual(image.alternativeText, "alternative text") + XCTAssertEqual(image.source, .local("path/to/image.png")) + default: + XCTFail() + } + } + + do { // with non-tail `.image` + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n![alternative text](path/to/image.png)\nTwinkle, twinkle, little star, How I wonder what you are!" + do { + _ = try Tweet(rawString: string) + XCTFail() + } catch let TweetParseError.nonTailAttachment(rawString, .image(image)) { + XCTAssertEqual(rawString, string) + XCTAssertEqual(image.alternativeText, "alternative text") + XCTAssertEqual(image.source, .local("path/to/image.png")) + } catch _ { + XCTFail() + } + } + + do { // with `.image` + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n![alternative text](path/to/image.png)\n```swift:hello.swift\nprint(\"Hello world!\")\n```" + do { + _ = try Tweet(rawString: string) + XCTFail() + } catch let TweetParseError.multipleAttachments(rawString, attachments) { + XCTAssertEqual(rawString, string) + switch attachments[0] { + case let .code(code): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "print(\"Hello world!\")\n") + default: + XCTFail() + } + + switch attachments[1] { + case let .image(image): + XCTAssertEqual(image.alternativeText, "alternative text") + XCTAssertEqual(image.source, .local("path/to/image.png")) + default: + XCTFail() + } + } catch _ { + XCTFail() + } + } + } + + func testCodePattern() { + do { + let string = "```swift:hello.swift\nprint(\"Hello world!\")\n```" + + let results = Tweet.codePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "```swift:hello.swift\nlet s = \"Hello world!\"\nprint(s)\n```" + + let results = Tweet.codePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```" + + let results = Tweet.codePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "foo bar\n```swift:hello.swift\nprint(\"Hello world!\")\n```\nqux" + + let results = Tweet.codePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n```swift:hello.swift\nprint(\"Hello world!\")\n```" + + let results = Tweet.codePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "Twinkle, twinkle, little star, How I wonder what you are! Up above the world so high, Like a diamond in the sky.\n```swift:hello.swift\nprint(\"1️⃣2️⃣3️⃣4️⃣5️⃣1️⃣2️⃣3️⃣4️⃣5️⃣\")\n```" + + let results = Tweet.codePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + } + + func testImagePattern() { + do { + let string = "![](path/to/image.png)" + + let results = Tweet.imagePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "![alternative text](path/to/image.png)" + + let results = Tweet.imagePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { // twitter + let string = "![alternative text](twitter:471592142565957632)" + + let results = Tweet.imagePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { // gist + let string = "![alternative text](gist:aa5a315d61ae9438b18d)" + + let results = Tweet.imagePattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + 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") + } + + do { + let string = "foo bar ![alternative text 1](path/to/image1.png)\nbaz\n\n![alternative text 2](path/to/image2.png)\nqux" + + let results = Tweet.imagePattern.matches(in: string) + XCTAssertEqual(results.count, 2) + + 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") + } + + 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") + } + } + + do { + let string = "abcdefg" + + let results = Tweet.imagePattern.matches(in: string) + XCTAssertEqual(results.count, 0) + } + } + + func testHashTagPattern() { + do { + let string = "#abc" + + let results = Tweet.hashTagPattern.matches(in: string) + XCTAssertEqual(results.count, 1) + XCTAssertEqual((string as NSString).substring(with: results[0].rangeAt(0)), "#abc") + } + + do { + let string = "##abc" + + let results = Tweet.hashTagPattern.matches(in: string) + XCTAssertEqual(results.count, 0) + } + + do { + let string = "#a-bc" + + let results = Tweet.hashTagPattern.matches(in: string) + XCTAssertEqual(results.count, 0) + } + } + + func testHashTagInTweetPattern() { + do { + let string = "foo bar #swtws\nqux" + + let results = Tweet.hashTagInTweetPattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + do { + let result = results[0] + XCTAssertEqual(result.numberOfRanges, 4) + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#swtws") + } + } + + do { + let string = "#swtws foo bar \nqux" + + let results = Tweet.hashTagInTweetPattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + do { + let result = results[0] + XCTAssertEqual(result.numberOfRanges, 4) + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#swtws") + } + } + + do { + let string = "foo bar \nqux #swtws" + + let results = Tweet.hashTagInTweetPattern.matches(in: string) + XCTAssertEqual(results.count, 1) + + do { + let result = results[0] + XCTAssertEqual(result.numberOfRanges, 4) + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#swtws") + } + } + + do { + let string = "#abc Up above the world so high, #def\nLike a diamond in the sky. #ghi\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```" + + let results = Tweet.hashTagInTweetPattern.matches(in: string) + XCTAssertEqual(results.count, 3) + + do { + let result = results[0] + XCTAssertEqual(result.numberOfRanges, 4) + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#abc") + } + + do { + let result = results[1] + XCTAssertEqual(result.numberOfRanges, 4) + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#def") + } + + do { + let result = results[2] + XCTAssertEqual(result.numberOfRanges, 4) + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(2)), "#ghi") + } + } + } +} diff --git a/Tests/TweetupKitTests/SpeakerTests.swift b/Tests/TweetupKitTests/SpeakerTests.swift new file mode 100644 index 0000000..4439dbf --- /dev/null +++ b/Tests/TweetupKitTests/SpeakerTests.swift @@ -0,0 +1,485 @@ +import XCTest +@testable import TweetupKit + +import Foundation + +class SpeakerTests: XCTestCase { + var twitterCredential: OAuthCredential? + var githubToken: String? + + override func setUp() { + super.setUp() + + do { + twitterCredential = try loadTwitterCredential() + githubToken = try loadGithubToken() + } catch let error { + XCTFail("\(error)") + } + } + + override func tearDown() { + twitterCredential = nil + githubToken = nil + super.tearDown() + } + + func testPostTweets() { + do { + let speaker = Speaker(twitterCredential: twitterCredential, githubToken: githubToken, outputDirectoryPath: imageDirectoryPath) + + let start = Date.timeIntervalSinceReferenceDate + + 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 tweets = try! Tweet.tweets(from: string) + speaker.post(tweets: tweets, with: 5.0) { getIds in + defer { + expectation.fulfill() + } + do { + let ids = try getIds() + XCTAssertEqual(ids.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) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 14.0, handler: nil) + + let end = Date.timeIntervalSinceReferenceDate + + XCTAssertGreaterThan(end - start, 10.0) + } + + do { // error duraing posting tweets + let speaker = Speaker(twitterCredential: twitterCredential, githubToken: githubToken) + + let start = Date.timeIntervalSinceReferenceDate + + 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 tweets = try! Tweet.tweets(from: string) + speaker.post(tweets: tweets, with: 10.0) { getIds in + defer { + expectation.fulfill() + } + do { + _ = try getIds() + XCTFail() + } catch let error { + print(error) + } + } + + waitForExpectations(timeout: 15.0, handler: nil) + + let end = Date.timeIntervalSinceReferenceDate + + XCTAssertGreaterThan(end - start, 10.0) + } + } + + func testResolveImages() { + do { + let speaker = Speaker(twitterCredential: twitterCredential) + + 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 tweets = try! Tweet.tweets(from: string) + speaker.resolveImages(of: tweets) { getTweets in + defer { + expectation.fulfill() + } + do { + let results = try getTweets() + + XCTAssertEqual(results.count, 6) + + do { + let result = results[0] + XCTAssertEqual(result, tweets[0]) + } + + do { + let result = results[1] + XCTAssertEqual(result, tweets[1]) + } + + do { + let result = results[2] + + guard case let .some(.image(image)) = result.attachment, case let .twitter(id) = image.source else { + XCTFail() + return + } + XCTAssertEqual(result, try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!", attachment: .image(Image(alternativeText: "", source: .twitter(id))))) + } + + do { + let result = results[3] + + guard case let .some(.image(image)) = result.attachment, case let .twitter(id) = image.source else { + 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))))) + } + + do { + let result = results[4] + + guard case let .some(.image(image)) = result.attachment, case let .twitter(id) = image.source else { + 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))))) + } + + do { + let result = results[5] + + guard case let .some(.image(image)) = result.attachment, case let .twitter(id) = image.source else { + 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))))) + } + + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 90.0, handler: nil) + } + } + + func testResolveImage() { + guard let twitterCredential = twitterCredential else { return } + + do { + let speaker = Speaker(twitterCredential: twitterCredential) + + do { + 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 + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + XCTAssertEqual(result, tweet) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + + 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)))) + speaker.resolveImage(of: tweet) { getTweet in + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + guard case let .some(.image(image)) = result.attachment, case let .twitter(id) = image.source else { + 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))))) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + } + + do { // no token + let speaker = Speaker() + + do { + 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 + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + XCTAssertEqual(result, tweet) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + + 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)))) + speaker.resolveImage(of: tweet) { getTweet in + defer { + expectation.fulfill() + } + do { + _ = try getTweet() + XCTFail() + } catch SpeakerError.noTwitterCredential { + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + } + + do { // base directory + let speaker = Speaker(twitterCredential: twitterCredential, baseDirectoryPath: imageDirectoryPath) + + 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")))) + speaker.resolveImage(of: tweet) { getTweet in + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + guard case let .some(.image(image)) = result.attachment, case let .twitter(id) = image.source else { + 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))))) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + } + } + + func testResolveCodes() { + guard let githubToken = githubToken else { return } + + do { + let speaker = Speaker(githubToken: githubToken) + + 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 tweets = try! Tweet.tweets(from: string) + speaker.resolveCodes(of: tweets) { getTweets in + defer { + expectation.fulfill() + } + do { + let results = try getTweets() + + XCTAssertEqual(results.count, 6) + + do { + let result = results[0] + XCTAssertEqual(result, tweets[0]) + } + + do { + let result = results[1] + + guard case let .some(.image(image)) = result.attachment, case let .gist(id) = image.source else { + 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))))) + } + + do { + let result = results[2] + XCTAssertEqual(result, tweets[2]) + } + + do { + let result = results[3] + + guard case let .some(.image(image)) = result.attachment, case let .gist(id) = image.source else { + 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))))) + } + + do { + let result = results[4] + + guard case let .some(.image(image)) = result.attachment, case let .gist(id) = image.source else { + 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))))) + } + + do { + let result = results[5] + + guard case let .some(.image(image)) = result.attachment, case let .gist(id) = image.source else { + 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))))) + } + + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 30.0, handler: nil) + } + } + + func testResolveCode() { + guard let githubToken = githubToken else { return } + + do { + let speaker = Speaker(githubToken: githubToken) + + do { + 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 + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + XCTAssertEqual(result, tweet) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + + 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)!\")"))) + speaker.resolveCode(of: tweet) { getTweet in + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + guard case let .some(.image(image)) = result.attachment, case let .gist(id) = image.source else { + 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))))) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + } + + do { // no token + let speaker = Speaker() + + do { + 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 + defer { + expectation.fulfill() + } + do { + let result = try getTweet() + XCTAssertEqual(result, tweet) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + + 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)!\")"))) + speaker.resolveCode(of: tweet) { getTweet in + defer { + expectation.fulfill() + } + do { + _ = try getTweet() + XCTFail() + } catch SpeakerError.noGithubToken { + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + } + } + + func testImagePath() { + do { + let path = "path/to/image" + let from = "base/dir" + let result = Speaker.imagePath(path, from: from) + XCTAssertEqual(result, "base/dir/path/to/image") + } + + do { + let path = "path/to/image" + let from = "/base/dir" + let result = Speaker.imagePath(path, from: from) + XCTAssertEqual(result, "/base/dir/path/to/image") + } + + do { + let path = "/path/to/image" + let from = "base/dir" + let result = Speaker.imagePath(path, from: from) + XCTAssertEqual(result, "/path/to/image") + } + + do { + let path = "path/to/image" + let from: String? = nil + let result = Speaker.imagePath(path, from: from) + XCTAssertEqual(result, "path/to/image") + } + } +} diff --git a/Tests/TweetupKitTests/TweetTests.swift b/Tests/TweetupKitTests/TweetTests.swift new file mode 100644 index 0000000..2248376 --- /dev/null +++ b/Tests/TweetupKitTests/TweetTests.swift @@ -0,0 +1,154 @@ +import XCTest +@testable import TweetupKit + +class TweetTests: XCTestCase { + func testInit() { + do { + let result = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") + XCTAssertEqual(result.body, "Twinkle, twinkle, little star,\nHow I wonder what you are!") + } + + do { // empty (0) + _ = try Tweet(body: "") + XCTFail() + } catch TweetInitializationError.empty { + } catch { + XCTFail() + } + + do { // 1 + let result = try! Tweet(body: "A") + XCTAssertEqual(result.body, "A") + } + + do { // 140 + let result = try! Tweet(body: "0123456789112345678921234567893123456789412345678951234567896123456789712345678981234567899123456789A123456789B123456789C123456789D123456789") + XCTAssertEqual(result.body, "0123456789112345678921234567893123456789412345678951234567896123456789712345678981234567899123456789A123456789B123456789C123456789D123456789") + } + + do { // too long (141) + _ = try Tweet(body: "0123456789112345678921234567893123456789412345678951234567896123456789712345678981234567899123456789A123456789B123456789C123456789D123456789X") + XCTFail() + } catch let TweetInitializationError.tooLong(body, attachment, length) { + XCTAssertEqual(body, "0123456789112345678921234567893123456789412345678951234567896123456789712345678981234567899123456789A123456789B123456789C123456789D123456789X") + XCTAssertNil(attachment) + XCTAssertEqual(length, 141) + } catch { + XCTFail() + } + + do { // too long with a `.code` + _ = try Tweet(body: "0123456789112345678921234567893123456789412345678951234567896123456789712345678981234567899123456789A123456789B1234X", attachment: .code(Code(language: .swift, fileName: "hello.swift", body: "let name = \"Swift\"\nprint(\"Hello \\(name)!\")"))) + XCTFail() + } catch let TweetInitializationError.tooLong(body, attachment, length) { + XCTAssertEqual(body, "0123456789112345678921234567893123456789412345678951234567896123456789712345678981234567899123456789A123456789B1234X") + guard let attachment = attachment else { XCTFail(); return } + switch attachment { + case let .code(code): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "let name = \"Swift\"\nprint(\"Hello \\(name)!\")") + case .image(_): + XCTFail() + } + XCTAssertEqual(length, 141) + } catch { + XCTFail() + } + } + + func testDescription() { + do { + let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!") + let result = tweet.description + XCTAssertEqual(result, "Twinkle, twinkle, little star,\nHow I wonder what you are!") + } + + do { + 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 result = tweet.description + XCTAssertEqual(result, "Up above the world so high,\nLike a diamond in the sky.\n\n```swift:hello.swift\nlet name = \"Swift\"\nprint(\"Hello \\(name)!\")\n```") + } + + do { + let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!", attachment: .image(Image(alternativeText: "", source: .local("path/to/image.png")))) + let result = tweet.description + XCTAssertEqual(result, "Twinkle, twinkle, little star,\nHow I wonder what you are!\n\n![](path/to/image.png)") + } + } + + func testLength() { + do { + let tweet = try! Tweet(body: "A") + let result = tweet.length + XCTAssertEqual(result, 1) + } + + do { // new lines + let tweet = try! Tweet(body: "A\nB\nC") + let result = tweet.length + XCTAssertEqual(result, 5) + } + + do { // Japanese + let tweet = try! Tweet(body: "あいうえお山川空") + let result = tweet.length + XCTAssertEqual(result, 8) + } + + do { // 16 for Twitter, 1 for Swift + let tweet = try! Tweet(body: "🇬🇧🇨🇦🇫🇷🇩🇪🇮🇹🇯🇵🇷🇺🇺🇸") + let result = tweet.length + XCTAssertEqual(result, 16) + } + + do { // http + let tweet = try! Tweet(body: "http://qaleido.space") + let result = tweet.length + XCTAssertEqual(result, 23) + } + + do { // https + let tweet = try! Tweet(body: "https://swift-tweets.github.io") + let result = tweet.length + XCTAssertEqual(result, 23) + } + + do { // mixed + let tweet = try! Tweet(body: "Twinkle, twinkle, little star, http://qaleido.space How I wonder what you are! https://swift-tweets.github.io/?foo=bar&baz=qux#tweeters") + let result = tweet.length + XCTAssertEqual(result, 105) + } + + do { // with a `.code` + 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 result = tweet.length + XCTAssertEqual(result, 79) + } + + do { // with a `.image` + let tweet = try! Tweet(body: "Twinkle, twinkle, little star,\nHow I wonder what you are!", attachment: .image(Image(alternativeText: "", source: .local("path/to/image.png")))) + let result = tweet.length + XCTAssertEqual(result, 57) + } + } + + func testUrlPattern() { + do { + let string = "Twinkle, twinkle, little star, http://qaleido.space How I wonder what you are! https://swift-tweets.github.io/?foo=bar&baz=qux#tweeters" + + let results = Tweet.urlPattern.matches(in: string) + XCTAssertEqual(results.count, 2) + + do { + let result = results[0] + XCTAssertEqual((string as NSString).substring(with: result.rangeAt(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") + } + } + } +} diff --git a/Tests/TweetupKitTests/TweetupKitTests.swift b/Tests/TweetupKitTests/TweetupKitTests.swift new file mode 100644 index 0000000..72fe660 --- /dev/null +++ b/Tests/TweetupKitTests/TweetupKitTests.swift @@ -0,0 +1,40 @@ +import XCTest +@testable import TweetupKit + +import Foundation + +class TweetupKitTests: XCTestCase { + func testSample() { + let string = try! String(contentsOf: URL(string: "https://gist.githubusercontent.com/koher/6707cd98ea3a2c29f58c0fdecbe4825c/raw/428dc616a87a39baf1681c910984a3f53c91378b/sample.tw")!, encoding: .utf8) + let tweets = try! Tweet.tweets(from: string) + XCTAssertEqual(tweets.count, 9) + + do { + let tweet = tweets[5] + switch tweet.attachment { + case let .some(.image(image)): + XCTAssertEqual(image.source, .local("path/to/image/file.png")) + default: + XCTFail() + } + } + + do { + let tweet = tweets[6] + switch tweet.attachment { + case let .some(.code(code)): + XCTAssertEqual(code.language, .swift) + XCTAssertEqual(code.fileName, "hello.swift") + XCTAssertEqual(code.body, "print(\"Hello swift!\")\n") + default: + XCTFail() + } + } + } + + static var allTests : [(String, (TweetupKitTests) -> () throws -> Void)] { + return [ + ("testSample", testSample), + ] + } +} diff --git a/Tests/TweetupKitTests/TwitterTests.swift b/Tests/TweetupKitTests/TwitterTests.swift new file mode 100644 index 0000000..8bdaa1e --- /dev/null +++ b/Tests/TweetupKitTests/TwitterTests.swift @@ -0,0 +1,127 @@ +import XCTest +@testable import TweetupKit + +import Foundation + +class TwitterTests: XCTestCase { + var credential: OAuthCredential? + + override func setUp() { + super.setUp() + + do { + credential = try loadTwitterCredential() + } catch let error { + XCTFail("\(error)") + } + } + + override func tearDown() { + credential = nil + super.tearDown() + } + + func testUpdateStatus() { + guard let credential = credential else { return } + + do { + let expectation = self.expectation(description: "") + + Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", credential: credential) { getId in + defer { + expectation.fulfill() + } + do { + let (id, _) = try getId() + XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: id).count == 1) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 10.0, handler: nil) + } + + do { + let expectation = self.expectation(description: "") + + let data = try! Data(contentsOf: URL(fileURLWithPath: imagePath)) + Twitter.upload(media: data, credential: credential) { getMediaId in + do { + let mediaId = try getMediaId() + Twitter.update(status: "TweetupKitTest: testUpdateStatus at \(Date.timeIntervalSinceReferenceDate)", mediaId: mediaId, credential: credential) { getId in + defer { + expectation.fulfill() + } + do { + let (id, _) = try getId() + XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: id).count == 1) + } catch let error { + XCTFail("\(error)") + } + } + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 40.0, handler: nil) + } + } + + func testUploadMedia() { + guard let credential = credential else { return } + + let expectation = self.expectation(description: "") + + let data = try! Data(contentsOf: URL(fileURLWithPath: imagePath)) + Twitter.upload(media: data, credential: credential) { getId in + defer { + expectation.fulfill() + } + do { + let id = try getId() + XCTAssertTrue(try! NSRegularExpression(pattern: "^[0-9]+$").matches(in: id).count == 1) + } catch let error { + XCTFail("\(error)") + } + } + + waitForExpectations(timeout: 30.0, handler: nil) + } +} + +func loadTwitterCredential() throws -> OAuthCredential { + let path = #file.deletingLastPathComponent.deletingLastPathComponent.appendingPathComponent("twitter.json") + let data: Data + do { + data = try Data(contentsOf: URL(fileURLWithPath: path)) + } catch { + throw GeneralError(message: "Put a file at \(path), which contains tokens of Twitter for the tests in the format same as twitter-template.json in the same directory.") + } + + let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] + + guard let consumerKey = json["consumerKey"] as? String else { + throw GeneralError(message: "Lack of `consumerKey` in \(path).") + } + guard let consumerSecret = json["consumerSecret"] as? String else { + throw GeneralError(message: "Lack of `consumerSecret` in \(path).") + } + guard let oauthToken = json["oauthToken"] as? String else { + throw GeneralError(message: "Lack of `oauthToken` in \(path).") + } + guard let oauthTokenSecret = json["oauthTokenSecret"] as? String else { + throw GeneralError(message: "Lack of `oauthTokenSecret` in \(path).") + } + + return OAuthCredential( + consumerKey: consumerKey, + consumerSecret: consumerSecret, + oauthToken: oauthToken, + oauthTokenSecret: oauthTokenSecret + ) +} + +let imageDirectoryPath = #file.deletingLastPathComponent.deletingLastPathComponent +let imagePath = imageDirectoryPath.appendingPathComponent("image.png") diff --git a/Tests/github-template.json b/Tests/github-template.json new file mode 100644 index 0000000..569291a --- /dev/null +++ b/Tests/github-template.json @@ -0,0 +1,3 @@ +{ + "accessToken": "" +} diff --git a/Tests/image.png b/Tests/image.png new file mode 100644 index 0000000..9290cf0 Binary files /dev/null and b/Tests/image.png differ diff --git a/Tests/twitter-template.json b/Tests/twitter-template.json new file mode 100644 index 0000000..8ec13ec --- /dev/null +++ b/Tests/twitter-template.json @@ -0,0 +1,6 @@ +{ + "consumerKey": "", + "consumerSecret": "", + "oauthToken": "", + "oauthTokenSecret": "" +}