-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for earlier OS versions + Change stream line separator to CRLF #59
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
import Foundation | ||
|
||
// AsyncLineSequence but separated by CRLF only | ||
struct AsyncCRLFLineSequence<Base: AsyncSequence>: 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<Self> { | ||
AsyncCRLFLineSequence(underlyingSequence: self) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
import Foundation | ||
|
||
extension Date { | ||
func _ISO8601Format() -> String { | ||
if #available(iOS 15.0, macOS 12.0, *) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can wrap functions & structs in available (by using E.G. Change this: extension Date {
func _ISO8601Format() -> String {
if #available(iOS 15.0, macOS 12.0, *) {
…
}
}
} to this: @available(iOS 15.0, macOS 12.0, *)
extension Date {
func ISO8601Format() -> String {
… // only your custom implementation here; no need to call out to `ISO8601Format()`
}
} |
||
return ISO8601Format() | ||
} else { | ||
return ISO8601DateFormatter.string(from: self, | ||
timeZone: TimeZone(secondsFromGMT: 0)!, | ||
formatOptions: [.withInternetDateTime]) | ||
} | ||
} | ||
} | ||
|
||
struct _AsyncBytes: AsyncSequence { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Likewise for |
||
typealias Element = UInt8 | ||
|
||
private let makeUnderlyingIterator: () -> AsyncIterator | ||
|
||
init<I: AsyncIteratorProtocol>(_ 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<I: AsyncIteratorProtocol>(_ 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<URLResponse, Error>! | ||
|
||
private let stream: AsyncThrowingStream<UInt8, Error> | ||
private let streamContinuation: AsyncThrowingStream<UInt8, Error>.Continuation | ||
|
||
override init() { | ||
var continuation: AsyncThrowingStream<UInt8, Error>.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 error | ||
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<UInt8, Error>.AsyncIterator | ||
private let token: Token | ||
|
||
init(_ underlyingIterator: AsyncThrowingStream<UInt8, Error>.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() } | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Personally, I would put extensions & polyfills each in their own file based on what type they're extending, e.g.
DateExtensions.swift
(common Swift style),Date+TwiftExtensions.swift
(older Obj-C style), or following the existing naming of files in Twift,Date++.swift
, or maybeDate+Polyfill.swift
considering the use-case.