From 4b2d6f4becd2414df48bb37c0727841636d44e88 Mon Sep 17 00:00:00 2001 From: MMP0 <28616020+MMP0@users.noreply.github.com> Date: Thu, 3 Nov 2022 22:47:46 +0900 Subject: [PATCH 1/3] Add support for earlier OS versions + Change stream line separator to CRLF --- Package.swift | 2 +- Sources/AsyncCRLFLineSequence.swift | 75 ++++++++++++++++++ Sources/Polyfill.swift | 118 ++++++++++++++++++++++++++++ Sources/Twift+Authentication.swift | 2 +- Sources/Twift+Search.swift | 8 +- Sources/Twift+Streams.swift | 58 ++++++++------ Sources/Twift+Tweets.swift | 12 +-- Sources/Twift.swift | 2 +- Sources/Types+Stream.swift | 26 ++++++ 9 files changed, 265 insertions(+), 38 deletions(-) create mode 100644 Sources/AsyncCRLFLineSequence.swift create mode 100644 Sources/Polyfill.swift diff --git a/Package.swift b/Package.swift index fa63b8f..65b0f41 100644 --- a/Package.swift +++ b/Package.swift @@ -5,7 +5,7 @@ import PackageDescription let package = Package( name: "Twift", platforms: [ - .macOS(.v12), .iOS(.v15) + .macOS(.v10_15), .iOS(.v13) ], products: [ .library(name: "Twift", targets: ["Twift"]) diff --git a/Sources/AsyncCRLFLineSequence.swift b/Sources/AsyncCRLFLineSequence.swift new file mode 100644 index 0000000..4d7618e --- /dev/null +++ b/Sources/AsyncCRLFLineSequence.swift @@ -0,0 +1,75 @@ +import Foundation + +// AsyncLineSequence but separated by CRLF only +struct AsyncCRLFLineSequence: AsyncSequence where Base.Element == UInt8 { + typealias Element = String + + private let base: Base + + struct AsyncIterator: AsyncIteratorProtocol { + private var byteSource: Base.AsyncIterator + private var buffer = [UInt8]() + + init(underlyingIterator: Base.AsyncIterator) { + byteSource = underlyingIterator + } + + mutating func next() async rethrows -> String? { + let _CR: UInt8 = 0x0D + let _LF: UInt8 = 0x0A + + func yield() -> String? { + defer { + buffer.removeAll(keepingCapacity: true) + } + if buffer.isEmpty { + return nil + } + return String(decoding: buffer, as: UTF8.self) + } + + while let first = try await byteSource.next() { + switch first { + case _CR: + // Try to read: 0D [0A]. + guard let next = try await byteSource.next() else { + buffer.append(first) + return yield() + } + guard next == _LF else { + buffer.append(first) + buffer.append(next) + continue + } + if let result = yield() { + return result + } + default: + buffer.append(first) + } + } + // Don't emit an empty newline when there is no more content (e.g. end of file) + if !buffer.isEmpty { + return yield() + } + return nil + } + } + + func makeAsyncIterator() -> AsyncIterator { + return AsyncIterator(underlyingIterator: base.makeAsyncIterator()) + } + + init(underlyingSequence: Base) { + base = underlyingSequence + } +} + +extension AsyncSequence where Self.Element == UInt8 { + /** + A non-blocking sequence of CRLF-separated `Strings` created by decoding the elements of `self` as UTF8. + */ + var linesCRLF: AsyncCRLFLineSequence { + AsyncCRLFLineSequence(underlyingSequence: self) + } +} diff --git a/Sources/Polyfill.swift b/Sources/Polyfill.swift new file mode 100644 index 0000000..63d479c --- /dev/null +++ b/Sources/Polyfill.swift @@ -0,0 +1,118 @@ +import Foundation + +extension Date { + func _ISO8601Format() -> String { + if #available(iOS 15.0, macOS 12.0, *) { + return ISO8601Format() + } else { + return ISO8601DateFormatter.string(from: self, + timeZone: TimeZone(secondsFromGMT: 0)!, + formatOptions: [.withInternetDateTime]) + } + } +} + +struct _AsyncBytes: AsyncSequence { + typealias Element = UInt8 + + private let makeUnderlyingIterator: () -> AsyncIterator + + init(_ underlyingIterator: I) where I.Element == UInt8 { + makeUnderlyingIterator = { AsyncIterator(underlyingIterator) } + } + + public func makeAsyncIterator() -> AsyncIterator { + return makeUnderlyingIterator() + } + + struct AsyncIterator: AsyncIteratorProtocol { + private let _next: () async throws -> Element? + + init(_ base: I) where I.Element == Element { + var iterator = base + _next = { try await iterator.next() } + } + + public func next() async throws -> Element? { + return try await _next() + } + } +} + +extension _AsyncBytes { + static func bytes(for request: URLRequest) async throws -> (_AsyncBytes, URLResponse) { + return try await _URLSessionAsyncBytesDelegate().bytes(for: request) + } +} + +private class _URLSessionAsyncBytesDelegate: NSObject, URLSessionDataDelegate { + private var responseContinuation: CheckedContinuation! + + private let stream: AsyncThrowingStream + private let streamContinuation: AsyncThrowingStream.Continuation + + override init() { + var continuation: AsyncThrowingStream.Continuation! + stream = AsyncThrowingStream { continuation = $0 } + streamContinuation = continuation + + super.init() + } + + func bytes(for request: URLRequest) async throws -> (_AsyncBytes, URLResponse) { + let response = try await withCheckedThrowingContinuation { continuation in + responseContinuation = continuation + + let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) + streamContinuation.onTermination = { @Sendable _ in session.invalidateAndCancel() } + session.dataTask(with: request).resume() + } + let iterator = AsyncIterator(stream.makeAsyncIterator()) { [streamContinuation] in + streamContinuation.finish() + } + return (_AsyncBytes(iterator), response) + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if task.response == nil, let error = error { + // Client-side errors + responseContinuation.resume(throwing: error) + } + streamContinuation.finish(throwing: error) + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition { + responseContinuation.resume(returning: response) + return .allow + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + for b in data { + streamContinuation.yield(b) + } + } + + struct AsyncIterator: AsyncIteratorProtocol { + private var base: AsyncThrowingStream.AsyncIterator + private let token: Token + + init(_ underlyingIterator: AsyncThrowingStream.AsyncIterator, onDeinit: @escaping () -> Void) { + base = underlyingIterator + token = Token(onDeinit: onDeinit) + } + + mutating func next() async throws -> UInt8? { + return try await base.next() + } + + private final class Token { + private let onDeinit: () -> Void + + init(onDeinit: @escaping () -> Void) { + self.onDeinit = onDeinit + } + + deinit { onDeinit() } + } + } +} diff --git a/Sources/Twift+Authentication.swift b/Sources/Twift+Authentication.swift index 0294868..40668cc 100644 --- a/Sources/Twift+Authentication.swift +++ b/Sources/Twift+Authentication.swift @@ -270,7 +270,7 @@ public struct OAuth2User: Codable { /// Whether or not the access token has expired (i.e. whether `expiresAt` is in the past). public var expired: Bool { - expiresAt < .now + expiresAt.timeIntervalSinceNow < 0 } internal enum CodingKeys: String, CodingKey { diff --git a/Sources/Twift+Search.swift b/Sources/Twift+Search.swift index 22a6622..d2a27a2 100644 --- a/Sources/Twift+Search.swift +++ b/Sources/Twift+Search.swift @@ -31,8 +31,8 @@ extension Twift { if let nextToken = nextToken { queryItems.append(URLQueryItem(name: "next_token", value: nextToken)) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -74,8 +74,8 @@ extension Twift { if let nextToken = nextToken { queryItems.append(URLQueryItem(name: "next_token", value: nextToken)) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) diff --git a/Sources/Twift+Streams.swift b/Sources/Twift+Streams.swift index 31a70b6..e8e8c29 100644 --- a/Sources/Twift+Streams.swift +++ b/Sources/Twift+Streams.swift @@ -12,11 +12,11 @@ extension Twift { /// - backfillMinutes: By passing this parameter, you can request up to five (5) minutes worth of streaming data that you might have missed during a disconnection to be delivered to you upon reconnection. The backfilled Tweets will automatically flow through the reconnected stream, with older Tweets generally being delivered before any newly matching Tweets. You must include a whole number between 1 and 5 as the value to this parameter. /// This feature will deliver duplicate Tweets, meaning that if you were disconnected for 90 seconds, and you requested two minutes of backfill, you will receive 30 seconds worth of duplicate Tweets. Due to this, you should make sure your system is tolerant of duplicate data. /// This feature is currently only available to the Academic Research product track. - /// - Returns: An `AsyncSequence` of `TwitterAPIDataAndIncludes` objects. + /// - Returns: A stream of `TwitterAPIDataAndIncludes` objects. public func volumeStream(fields: Set = [], expansions: [Tweet.Expansions] = [], backfillMinutes: Int? = nil - ) async throws -> AsyncThrowingCompactMapSequence, TwitterAPIDataAndIncludes> { + ) async throws -> Stream> { guard case .appOnly(_) = authenticationType else { throw TwiftError.WrongAuthenticationType(needs: .appOnly) } var queryItems = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -30,17 +30,7 @@ extension Twift { signURLRequest(method: .GET, request: &request) - let (bytes, response) = try await URLSession.shared.bytes(for: request) - - guard let response = response as? HTTPURLResponse, - response.statusCode == 200 else { - throw URLError.init(.resourceUnavailable) - } - - return bytes.lines - .compactMap { - try? await self.decodeOrThrow(decodingType: TwitterAPIDataAndIncludes.self, data: Data($0.utf8)) - } + return try await stream(for: request) } /// Streams Tweets in real-time based on a specific set of filter rules. @@ -52,11 +42,11 @@ extension Twift { /// - backfillMinutes: By passing this parameter, you can request up to five (5) minutes worth of streaming data that you might have missed during a disconnection to be delivered to you upon reconnection. The backfilled Tweets will automatically flow through the reconnected stream, with older Tweets generally being delivered before any newly matching Tweets. You must include a whole number between 1 and 5 as the value to this parameter. /// This feature will deliver duplicate Tweets, meaning that if you were disconnected for 90 seconds, and you requested two minutes of backfill, you will receive 30 seconds worth of duplicate Tweets. Due to this, you should make sure your system is tolerant of duplicate data. /// This feature is currently only available to the Academic Research product track. - /// - Returns: An `AsyncSequence` of `TwitterAPIDataAndIncludes` objects. + /// - Returns: A stream of `TwitterAPIDataAndIncludes` objects. public func filteredStream(fields: Set = [], expansions: [Tweet.Expansions] = [], backfillMinutes: Int? = nil - ) async throws -> AsyncThrowingCompactMapSequence, TwitterAPIDataAndIncludes> { + ) async throws -> Stream> { guard case .appOnly(_) = authenticationType else { throw TwiftError.WrongAuthenticationType(needs: .appOnly) } var queryItems = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -70,17 +60,35 @@ extension Twift { signURLRequest(method: .GET, request: &request) - let (bytes, response) = try await URLSession.shared.bytes(for: request) - - guard let response = response as? HTTPURLResponse, - response.statusCode == 200 else { - throw URLError.init(.resourceUnavailable) - } - - return bytes.lines - .compactMap { - try? await self.decodeOrThrow(decodingType: TwitterAPIDataAndIncludes.self, data: Data($0.utf8)) + return try await stream(for: request) + } + + func stream(for request: URLRequest) async throws -> Stream { + if #available(iOS 15.0, macOS 12.0, *) { + let (bytes, response) = try await URLSession.shared.bytes(for: request) + + guard let response = response as? HTTPURLResponse, + response.statusCode == 200 else { + throw TwiftError.UnknownError(response) } + + return Stream( + bytes.linesCRLF + .compactMap { try? await self.decodeOrThrow(decodingType: T.self, data: Data($0.utf8)) } + ) + } else { + let (bytes, response) = try await _AsyncBytes.bytes(for: request) + + guard let response = response as? HTTPURLResponse, + response.statusCode == 200 else { + throw TwiftError.UnknownError(response) + } + + return Stream( + bytes.linesCRLF + .compactMap { try? await self.decodeOrThrow(decodingType: T.self, data: Data($0.utf8)) } + ) + } } } diff --git a/Sources/Twift+Tweets.swift b/Sources/Twift+Tweets.swift index 57d8c35..f8c9213 100644 --- a/Sources/Twift+Tweets.swift +++ b/Sources/Twift+Tweets.swift @@ -71,8 +71,8 @@ extension Twift { if let exclude = exclude { queryItems.append(URLQueryItem(name: "exclude", value: exclude.map(\.rawValue).joined(separator: ","))) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -111,8 +111,8 @@ extension Twift { if let exclude = exclude { queryItems.append(URLQueryItem(name: "exclude", value: exclude.map(\.rawValue).joined(separator: ","))) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -151,8 +151,8 @@ extension Twift { if let exclude = exclude { queryItems.append(URLQueryItem(name: "exclude", value: exclude.map(\.rawValue).joined(separator: ","))) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) diff --git a/Sources/Twift.swift b/Sources/Twift.swift index 0fae163..21ef077 100644 --- a/Sources/Twift.swift +++ b/Sources/Twift.swift @@ -77,7 +77,7 @@ public class Twift: NSObject, ObservableObject { if dateStr == "string" && isTestEnvironment { print("Test environment detected: simulating date for data decoder") - return .now + return Date() } throw TwiftError.UnknownError("Couldn't decode date from returned data: \(decoder.codingPath.description)") diff --git a/Sources/Types+Stream.swift b/Sources/Types+Stream.swift index ad5db56..59aa715 100644 --- a/Sources/Types+Stream.swift +++ b/Sources/Types+Stream.swift @@ -42,3 +42,29 @@ public struct MutableFilteredStreamRule: Codable { /// The optional tag for this stream rule public var tag: String? } + +/// An asychronous sequence of stream objects. +public struct Stream: AsyncSequence { + private let makeUnderlyingIterator: () -> AsyncIterator + + init(_ base: S) where S.Element == Element { + makeUnderlyingIterator = { AsyncIterator(base.makeAsyncIterator()) } + } + + public func makeAsyncIterator() -> AsyncIterator { + return makeUnderlyingIterator() + } + + public struct AsyncIterator: AsyncIteratorProtocol { + private var _next: () async throws -> Element? + + init(_ base: I) where I.Element == Element { + var iterator = base + _next = { try await iterator.next() } + } + + public func next() async throws -> Element? { + return try await _next() + } + } +} From 72fa27580bc0dd047abf8a827b457f48980e3a83 Mon Sep 17 00:00:00 2001 From: MMP0 <28616020+MMP0@users.noreply.github.com> Date: Thu, 3 Nov 2022 23:07:07 +0900 Subject: [PATCH 2/3] Small fixes --- Sources/Polyfill.swift | 2 +- Sources/Types+Stream.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/Polyfill.swift b/Sources/Polyfill.swift index 63d479c..7eabaa0 100644 --- a/Sources/Polyfill.swift +++ b/Sources/Polyfill.swift @@ -75,7 +75,7 @@ private class _URLSessionAsyncBytesDelegate: NSObject, URLSessionDataDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if task.response == nil, let error = error { - // Client-side errors + // Client-side error responseContinuation.resume(throwing: error) } streamContinuation.finish(throwing: error) diff --git a/Sources/Types+Stream.swift b/Sources/Types+Stream.swift index 59aa715..ff99f3a 100644 --- a/Sources/Types+Stream.swift +++ b/Sources/Types+Stream.swift @@ -56,7 +56,7 @@ public struct Stream: AsyncSequence { } public struct AsyncIterator: AsyncIteratorProtocol { - private var _next: () async throws -> Element? + private let _next: () async throws -> Element? init(_ base: I) where I.Element == Element { var iterator = base From d239b125d45a73d9889c2861e448d8422a593d03 Mon Sep 17 00:00:00 2001 From: MMP0 <28616020+MMP0@users.noreply.github.com> Date: Tue, 22 Nov 2022 11:10:37 +0900 Subject: [PATCH 3/3] Organize file names --- Sources/Date++.swift | 13 +++++++++++++ Sources/Twift+Search.swift | 8 ++++---- Sources/Twift+Tweets.swift | 12 ++++++------ Sources/{Polyfill.swift => _AsyncBytes.swift} | 12 ------------ 4 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 Sources/Date++.swift rename Sources/{Polyfill.swift => _AsyncBytes.swift} (90%) diff --git a/Sources/Date++.swift b/Sources/Date++.swift new file mode 100644 index 0000000..f0a4213 --- /dev/null +++ b/Sources/Date++.swift @@ -0,0 +1,13 @@ +import Foundation + +extension Date { + func ISO8601Format() -> String { + if #available(iOS 15.0, macOS 12.0, *) { + return ISO8601Format(.init()) + } else { + return ISO8601DateFormatter.string(from: self, + timeZone: TimeZone(secondsFromGMT: 0)!, + formatOptions: [.withInternetDateTime]) + } + } +} diff --git a/Sources/Twift+Search.swift b/Sources/Twift+Search.swift index d2a27a2..22a6622 100644 --- a/Sources/Twift+Search.swift +++ b/Sources/Twift+Search.swift @@ -31,8 +31,8 @@ extension Twift { if let nextToken = nextToken { queryItems.append(URLQueryItem(name: "next_token", value: nextToken)) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -74,8 +74,8 @@ extension Twift { if let nextToken = nextToken { queryItems.append(URLQueryItem(name: "next_token", value: nextToken)) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) diff --git a/Sources/Twift+Tweets.swift b/Sources/Twift+Tweets.swift index f8c9213..57d8c35 100644 --- a/Sources/Twift+Tweets.swift +++ b/Sources/Twift+Tweets.swift @@ -71,8 +71,8 @@ extension Twift { if let exclude = exclude { queryItems.append(URLQueryItem(name: "exclude", value: exclude.map(\.rawValue).joined(separator: ","))) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -111,8 +111,8 @@ extension Twift { if let exclude = exclude { queryItems.append(URLQueryItem(name: "exclude", value: exclude.map(\.rawValue).joined(separator: ","))) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) @@ -151,8 +151,8 @@ extension Twift { if let exclude = exclude { queryItems.append(URLQueryItem(name: "exclude", value: exclude.map(\.rawValue).joined(separator: ","))) } if let sinceId = sinceId { queryItems.append(URLQueryItem(name: "since_id", value: sinceId)) } if let untilId = untilId { queryItems.append(URLQueryItem(name: "until_id", value: untilId)) } - if let startTime = startTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } - if let endTime = endTime?._ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } + if let startTime = startTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "start_time", value: startTime)) } + if let endTime = endTime?.ISO8601Format() { queryItems.append(URLQueryItem(name: "end_time", value: endTime)) } let fieldsAndExpansions = fieldsAndExpansions(for: Tweet.self, fields: fields, expansions: expansions) diff --git a/Sources/Polyfill.swift b/Sources/_AsyncBytes.swift similarity index 90% rename from Sources/Polyfill.swift rename to Sources/_AsyncBytes.swift index 7eabaa0..57ea9ad 100644 --- a/Sources/Polyfill.swift +++ b/Sources/_AsyncBytes.swift @@ -1,17 +1,5 @@ import Foundation -extension Date { - func _ISO8601Format() -> String { - if #available(iOS 15.0, macOS 12.0, *) { - return ISO8601Format() - } else { - return ISO8601DateFormatter.string(from: self, - timeZone: TimeZone(secondsFromGMT: 0)!, - formatOptions: [.withInternetDateTime]) - } - } -} - struct _AsyncBytes: AsyncSequence { typealias Element = UInt8