diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift index f879457..810bc85 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToCountSequence.swift @@ -13,7 +13,8 @@ extension AsyncIteratorProtocol { /// If a complete array could not be collected, an error is thrown and the sequence should be considered finished. /// - Parameter count: The number of elements to collect. /// - Returns: A collection with exactly `count` elements, or `nil` if the sequence is finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect(_ count: Int) async throws -> [Element]? { assert(count >= 0, "count must be larger than or equal to 0") return try await collect(min: count, max: count) @@ -25,7 +26,8 @@ extension AsyncIteratorProtocol { /// - Parameter minCount: The minimum number of elements to collect. /// - Parameter maxCount: The maximum number of elements to collect. /// - Returns: A collection with at least `minCount` and at most `maxCount` elements, or `nil` if the sequence is finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect(min minCount: Int = 0, max maxCount: Int) async throws -> [Element]? { precondition(minCount <= maxCount, "maxCount must be larger than or equal to minCount") precondition(minCount >= 0, "minCount must be larger than or equal to 0") @@ -50,7 +52,80 @@ extension AsyncIteratorProtocol { return result } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension AsyncIteratorProtocol where Failure == Never { + /// Asynchronously advances by the specified number of elements, or ends the sequence if there is no next element. + /// + /// If a complete array could not be collected, an error is thrown and the sequence should be considered finished. + /// - Parameter count: The number of elements to collect. + /// - Returns: A collection with exactly `count` elements, or `nil` if the sequence is finished. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect(_ count: Int) async throws(AsyncSequenceReaderError) -> [Element]? { + assert(count >= 0, "count must be larger than or equal to 0") + #if compiler(<6.1.3) || compiler(>=6.2) + return try await collect(min: count, max: count) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + if count == 0 { return [] } + + var result = [Element]() + result.reserveCapacity(count) + + while let next = await _nextIsolated() { + result.append(next) + + if result.count == count { + return result + } + } + + guard !result.isEmpty else { return nil } + + guard result.count >= count else { + throw AsyncSequenceReaderError.insufficientElements(minimum: count, actual: result.count) + } + + return result + #endif + } + /// Asynchronously advances by the specified minimum number of elements, continuing until the specified maximum number of elements, or ends the sequence if there is no next element. + /// + /// If a complete array larger than `minCount` could not be constructed, an error is thrown and the sequence should be considered finished. + /// - Parameter minCount: The minimum number of elements to collect. + /// - Parameter maxCount: The maximum number of elements to collect. + /// - Returns: A collection with at least `minCount` and at most `maxCount` elements, or `nil` if the sequence is finished. + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect(min minCount: Int = 0, max maxCount: Int) async throws(AsyncSequenceReaderError) -> [Element]? { + precondition(minCount <= maxCount, "maxCount must be larger than or equal to minCount") + precondition(minCount >= 0, "minCount must be larger than or equal to 0") + if maxCount == 0 { return [] } + + var result = [Element]() + result.reserveCapacity(minCount) + + while let next = await _nextIsolated() { + result.append(next) + + if result.count == maxCount { + return result + } + } + + guard !result.isEmpty else { return nil } + + guard result.count >= minCount else { + throw AsyncSequenceReaderError.insufficientElements(minimum: minCount, actual: result.count) + } + + return result + } +} + +extension AsyncIteratorProtocol { /// Collect the specified number of elements into a sequence, and transform it using the provided closure. /// /// In this example, an asynchronous sequence of Strings encodes sentences by prefixing each word sequence with a number. @@ -81,11 +156,15 @@ extension AsyncIteratorProtocol { /// - Parameter count: The number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect< + Transformed, + TransformFailure + >( _ count: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { assert(count >= 0, "count must be larger than or equal to 0") return try await collect(min: count, max: count, sequenceTransform: sequenceTransform) } @@ -123,12 +202,16 @@ extension AsyncIteratorProtocol { /// - Parameter maxCount: The maximum number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect< + Transformed, + TransformFailure + >( min minCount: Int = 1, max maxCount: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { /// It is unsafe to read ahead in this case, so exit early if we know we won't need to read. if maxCount == 0 { return nil } assert(minCount >= 1, "minCount must be larger than or equal to 1, or the first value risks getting dropped") @@ -167,11 +250,15 @@ extension AsyncBufferedIterator { /// - Parameter count: The number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect< + Transformed, + TransformFailure + >( _ count: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { assert(count >= 0, "count must be larger than 0") return try await collect(min: count, max: count, sequenceTransform: sequenceTransform) } @@ -207,12 +294,16 @@ extension AsyncBufferedIterator { /// - Parameter maxCount: The maximum number of elements the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence of the specified size that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.insufficientElements` if a complete byte sequence could not be returned by the time the sequence ended. - public mutating func collect( + /// - Throws: ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect< + Transformed, + TransformFailure: Error + >( min minCount: Int = 0, max maxCount: Int, - sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws -> Transformed - ) async throws -> Transformed? { + sequenceTransform: sending (sending AsyncReadUpToCountSequence) async throws(TransformFailure) -> Transformed + ) async throws(TransformFailure) -> Transformed? { try await transform(with: sequenceTransform) { .init($0, minCount: minCount, maxCount: maxCount) } } } diff --git a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift index 377c0d7..24cc90f 100644 --- a/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift +++ b/Sources/AsyncSequenceReader/AsyncReadUpToElementsSequence.swift @@ -7,7 +7,7 @@ // async-sequence-reader-watermark: 7E20A9CAB0604E89B17C6747A34F00C0 // -extension AsyncIteratorProtocol { +extension AsyncIteratorProtocol where Element: Equatable { /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. /// /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. @@ -33,11 +33,12 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToIncluding termination: Element, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { try await collect(upToIncluding: [termination], throwsIfOver: maximumBufferSize) } @@ -66,11 +67,12 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToIncluding termination: some Collection, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { precondition(!termination.isEmpty, "termination must not be empty") var result = [Element]() @@ -116,11 +118,12 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToExcluding termination: Element, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { try await collect(upToExcluding: [termination], throwsIfOver: maximumBufferSize) } @@ -149,17 +152,235 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. - /// - Throws: `AsyncSequenceReaderError.terminationNotFound` if a complete byte sequence could not be returned by the time the sequence ended. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable public mutating func collect( upToExcluding termination: some Collection, throwsIfOver maximumBufferSize: Int - ) async throws -> [Element]? where Element: Equatable { + ) async throws -> [Element]? { try await collect(upToIncluding: termination, throwsIfOver: maximumBufferSize)?.dropLast(termination.count) } } -extension AsyncIteratorProtocol { +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension AsyncIteratorProtocol where Element: Equatable, Failure == Never { + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToIncluding: " ", throwsIfOver: 100)) + /// .map { String($0.dropLast()) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The element marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect( + upToIncluding termination: Element, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? { + #if compiler(<6.1.3) || compiler(>=6.2) + try await collect(upToIncluding: [termination], throwsIfOver: maximumBufferSize) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + let termination = [termination] + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + #endif + } + + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, including the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToIncluding: [" "], throwsIfOver: 100)) + /// .map { String($0.dropLast()) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect( + upToIncluding termination: some Collection, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? { + precondition(!termination.isEmpty, "termination must not be empty") + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + } + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, excluding the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToExcluding: " ", throwsIfOver: 100)) + /// .map { String($0) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The element marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect( + upToExcluding termination: Element, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? { + #if compiler(<6.1.3) || compiler(>=6.2) + try await collect(upToExcluding: [termination], throwsIfOver: maximumBufferSize) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + let termination = [termination] + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result.dropLast(termination.count) + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + #endif + } + + /// Collect elements into a sequence until the termination sequence is encountered, and return them as an array, excluding the termination sequence. + /// + /// If the termination sequence was not detected before the end of the stream, or more than the specified maximum elements are read, an error will be thrown. + /// + /// In this example, an asynchronous sequence of Characters represents a list of words. + /// + /// The closure provided to the `iteratorMap(_:)` reads characters up to and inluding the termination provided, splitting the sequence into an array of words. + /// + /// ```swift + /// let dataStream = ... // "apple orange banana kiwi kumquat pear pineapple " + /// + /// let wordStream = dataStream.iteratorMap { iterator -> String? in + /// (try await iterator.collect(upToExcluding: [" "], throwsIfOver: 100)) + /// .map { String($0) } + /// } + /// + /// for await word in wordStream { + /// print("\"\(word)\"", terminator: ", ") + /// } + /// // Prints: "apple", "orange", "banana", "kiwi", "kumquat", "pear", "pineapple", + /// ``` + /// + /// - Parameter termination: The sequence of elements marking the end of the sequence that will be collected. + /// - Parameter throwsIfOver: The maximum amount of elements that will be read before an error is thrown if a termination is not detected. + /// - Returns: An array of the collected elements, or `nil` if the sequence was already finished. + /// - Throws: ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` if a complete byte sequence could not be returned by the time the sequence ended. + @inlinable + public mutating func collect( + upToExcluding termination: some Collection, + throwsIfOver maximumBufferSize: Int + ) async throws(AsyncSequenceReaderError) -> [Element]? { + #if compiler(<6.1.3) || compiler(>=6.2) + try await collect(upToIncluding: termination, throwsIfOver: maximumBufferSize)?.dropLast(termination.count) + #else /// The above crashes the Swift 6.1.3 compiler. Make sure to inline it manually: + var result = [Element]() + + while let next = await _nextIsolated() { + if result.count == maximumBufferSize { + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count + 1) + } + + result.append(next) + + if result.suffix(termination.count).elementsEqual(termination) { + return result.dropLast(termination.count) + } + } + + guard !result.isEmpty else { return nil } + + throw AsyncSequenceReaderError.terminationNotFound(maximum: maximumBufferSize, actual: result.count) + #endif + } +} + +extension AsyncIteratorProtocol where Element: Equatable { /// Collect elements into a sequence until the termination sequence is encountered, and transform it using the provided closure. /// /// - Note: It is up to the caller to verify if the termination sequence was encountered or not, which can easily be done by checking `result.suffix(termination.count).elementsEqual(termination)`. @@ -192,13 +413,14 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The element marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect< Transformed, TransformFailure: Error >( upToIncluding termination: Element, sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws(TransformFailure) -> Transformed - ) async throws(TransformFailure) -> Transformed? where Element: Equatable { + ) async throws(TransformFailure) -> Transformed? { try await collect(upToIncluding: [termination], sequenceTransform: sequenceTransform) } @@ -234,6 +456,7 @@ extension AsyncIteratorProtocol { /// - Parameter termination: The sequence of elements marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect< TerminationCollection: Collection, Transformed, @@ -241,7 +464,7 @@ extension AsyncIteratorProtocol { >( upToIncluding termination: TerminationCollection, sequenceTransform: sending (AsyncReadUpToElementsSequence) async throws(TransformFailure) -> Transformed - ) async throws(TransformFailure) -> Transformed? where Element: Equatable { + ) async throws(TransformFailure) -> Transformed? { try await transform(with: sequenceTransform) { .init($0, termination: termination) } } } @@ -279,6 +502,7 @@ extension AsyncBufferedIterator where Element: Equatable { /// - Parameter termination: The element marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect( upToIncluding termination: Element, sequenceTransform: sending (AsyncReadUpToElementsSequence>) async throws -> Transformed @@ -318,6 +542,7 @@ extension AsyncBufferedIterator where Element: Equatable { /// - Parameter termination: The sequence of elements marking the end of the sequence the `sequenceTransform` closure will have access to. /// - Parameter sequenceTransform: A transformation that accepts a sequence containing elements up to the termination that can be read from, or stopped prematurely by returning early. The receiving iterator will have moved forward by the same amount of items consumed within `sequenceTransform`. /// - Returns: A transformed value as returned by `sequenceTransform`, or `nil` if the sequence was already finished. + @inlinable public mutating func collect< TerminationCollection: Collection, Transformed diff --git a/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md b/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md index 2a23942..b045e31 100644 --- a/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md +++ b/Sources/AsyncSequenceReader/AsyncSequenceReader.docc/AsyncSequenceReader.md @@ -72,7 +72,7 @@ var limitedSequence = try await iterator.collect(min: 128, max: 256) // Array of For that last example, do note that the `limitedSequence` will only become available if and when all the bytes have been read. ie. you will not get results back if only 128 bytes are available _right now_, if the sequence is still ongoing. -If the minimum number of bytes cannot be collected, an `AsyncSequenceReaderError.insufficientElements` error will be thrown. +If the minimum number of bytes cannot be collected, an ``AsyncSequenceReaderError/insufficientElements(minimum:actual:)`` error will be thrown. You can also collect elements into another async sequence using a **sequence transform**: @@ -104,7 +104,7 @@ var httpHeaderEntry = try await iterator.collect(upToExcluding: ["\r".asciiValue This is especially useful when scanning for strings or other known boundaries, allowing you get get an array of elements either including or excluding the terminator you specified. -Note how a ``throwsIfOver`` parameter is necessary — this is to prevent un-bounded reads from running out of control. If the terminator is not detected, or your maximum element allowance has been reached, an ``AsyncSequenceReaderError/terminationNotFound`` error will be thrown. +Note how a `throwsIfOver` parameter is necessary — this is to prevent un-bounded reads from running out of control. If the terminator is not detected, or your maximum element allowance has been reached, an ``AsyncSequenceReaderError/terminationNotFound(maximum:actual:)`` error will be thrown. You can bypass the `throwsIfOver` parameter if you use a **sequence transform** instead, which may be a better option if your algorithm deals with large amounts of data. If you stop reading early, elements can still be read by subsequent requests, giving you more control over how to read your data. diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift index 2b0f8e1..9712099 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToCountSequenceTests.swift @@ -221,7 +221,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:175: Assertion failed: count must be larger than 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:262: Assertion failed: count must be larger than 0") #endif } @@ -236,7 +236,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:240: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:331: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -251,7 +251,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:240: Precondition failed: minCount must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:331: Precondition failed: minCount must be larger than or equal to 0") #endif } @@ -266,7 +266,7 @@ import Testing }.reduce(into: [], { $0.append($1) }) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:239: Precondition failed: maxCount must be larger than or equal to minCount") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:330: Precondition failed: maxCount must be larger than or equal to minCount") #endif } @@ -280,7 +280,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:89: Assertion failed: count must be larger than or equal to 0") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:168: Assertion failed: count must be larger than or equal to 0") #endif } @@ -292,7 +292,11 @@ import Testing _ = try await iterator.collect(-1) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:18: Assertion failed: count must be larger than or equal to 0") + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:19: Assertion failed: count must be larger than or equal to 0") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:67: Assertion failed: count must be larger than or equal to 0") + #endif #endif } @@ -306,7 +310,7 @@ import Testing try await sequence.reduce(into: "") { $0 += ($0.isEmpty ? "" : " ") + String($1) } } } - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -320,7 +324,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -332,7 +336,11 @@ import Testing _ = try await iterator.collect(min: -1, max: 1) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:31: Precondition failed: minCount must be larger than or equal to 0") + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:33: Precondition failed: minCount must be larger than or equal to 0") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:104: Precondition failed: minCount must be larger than or equal to 0") + #endif #endif } @@ -346,7 +354,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -358,7 +366,11 @@ import Testing _ = try await iterator.collect(min: -1, max: -1) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:31: Precondition failed: minCount must be larger than or equal to 0") + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:33: Precondition failed: minCount must be larger than or equal to 0") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:104: Precondition failed: minCount must be larger than or equal to 0") + #endif #endif } @@ -372,7 +384,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:134: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:217: Assertion failed: minCount must be larger than or equal to 1, or the first value risks getting dropped") #endif } @@ -384,7 +396,11 @@ import Testing _ = try await iterator.collect(min: 0, max: -1) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:30: Precondition failed: maxCount must be larger than or equal to minCount") + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:32: Precondition failed: maxCount must be larger than or equal to minCount") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToCountSequence.swift:103: Precondition failed: maxCount must be larger than or equal to minCount") + #endif #endif } diff --git a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift index 66a0d4b..c8d359f 100644 --- a/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift +++ b/Tests/AsyncSequenceReaderTests/AsyncReadUpToElementsSequenceTests.swift @@ -158,7 +158,11 @@ import Testing let _ = try await inputSequence.collect(upToIncluding: "", throwsIfOver: 10) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:74: Precondition failed: termination must not be empty") + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:76: Precondition failed: termination must not be empty") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:253: Precondition failed: termination must not be empty") + #endif #endif } @@ -309,7 +313,11 @@ import Testing let _ = try await inputSequence.collect(upToExcluding: "", throwsIfOver: 10) } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:74: Precondition failed: termination must not be empty") + #if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:76: Precondition failed: termination must not be empty") + #else + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:253: Precondition failed: termination must not be empty") + #endif #endif } @@ -363,7 +371,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:350: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:575: Precondition failed: termination must not be empty") #endif } @@ -431,7 +439,7 @@ import Testing } } #if DEBUG - #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:350: Precondition failed: termination must not be empty") + #expect(result?.standardErrorUTF8Lines.first == "AsyncSequenceReader/AsyncReadUpToElementsSequence.swift:575: Precondition failed: termination must not be empty") #endif } }