From 846e5f3a3b88167986f4573e9ae94a29d366a70e Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 14 Dec 2023 16:07:30 +0100 Subject: [PATCH 01/18] [WIP] Event streams sequences --- .../EventStreams/JSONLinesDecoding.swift | 134 ++++++++++++++++++ .../EventStreams/JSONLinesEncoding.swift | 117 +++++++++++++++ .../EventStreams/Test_JSONLinesDecoding.swift | 37 +++++ .../EventStreams/Test_JSONLinesEncoding.swift | 34 +++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 50 ++++++- 5 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift create mode 100644 Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift new file mode 100644 index 00000000..10d24d69 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension JSONLinesDeserializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: JSONLinesDeserializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .noop: + continue + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + func asParsedJSONLines() -> JSONLinesDeserializationSequence { + .init(upstream: self) + } + + func asDecodedJSONLines( + of eventType: Event.Type = Event.self, + using decoder: JSONDecoder = .init() + ) -> AsyncThrowingMapSequence, Event> { + asParsedJSONLines().map { line in + try decoder.decode(Event.self, from: Data(line)) + } + } +} + +struct JSONLinesDeserializerStateMachine { + + enum State { + case waitingForDelimiter([UInt8]) + case finished + case mutating + } + private(set) var state: State = .waitingForDelimiter([]) + + enum NextAction { + case returnNil + case returnLine(ArraySlice) + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .waitingForDelimiter(var buffer): + state = .mutating + guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { + state = .waitingForDelimiter(buffer) + return .needsMore + } + let line = buffer[..) + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForDelimiter(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForDelimiter(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnLine(line) + } + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift new file mode 100644 index 00000000..1438c453 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension JSONLinesSerializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: JSONLinesSerializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnBytes(let bytes): + return bytes + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + + func asSerializedJSONLines() -> JSONLinesSerializationSequence { + .init(upstream: self) + } +} + +extension AsyncSequence where Element: Encodable { + func asEncodedJSONLines( + using encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + return encoder + }() + ) -> JSONLinesSerializationSequence>> { + map { event in + try ArraySlice(encoder.encode(event)) + } + .asSerializedJSONLines() + } +} + +struct JSONLinesSerializerStateMachine { + + enum State { + case running + case finished + } + private(set) var state: State = .running + + enum NextAction { + case returnNil + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .running: + return .needsMore + case .finished: + return .returnNil + } + } + + enum ReceivedValueAction { + case returnNil + case returnBytes(ArraySlice) + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer = value + buffer.append(ASCII.lf) + return .returnBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: + preconditionFailure("Invalid state") + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift new file mode 100644 index 00000000..2bd353e2 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_JSONLinesDecoding: Test_Runtime { + + func testParsed() async throws { + let sequence = testJSONLinesOneBytePerElementSequence.asParsedJSONLines() + let lines = try await [ArraySlice](collecting: sequence) + XCTAssertEqual( + lines, + [ + ArraySlice(#"{"name":"Rover"}"#.utf8), + ArraySlice(#"{"name":"Pancake"}"#.utf8) + ] + ) + } + + func testTyped() async throws { + let sequence = testJSONLinesOneBytePerElementSequence.asDecodedJSONLines(of: TestPet.self) + let events = try await [TestPet](collecting: sequence) + XCTAssertEqual(events, testEvents) + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift new file mode 100644 index 00000000..00291401 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_JSONLinesEncoding: Test_Runtime { + + func testSerialized() async throws { + let sequence = WrappedSyncSequence( + sequence: [ + ArraySlice(#"{"name":"Rover"}"#.utf8), + ArraySlice(#"{"name":"Pancake"}"#.utf8) + ] + ).asSerializedJSONLines() + try await XCTAssertEqualAsyncData(sequence, testJSONLinesBytes) + } + + func testTyped() async throws { + let sequence = testEventsAsyncSequence.asEncodedJSONLines() + try await XCTAssertEqualAsyncData(sequence, testJSONLinesBytes) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 0d7d108e..6b69bbc4 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -114,6 +114,27 @@ class Test_Runtime: XCTestCase { var testStructURLFormData: Data { Data(testStructURLFormString.utf8) } + var testEvents: [TestPet] { [.init(name: "Rover"), .init(name: "Pancake")] } + + var testEventsAsyncSequence: WrappedSyncSequence<[TestPet]> { + WrappedSyncSequence(sequence: testEvents) + } + + var testJSONLinesBytes: ArraySlice { + let encoder = JSONEncoder() + let bytes = try! testEvents.map { try encoder.encode($0) }.joined(separator: [0x0a]) + [0x0a] + return ArraySlice(bytes) + } + + var testJSONLinesOneBytePerElementSequence: HTTPBody { + HTTPBody( + WrappedSyncSequence(sequence: testJSONLinesBytes) + .map { ArraySlice([$0]) }, + length: .known(Int64(testJSONLinesBytes.count)), + iterationBehavior: .multiple + ) + } + @discardableResult func _testPrettyEncoded(_ value: Value, expectedJSON: String) throws -> String { let encoder = JSONEncoder() @@ -395,17 +416,38 @@ public func XCTAssertEqualData( } /// Asserts that the data matches the expected value. -public func XCTAssertEqualData( - _ expression1: @autoclosure () throws -> HTTPBody?, +public func XCTAssertEqualAsyncData( + _ expression1: @autoclosure () throws -> AS?, _ expression2: @autoclosure () throws -> C, _ message: @autoclosure () -> String = "Data doesn't match.", file: StaticString = #filePath, line: UInt = #line -) async throws where C.Element == UInt8 { +) async throws where C.Element == UInt8, AS.Element == ArraySlice { guard let actualBytesBody = try expression1() else { XCTFail("First value is nil", file: file, line: line) return } - let actualBytes = try await [UInt8](collecting: actualBytesBody, upTo: .max) + let actualBytes = try await [ArraySlice](collecting: actualBytesBody).flatMap { $0 } XCTAssertEqualData(actualBytes, try expression2(), file: file, line: line) } + +/// Asserts that the data matches the expected value. +public func XCTAssertEqualData( + _ expression1: @autoclosure () throws -> HTTPBody?, + _ expression2: @autoclosure () throws -> C, + _ message: @autoclosure () -> String = "Data doesn't match.", + file: StaticString = #filePath, + line: UInt = #line +) async throws where C.Element == UInt8 { + try await XCTAssertEqualAsyncData(try expression1(), try expression2(), file: file, line: line) +} + +extension Array { + init(collecting source: Source) async throws where Source.Element == Element { + var elements: [Element] = [] + for try await element in source { + elements.append(element) + } + self = elements + } +} From 15462a641d1d126c60ee3a155b482b833fad15c0 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 14 Dec 2023 17:23:44 +0100 Subject: [PATCH 02/18] Add JSON Sequence --- .../EventStreams/JSONSequenceDecoding.swift | 190 ++++++++++++++++++ .../EventStreams/JSONSequenceEncoding.swift | 120 +++++++++++ .../Multipart/ByteUtilities.swift | 3 + .../Test_JSONSequenceDecoding.swift | 33 +++ .../Test_JSONSequenceEncoding.swift | 34 ++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 31 ++- 6 files changed, 403 insertions(+), 8 deletions(-) create mode 100644 Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift create mode 100644 Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift new file mode 100644 index 00000000..ff415032 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift @@ -0,0 +1,190 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct JSONSequenceDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension JSONSequenceDeserializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct DeserializerError: Swift.Error, CustomStringConvertible, LocalizedError { + + let error: JSONSequenceDeserializerStateMachine.ActionError + + var description: String { + switch error { + case .missingInitialRS: + return "Missing an initial character, the bytes might not be a JSON Sequence." + } + } + + var errorDescription: String? { description } + } + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: JSONSequenceDeserializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .returnEvent(let line): + return line + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnEvent(let line): + return line + case .noop: + continue + } + case .emitError(let error): + throw DeserializerError(error: error) + case .noop: + continue + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + func asParsedJSONSequence() -> JSONSequenceDeserializationSequence { + .init(upstream: self) + } + + func asDecodedJSONSequence( + of eventType: Event.Type = Event.self, + using decoder: JSONDecoder = .init() + ) -> AsyncThrowingMapSequence, Event> { + asParsedJSONSequence().map { line in + try decoder.decode(Event.self, from: Data(line)) + } + } +} + +struct JSONSequenceDeserializerStateMachine { + + enum State { + case initial([UInt8]) + case parsingEvent([UInt8]) + case finished + case mutating + } + private(set) var state: State = .initial([]) + + enum ActionError { + case missingInitialRS + } + + enum NextAction { + case returnNil + case returnEvent(ArraySlice) + case emitError(ActionError) + case needsMore + case noop + } + + mutating func next() -> NextAction { + switch state { + case .initial(var buffer): + guard !buffer.isEmpty else { + return .needsMore + } + guard buffer[0] == ASCII.rs else { + return .emitError(.missingInitialRS) + } + state = .mutating + buffer.removeFirst() + state = .parsingEvent(buffer) + return .noop + case .parsingEvent(var buffer): + state = .mutating + guard let indexOfRecordSeparator = buffer.firstIndex(of: ASCII.rs) else { + state = .parsingEvent(buffer) + return .needsMore + } + let event = buffer[..) + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .initial(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .initial(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnEvent(line) + } + } + case .parsingEvent(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .parsingEvent(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnEvent(line) + } + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift new file mode 100644 index 00000000..d717c0ee --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift @@ -0,0 +1,120 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct JSONSequenceSerializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension JSONSequenceSerializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: JSONSequenceSerializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnBytes(let bytes): + return bytes + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + + func asSerializedJSONSequence() -> JSONSequenceSerializationSequence { + .init(upstream: self) + } +} + +extension AsyncSequence where Element: Encodable { + func asEncodedJSONSequence( + using encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + return encoder + }() + ) -> JSONSequenceSerializationSequence>> { + map { event in + try ArraySlice(encoder.encode(event)) + } + .asSerializedJSONSequence() + } +} + +struct JSONSequenceSerializerStateMachine { + + enum State { + case running + case finished + } + private(set) var state: State = .running + + enum NextAction { + case returnNil + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .running: + return .needsMore + case .finished: + return .returnNil + } + } + + enum ReceivedValueAction { + case returnNil + case returnBytes(ArraySlice) + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer: [UInt8] = [] + buffer.reserveCapacity(value.count + 2) + buffer.append(ASCII.rs) + buffer.append(contentsOf: value) + buffer.append(ASCII.lf) + return .returnBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift index 9ae1c6a5..600b6815 100644 --- a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift @@ -24,6 +24,9 @@ enum ASCII { /// The line feed `` character. static let lf: UInt8 = 0x0a + /// The record separator `` character. + static let rs: UInt8 = 0x1e + /// The colon `:` character. static let colon: UInt8 = 0x3a diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift new file mode 100644 index 00000000..b3c77231 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_JSONSequenceDecoding: Test_Runtime { + + func testParsed() async throws { + let sequence = testJSONSequenceOneBytePerElementSequence.asParsedJSONSequence() + let events = try await [ArraySlice](collecting: sequence) + XCTAssertEqual(events.count, 2) + XCTAssertEqualData(events[0], "{\"name\":\"Rover\"}\n".utf8) + XCTAssertEqualData(events[1], "{\"name\":\"Pancake\"}\n".utf8) + } + + func testTyped() async throws { + let sequence = testJSONSequenceOneBytePerElementSequence.asDecodedJSONSequence(of: TestPet.self) + let events = try await [TestPet](collecting: sequence) + XCTAssertEqual(events, testEvents) + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift new file mode 100644 index 00000000..3cf4b235 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_JSONSequenceEncoding: Test_Runtime { + + func testSerialized() async throws { + let sequence = WrappedSyncSequence( + sequence: [ + ArraySlice(#"{"name":"Rover"}"#.utf8), + ArraySlice(#"{"name":"Pancake"}"#.utf8) + ] + ).asSerializedJSONSequence() + try await XCTAssertEqualAsyncData(sequence, testJSONSequenceBytes) + } + + func testTyped() async throws { + let sequence = testEventsAsyncSequence.asEncodedJSONSequence() + try await XCTAssertEqualAsyncData(sequence, testJSONSequenceBytes) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 6b69bbc4..c3356f7f 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -122,19 +122,33 @@ class Test_Runtime: XCTestCase { var testJSONLinesBytes: ArraySlice { let encoder = JSONEncoder() - let bytes = try! testEvents.map { try encoder.encode($0) }.joined(separator: [0x0a]) + [0x0a] + let bytes = try! testEvents.map { try encoder.encode($0) + [ASCII.lf] }.joined() + return ArraySlice(bytes) + } + + var testJSONSequenceBytes: ArraySlice { + let encoder = JSONEncoder() + let bytes = try! testEvents.map { try [ASCII.rs] + encoder.encode($0) + [ASCII.lf] }.joined() return ArraySlice(bytes) } - var testJSONLinesOneBytePerElementSequence: HTTPBody { + func asOneBytePerElementSequence(_ source: ArraySlice) -> HTTPBody { HTTPBody( - WrappedSyncSequence(sequence: testJSONLinesBytes) + WrappedSyncSequence(sequence: source) .map { ArraySlice([$0]) }, - length: .known(Int64(testJSONLinesBytes.count)), + length: .known(Int64(source.count)), iterationBehavior: .multiple ) } + var testJSONLinesOneBytePerElementSequence: HTTPBody { + asOneBytePerElementSequence(testJSONLinesBytes) + } + + var testJSONSequenceOneBytePerElementSequence: HTTPBody { + asOneBytePerElementSequence(testJSONSequenceBytes) + } + @discardableResult func _testPrettyEncoded(_ value: Value, expectedJSON: String) throws -> String { let encoder = JSONEncoder() @@ -168,7 +182,7 @@ func chunkFromStringLines(_ strings: [String], addExtraCRLFs: Int = 0) -> ArrayS func chunkFromString(_ string: String, addCRLFs: Int = 0) -> ArraySlice { var slice = ArraySlice(string.utf8) - for _ in 0.. [UInt8] { Array(string.utf8) } extension ArraySlice { mutating func append(_ string: String) { append(contentsOf: chunkFromString(string)) } - mutating func appendCRLF() { append(contentsOf: [0x0d, 0x0a]) } + mutating func appendCRLF() { append(contentsOf: ASCII.crlf) } } struct TestError: Error, Equatable {} @@ -359,8 +373,9 @@ fileprivate extension UInt8 { var asHex: String { let original: String switch self { - case 0x0d: original = "CR" - case 0x0a: original = "LF" + case ASCII.cr: original = "CR" + case ASCII.lf: original = "LF" + case ASCII.rs: original = "RS" default: original = "\(UnicodeScalar(self)) " } return String(format: "%02x \(original)", self) From 44c89de7bcc99e48696e9d4eae5255067bcfacd9 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 14 Dec 2023 17:39:06 +0100 Subject: [PATCH 03/18] Generalize line encoding and decoding --- .../EventStreams/JSONLinesDecoding.swift | 109 +------------- .../EventStreams/JSONLinesEncoding.swift | 91 +----------- .../EventStreams/LinesDecoding.swift | 125 ++++++++++++++++ .../EventStreams/LinesEncoding.swift | 102 +++++++++++++ .../ServerSentEventsDecoding.swift | 134 ++++++++++++++++++ .../EventStreams/Test_JSONLinesDecoding.swift | 10 +- .../EventStreams/Test_JSONLinesEncoding.swift | 2 +- .../EventStreams/Test_LinesDecoding.swift | 27 ++++ .../EventStreams/Test_LinesEncoding.swift | 29 ++++ 9 files changed, 426 insertions(+), 203 deletions(-) create mode 100644 Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift create mode 100644 Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift create mode 100644 Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index 10d24d69..051a313e 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -14,121 +14,18 @@ import Foundation -struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - init(upstream: Upstream) { - self.upstream = upstream - } -} - -extension JSONLinesDeserializationSequence: AsyncSequence { - typealias Element = ArraySlice - - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { - var upstream: UpstreamIterator - var stateMachine: JSONLinesDeserializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { - while true { - switch stateMachine.next() { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .needsMore: - let value = try await upstream.next() - switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .noop: - continue - } - } - } - } - } - - func makeAsyncIterator() -> Iterator { - Iterator(upstream: upstream.makeAsyncIterator()) - } -} - extension AsyncSequence where Element == ArraySlice { - func asParsedJSONLines() -> JSONLinesDeserializationSequence { + + func asParsedJSONLines() -> LinesDeserializationSequence { .init(upstream: self) } func asDecodedJSONLines( of eventType: Event.Type = Event.self, using decoder: JSONDecoder = .init() - ) -> AsyncThrowingMapSequence, Event> { + ) -> AsyncThrowingMapSequence, Event> { asParsedJSONLines().map { line in try decoder.decode(Event.self, from: Data(line)) } } } - -struct JSONLinesDeserializerStateMachine { - - enum State { - case waitingForDelimiter([UInt8]) - case finished - case mutating - } - private(set) var state: State = .waitingForDelimiter([]) - - enum NextAction { - case returnNil - case returnLine(ArraySlice) - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .waitingForDelimiter(var buffer): - state = .mutating - guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { - state = .waitingForDelimiter(buffer) - return .needsMore - } - let line = buffer[..) - case noop - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .waitingForDelimiter(var buffer): - if let value { - state = .mutating - buffer.append(contentsOf: value) - state = .waitingForDelimiter(buffer) - return .noop - } else { - let line = ArraySlice(buffer) - buffer = [] - state = .finished - if line.isEmpty { - return .returnNil - } else { - return .returnLine(line) - } - } - case .finished, .mutating: - preconditionFailure("Invalid state") - } - } -} diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift index 1438c453..8a0567f0 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -14,49 +14,6 @@ import Foundation -struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - init(upstream: Upstream) { - self.upstream = upstream - } -} - -extension JSONLinesSerializationSequence: AsyncSequence { - typealias Element = ArraySlice - - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { - var upstream: UpstreamIterator - var stateMachine: JSONLinesSerializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { - while true { - switch stateMachine.next() { - case .returnNil: - return nil - case .needsMore: - let value = try await upstream.next() - switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnBytes(let bytes): - return bytes - } - } - } - } - } - - func makeAsyncIterator() -> Iterator { - Iterator(upstream: upstream.makeAsyncIterator()) - } -} - -extension AsyncSequence where Element == ArraySlice { - - func asSerializedJSONLines() -> JSONLinesSerializationSequence { - .init(upstream: self) - } -} - extension AsyncSequence where Element: Encodable { func asEncodedJSONLines( using encoder: JSONEncoder = { @@ -64,54 +21,10 @@ extension AsyncSequence where Element: Encodable { encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return encoder }() - ) -> JSONLinesSerializationSequence>> { + ) -> LinesSerializationSequence>> { map { event in try ArraySlice(encoder.encode(event)) } - .asSerializedJSONLines() - } -} - -struct JSONLinesSerializerStateMachine { - - enum State { - case running - case finished - } - private(set) var state: State = .running - - enum NextAction { - case returnNil - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .running: - return .needsMore - case .finished: - return .returnNil - } - } - - enum ReceivedValueAction { - case returnNil - case returnBytes(ArraySlice) - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .running: - if let value { - var buffer = value - buffer.append(ASCII.lf) - return .returnBytes(ArraySlice(buffer)) - } else { - state = .finished - return .returnNil - } - case .finished: - preconditionFailure("Invalid state") - } + .asSerializedLines() } } diff --git a/Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift new file mode 100644 index 00000000..ebdb5573 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct LinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension LinesDeserializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: LinesDeserializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .noop: + continue + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + func asParsedLines() -> LinesDeserializationSequence { + .init(upstream: self) + } +} + +struct LinesDeserializerStateMachine { + + enum State { + case waitingForDelimiter([UInt8]) + case finished + case mutating + } + private(set) var state: State = .waitingForDelimiter([]) + + enum NextAction { + case returnNil + case returnLine(ArraySlice) + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .waitingForDelimiter(var buffer): + state = .mutating + guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { + state = .waitingForDelimiter(buffer) + return .needsMore + } + let line = buffer[..) + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForDelimiter(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForDelimiter(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnLine(line) + } + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift new file mode 100644 index 00000000..cf1bf3a7 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct LinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension LinesSerializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: LinesSerializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnBytes(let bytes): + return bytes + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + + func asSerializedLines() -> LinesSerializationSequence { + .init(upstream: self) + } +} + +struct LinesSerializerStateMachine { + + enum State { + case running + case finished + } + private(set) var state: State = .running + + enum NextAction { + case returnNil + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .running: + return .needsMore + case .finished: + return .returnNil + } + } + + enum ReceivedValueAction { + case returnNil + case returnBytes(ArraySlice) + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer = value + buffer.append(ASCII.lf) + return .returnBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift new file mode 100644 index 00000000..81596624 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -0,0 +1,134 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension ServerSentEventsDeserializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: ServerSentEventsDeserializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .noop: + continue + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + func asParsedServerSentEvents() -> ServerSentEventsDeserializationSequence { + .init(upstream: self) + } + + func asDecodedServerSentEvents( + of eventType: Event.Type = Event.self, + using decoder: JSONDecoder = .init() + ) -> AsyncThrowingMapSequence, Event> { + asParsedServerSentEvents().map { line in + try decoder.decode(Event.self, from: Data(line)) + } + } +} + +struct ServerSentEventsDeserializerStateMachine { + + enum State { + case waitingForDelimiter([UInt8]) + case finished + case mutating + } + private(set) var state: State = .waitingForDelimiter([]) + + enum NextAction { + case returnNil + case returnLine(ArraySlice) + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .waitingForDelimiter(var buffer): + state = .mutating + guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { + state = .waitingForDelimiter(buffer) + return .needsMore + } + let line = buffer[..) + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForDelimiter(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForDelimiter(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnLine(line) + } + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift index 2bd353e2..d469e734 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift @@ -20,13 +20,9 @@ final class Test_JSONLinesDecoding: Test_Runtime { func testParsed() async throws { let sequence = testJSONLinesOneBytePerElementSequence.asParsedJSONLines() let lines = try await [ArraySlice](collecting: sequence) - XCTAssertEqual( - lines, - [ - ArraySlice(#"{"name":"Rover"}"#.utf8), - ArraySlice(#"{"name":"Pancake"}"#.utf8) - ] - ) + XCTAssertEqual(lines.count, 2) + XCTAssertEqualData(lines[0], "{\"name\":\"Rover\"}".utf8) + XCTAssertEqualData(lines[1], "{\"name\":\"Pancake\"}".utf8) } func testTyped() async throws { diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift index 00291401..0e6148da 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift @@ -23,7 +23,7 @@ final class Test_JSONLinesEncoding: Test_Runtime { ArraySlice(#"{"name":"Rover"}"#.utf8), ArraySlice(#"{"name":"Pancake"}"#.utf8) ] - ).asSerializedJSONLines() + ).asSerializedLines() try await XCTAssertEqualAsyncData(sequence, testJSONLinesBytes) } diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift new file mode 100644 index 00000000..48996fa1 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_LinesDecoding: Test_Runtime { + + func testParsed() async throws { + let sequence = asOneBytePerElementSequence(ArraySlice("hello\nworld\n".utf8)).asParsedLines() + let lines = try await [ArraySlice](collecting: sequence) + XCTAssertEqual(lines.count, 2) + XCTAssertEqualData(lines[0], "hello".utf8) + XCTAssertEqualData(lines[1], "world".utf8) + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift new file mode 100644 index 00000000..06b1a25b --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift @@ -0,0 +1,29 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_LinesEncoding: Test_Runtime { + + func testSerialized() async throws { + let sequence = WrappedSyncSequence( + sequence: [ + ArraySlice("hello".utf8), + ArraySlice("world".utf8) + ] + ).asSerializedLines() + try await XCTAssertEqualAsyncData(sequence, "hello\nworld\n".utf8) + } +} From b206a0639001ba402033f1e4791d02cafabea4cc Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 14 Dec 2023 19:51:53 +0100 Subject: [PATCH 04/18] wip --- .../EventStreams/JSONLinesDecoding.swift | 7 +- .../ServerSentEventsDecoding.swift | 216 +++++++++--------- .../EventStreams/Test_JSONLinesDecoding.swift | 8 - .../EventStreams/Test_JSONLinesEncoding.swift | 10 - 4 files changed, 114 insertions(+), 127 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index 051a313e..4cb6cef6 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -15,16 +15,11 @@ import Foundation extension AsyncSequence where Element == ArraySlice { - - func asParsedJSONLines() -> LinesDeserializationSequence { - .init(upstream: self) - } - func asDecodedJSONLines( of eventType: Event.Type = Event.self, using decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence, Event> { - asParsedJSONLines().map { line in + asParsedLines().map { line in try decoder.decode(Event.self, from: Data(line)) } } diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 81596624..729fbb36 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -14,51 +14,61 @@ import Foundation -struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - init(upstream: Upstream) { - self.upstream = upstream - } +//struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { +// var upstream: Upstream +// init(upstream: Upstream) { +// self.upstream = upstream +// } +//} +// +//extension ServerSentEventsDeserializationSequence: AsyncSequence { +// typealias Element = ArraySlice +// +// struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { +// var upstream: UpstreamIterator +// var stateMachine: ServerSentEventsDeserializerStateMachine = .init() +// mutating func next() async throws -> ArraySlice? { +// while true { +// switch stateMachine.next() { +// case .returnNil: +// return nil +// case .returnLine(let line): +// return line +// case .needsMore: +// let value = try await upstream.next() +// switch stateMachine.receivedValue(value) { +// case .returnNil: +// return nil +// case .returnLine(let line): +// return line +// case .noop: +// continue +// } +// } +// } +// } +// } +// +// func makeAsyncIterator() -> Iterator { +// Iterator(upstream: upstream.makeAsyncIterator()) +// } +//} + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events + +struct ServerSentEventFrame { + var name: String? + var value: String? } -extension ServerSentEventsDeserializationSequence: AsyncSequence { - typealias Element = ArraySlice - - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { - var upstream: UpstreamIterator - var stateMachine: ServerSentEventsDeserializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { - while true { - switch stateMachine.next() { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .needsMore: - let value = try await upstream.next() - switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .noop: - continue - } - } - } - } - } - - func makeAsyncIterator() -> Iterator { - Iterator(upstream: upstream.makeAsyncIterator()) - } +struct ServerSentEvent { + var event: String? + var data: String? + var id: String? + var retry: Int64? } extension AsyncSequence where Element == ArraySlice { - func asParsedServerSentEvents() -> ServerSentEventsDeserializationSequence { - .init(upstream: self) - } - func asDecodedServerSentEvents( of eventType: Event.Type = Event.self, using decoder: JSONDecoder = .init() @@ -69,66 +79,66 @@ extension AsyncSequence where Element == ArraySlice { } } -struct ServerSentEventsDeserializerStateMachine { - - enum State { - case waitingForDelimiter([UInt8]) - case finished - case mutating - } - private(set) var state: State = .waitingForDelimiter([]) - - enum NextAction { - case returnNil - case returnLine(ArraySlice) - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .waitingForDelimiter(var buffer): - state = .mutating - guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { - state = .waitingForDelimiter(buffer) - return .needsMore - } - let line = buffer[..) - case noop - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .waitingForDelimiter(var buffer): - if let value { - state = .mutating - buffer.append(contentsOf: value) - state = .waitingForDelimiter(buffer) - return .noop - } else { - let line = ArraySlice(buffer) - buffer = [] - state = .finished - if line.isEmpty { - return .returnNil - } else { - return .returnLine(line) - } - } - case .finished, .mutating: - preconditionFailure("Invalid state") - } - } -} +//struct ServerSentEventsDeserializerStateMachine { +// +// enum State { +// case waitingForDelimiter([UInt8]) +// case finished +// case mutating +// } +// private(set) var state: State = .waitingForDelimiter([]) +// +// enum NextAction { +// case returnNil +// case returnLine(ArraySlice) +// case needsMore +// } +// +// mutating func next() -> NextAction { +// switch state { +// case .waitingForDelimiter(var buffer): +// state = .mutating +// guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { +// state = .waitingForDelimiter(buffer) +// return .needsMore +// } +// let line = buffer[..) +// case noop +// } +// +// mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { +// switch state { +// case .waitingForDelimiter(var buffer): +// if let value { +// state = .mutating +// buffer.append(contentsOf: value) +// state = .waitingForDelimiter(buffer) +// return .noop +// } else { +// let line = ArraySlice(buffer) +// buffer = [] +// state = .finished +// if line.isEmpty { +// return .returnNil +// } else { +// return .returnLine(line) +// } +// } +// case .finished, .mutating: +// preconditionFailure("Invalid state") +// } +// } +//} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift index d469e734..402e5c87 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift @@ -17,14 +17,6 @@ import Foundation final class Test_JSONLinesDecoding: Test_Runtime { - func testParsed() async throws { - let sequence = testJSONLinesOneBytePerElementSequence.asParsedJSONLines() - let lines = try await [ArraySlice](collecting: sequence) - XCTAssertEqual(lines.count, 2) - XCTAssertEqualData(lines[0], "{\"name\":\"Rover\"}".utf8) - XCTAssertEqualData(lines[1], "{\"name\":\"Pancake\"}".utf8) - } - func testTyped() async throws { let sequence = testJSONLinesOneBytePerElementSequence.asDecodedJSONLines(of: TestPet.self) let events = try await [TestPet](collecting: sequence) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift index 0e6148da..63af27f0 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift @@ -17,16 +17,6 @@ import Foundation final class Test_JSONLinesEncoding: Test_Runtime { - func testSerialized() async throws { - let sequence = WrappedSyncSequence( - sequence: [ - ArraySlice(#"{"name":"Rover"}"#.utf8), - ArraySlice(#"{"name":"Pancake"}"#.utf8) - ] - ).asSerializedLines() - try await XCTAssertEqualAsyncData(sequence, testJSONLinesBytes) - } - func testTyped() async throws { let sequence = testEventsAsyncSequence.asEncodedJSONLines() try await XCTAssertEqualAsyncData(sequence, testJSONLinesBytes) From e80c2fa608d60e0c1f0406828f5408c388f6cc62 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 15 Dec 2023 09:57:52 +0100 Subject: [PATCH 05/18] WIP --- .../{Multipart => Base}/ByteUtilities.swift | 31 +++ .../EventStreams/JSONLinesDecoding.swift | 112 +++++++++- .../EventStreams/JSONLinesEncoding.swift | 91 +++++++- .../EventStreams/LinesDecoding.swift | 125 ----------- .../EventStreams/LinesEncoding.swift | 102 --------- .../ServerSentEventsDecoding.swift | 200 ++++++++++++++++-- .../EventStreams/Test_JSONLinesDecoding.swift | 8 + .../EventStreams/Test_JSONLinesEncoding.swift | 10 + .../EventStreams/Test_LinesDecoding.swift | 27 --- .../EventStreams/Test_LinesEncoding.swift | 29 --- 10 files changed, 426 insertions(+), 309 deletions(-) rename Sources/OpenAPIRuntime/{Multipart => Base}/ByteUtilities.swift (86%) delete mode 100644 Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift delete mode 100644 Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift delete mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift delete mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift diff --git a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift similarity index 86% rename from Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift rename to Sources/OpenAPIRuntime/Base/ByteUtilities.swift index 600b6815..ee7300a7 100644 --- a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift @@ -125,3 +125,34 @@ extension RandomAccessCollection where Element: Equatable { return .noMatch } } + +/// A value returned by the `longestMatchOfOneOf` method. +enum MatchOfOneOfResult { + + /// No match found at any position in self. + case noMatch + + case first(C.Index) + case second(C.Index) +} + +extension RandomAccessCollection where Element: Equatable { + + func matchOfOneOf( + first: Element, + second: Element + ) -> MatchOfOneOfResult { + var index = startIndex + while index < endIndex { + let element = self[index] + if element == first { + return .first(index) + } + if element == second { + return .second(index) + } + formIndex(after: &index) + } + return .noMatch + } +} diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index 4cb6cef6..10d24d69 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -14,13 +14,121 @@ import Foundation +struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension JSONLinesDeserializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: JSONLinesDeserializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .noop: + continue + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + extension AsyncSequence where Element == ArraySlice { + func asParsedJSONLines() -> JSONLinesDeserializationSequence { + .init(upstream: self) + } + func asDecodedJSONLines( of eventType: Event.Type = Event.self, using decoder: JSONDecoder = .init() - ) -> AsyncThrowingMapSequence, Event> { - asParsedLines().map { line in + ) -> AsyncThrowingMapSequence, Event> { + asParsedJSONLines().map { line in try decoder.decode(Event.self, from: Data(line)) } } } + +struct JSONLinesDeserializerStateMachine { + + enum State { + case waitingForDelimiter([UInt8]) + case finished + case mutating + } + private(set) var state: State = .waitingForDelimiter([]) + + enum NextAction { + case returnNil + case returnLine(ArraySlice) + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .waitingForDelimiter(var buffer): + state = .mutating + guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { + state = .waitingForDelimiter(buffer) + return .needsMore + } + let line = buffer[..) + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForDelimiter(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForDelimiter(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnLine(line) + } + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift index 8a0567f0..1438c453 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -14,6 +14,49 @@ import Foundation +struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension JSONLinesSerializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: JSONLinesSerializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnBytes(let bytes): + return bytes + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + + func asSerializedJSONLines() -> JSONLinesSerializationSequence { + .init(upstream: self) + } +} + extension AsyncSequence where Element: Encodable { func asEncodedJSONLines( using encoder: JSONEncoder = { @@ -21,10 +64,54 @@ extension AsyncSequence where Element: Encodable { encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return encoder }() - ) -> LinesSerializationSequence>> { + ) -> JSONLinesSerializationSequence>> { map { event in try ArraySlice(encoder.encode(event)) } - .asSerializedLines() + .asSerializedJSONLines() + } +} + +struct JSONLinesSerializerStateMachine { + + enum State { + case running + case finished + } + private(set) var state: State = .running + + enum NextAction { + case returnNil + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .running: + return .needsMore + case .finished: + return .returnNil + } + } + + enum ReceivedValueAction { + case returnNil + case returnBytes(ArraySlice) + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer = value + buffer.append(ASCII.lf) + return .returnBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: + preconditionFailure("Invalid state") + } } } diff --git a/Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift deleted file mode 100644 index ebdb5573..00000000 --- a/Sources/OpenAPIRuntime/EventStreams/LinesDecoding.swift +++ /dev/null @@ -1,125 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation - -struct LinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - init(upstream: Upstream) { - self.upstream = upstream - } -} - -extension LinesDeserializationSequence: AsyncSequence { - typealias Element = ArraySlice - - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { - var upstream: UpstreamIterator - var stateMachine: LinesDeserializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { - while true { - switch stateMachine.next() { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .needsMore: - let value = try await upstream.next() - switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .noop: - continue - } - } - } - } - } - - func makeAsyncIterator() -> Iterator { - Iterator(upstream: upstream.makeAsyncIterator()) - } -} - -extension AsyncSequence where Element == ArraySlice { - func asParsedLines() -> LinesDeserializationSequence { - .init(upstream: self) - } -} - -struct LinesDeserializerStateMachine { - - enum State { - case waitingForDelimiter([UInt8]) - case finished - case mutating - } - private(set) var state: State = .waitingForDelimiter([]) - - enum NextAction { - case returnNil - case returnLine(ArraySlice) - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .waitingForDelimiter(var buffer): - state = .mutating - guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { - state = .waitingForDelimiter(buffer) - return .needsMore - } - let line = buffer[..) - case noop - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .waitingForDelimiter(var buffer): - if let value { - state = .mutating - buffer.append(contentsOf: value) - state = .waitingForDelimiter(buffer) - return .noop - } else { - let line = ArraySlice(buffer) - buffer = [] - state = .finished - if line.isEmpty { - return .returnNil - } else { - return .returnLine(line) - } - } - case .finished, .mutating: - preconditionFailure("Invalid state") - } - } -} diff --git a/Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift deleted file mode 100644 index cf1bf3a7..00000000 --- a/Sources/OpenAPIRuntime/EventStreams/LinesEncoding.swift +++ /dev/null @@ -1,102 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -import Foundation - -struct LinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - init(upstream: Upstream) { - self.upstream = upstream - } -} - -extension LinesSerializationSequence: AsyncSequence { - typealias Element = ArraySlice - - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { - var upstream: UpstreamIterator - var stateMachine: LinesSerializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { - while true { - switch stateMachine.next() { - case .returnNil: - return nil - case .needsMore: - let value = try await upstream.next() - switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnBytes(let bytes): - return bytes - } - } - } - } - } - - func makeAsyncIterator() -> Iterator { - Iterator(upstream: upstream.makeAsyncIterator()) - } -} - -extension AsyncSequence where Element == ArraySlice { - - func asSerializedLines() -> LinesSerializationSequence { - .init(upstream: self) - } -} - -struct LinesSerializerStateMachine { - - enum State { - case running - case finished - } - private(set) var state: State = .running - - enum NextAction { - case returnNil - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .running: - return .needsMore - case .finished: - return .returnNil - } - } - - enum ReceivedValueAction { - case returnNil - case returnBytes(ArraySlice) - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .running: - if let value { - var buffer = value - buffer.append(ASCII.lf) - return .returnBytes(ArraySlice(buffer)) - } else { - state = .finished - return .returnNil - } - case .finished: - preconditionFailure("Invalid state") - } - } -} diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 729fbb36..b8937231 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -56,28 +56,28 @@ import Foundation // https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events -struct ServerSentEventFrame { - var name: String? - var value: String? -} - -struct ServerSentEvent { - var event: String? - var data: String? - var id: String? - var retry: Int64? -} - -extension AsyncSequence where Element == ArraySlice { - func asDecodedServerSentEvents( - of eventType: Event.Type = Event.self, - using decoder: JSONDecoder = .init() - ) -> AsyncThrowingMapSequence, Event> { - asParsedServerSentEvents().map { line in - try decoder.decode(Event.self, from: Data(line)) - } - } -} +//struct ServerSentEventFrame { +// var name: String? +// var value: String? +//} +// +//struct ServerSentEvent { +// var event: String? +// var data: String? +// var id: String? +// var retry: Int64? +//} +// +//extension AsyncSequence where Element == ArraySlice { +// func asDecodedServerSentEvents( +// of eventType: Event.Type = Event.self, +// using decoder: JSONDecoder = .init() +// ) -> AsyncThrowingMapSequence, Event> { +// asParsedServerSentEvents().map { line in +// try decoder.decode(Event.self, from: Data(line)) +// } +// } +//} //struct ServerSentEventsDeserializerStateMachine { // @@ -142,3 +142,159 @@ extension AsyncSequence where Element == ArraySlice { // } // } //} + +struct ServerSentEventsLineDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension ServerSentEventsLineDeserializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + var upstream: UpstreamIterator + var stateMachine: ServerSentEventsLineDeserializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .noop: + continue + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnLine(let line): + return line + case .noop: + continue + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +/// A state machine for parsing lines in server-sent events. +/// +/// https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream +/// +/// This is not trivial to do with a streaming parser, as the end of line can be: +/// - LF +/// - CR +/// - CRLF +/// +/// So when we get CR, but have no more data, we want to be able to emit the previous line, +/// however we need to discard a LF if one comes. +struct ServerSentEventsLineDeserializerStateMachine { + + enum State { + case waitingForEndOfLine([UInt8]) + case consumedCR([UInt8]) + case finished + case mutating + } + private(set) var state: State = .waitingForEndOfLine([]) + + enum NextAction { + case returnNil + case returnLine(ArraySlice) + case needsMore + case noop + } + + mutating func next() -> NextAction { + switch state { + case .waitingForEndOfLine(var buffer): + switch buffer.matchOfOneOf(first: ASCII.lf, second: ASCII.cr) { + case .noMatch: + return .needsMore + case .first(let index): + // Just a LF, so consume the line and move onto the next line. + state = .mutating + let line = buffer[..) + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForEndOfLine(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForEndOfLine(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnLine(line) + } + } + case .consumedCR(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .consumedCR(buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { + return .returnNil + } else { + return .returnLine(line) + } + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift index 402e5c87..dbcd6fe1 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift @@ -17,6 +17,14 @@ import Foundation final class Test_JSONLinesDecoding: Test_Runtime { + func testParsed() async throws { + let sequence = asOneBytePerElementSequence(ArraySlice("hello\nworld\n".utf8)).asParsedJSONLines() + let lines = try await [ArraySlice](collecting: sequence) + XCTAssertEqual(lines.count, 2) + XCTAssertEqualData(lines[0], "hello".utf8) + XCTAssertEqualData(lines[1], "world".utf8) + } + func testTyped() async throws { let sequence = testJSONLinesOneBytePerElementSequence.asDecodedJSONLines(of: TestPet.self) let events = try await [TestPet](collecting: sequence) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift index 63af27f0..794030c5 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift @@ -17,6 +17,16 @@ import Foundation final class Test_JSONLinesEncoding: Test_Runtime { + func testSerialized() async throws { + let sequence = WrappedSyncSequence( + sequence: [ + ArraySlice("hello".utf8), + ArraySlice("world".utf8) + ] + ).asSerializedJSONLines() + try await XCTAssertEqualAsyncData(sequence, "hello\nworld\n".utf8) + } + func testTyped() async throws { let sequence = testEventsAsyncSequence.asEncodedJSONLines() try await XCTAssertEqualAsyncData(sequence, testJSONLinesBytes) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift deleted file mode 100644 index 48996fa1..00000000 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesDecoding.swift +++ /dev/null @@ -1,27 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -@_spi(Generated) @testable import OpenAPIRuntime -import Foundation - -final class Test_LinesDecoding: Test_Runtime { - - func testParsed() async throws { - let sequence = asOneBytePerElementSequence(ArraySlice("hello\nworld\n".utf8)).asParsedLines() - let lines = try await [ArraySlice](collecting: sequence) - XCTAssertEqual(lines.count, 2) - XCTAssertEqualData(lines[0], "hello".utf8) - XCTAssertEqualData(lines[1], "world".utf8) - } -} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift deleted file mode 100644 index 06b1a25b..00000000 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_LinesEncoding.swift +++ /dev/null @@ -1,29 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// -import XCTest -@_spi(Generated) @testable import OpenAPIRuntime -import Foundation - -final class Test_LinesEncoding: Test_Runtime { - - func testSerialized() async throws { - let sequence = WrappedSyncSequence( - sequence: [ - ArraySlice("hello".utf8), - ArraySlice("world".utf8) - ] - ).asSerializedLines() - try await XCTAssertEqualAsyncData(sequence, "hello\nworld\n".utf8) - } -} From 8dc10cf28a40d1e7ae7082fd42b232ee56fcab46 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 15 Dec 2023 10:04:28 +0100 Subject: [PATCH 06/18] SSE line parsing works --- .../ServerSentEventsDecoding.swift | 6 ++ .../Test_ServerSentEventsDecoding.swift | 62 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index b8937231..26d6d221 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -185,6 +185,12 @@ extension ServerSentEventsLineDeserializationSequence: AsyncSequence { } } +extension AsyncSequence where Element == ArraySlice { + func asParsedServerSentEventLines() -> ServerSentEventsLineDeserializationSequence { + .init(upstream: self) + } +} + /// A state machine for parsing lines in server-sent events. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift new file mode 100644 index 00000000..3135754f --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift @@ -0,0 +1,62 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_ServerSentEventsDecoding_Lines: Test_Runtime { + + func _test( + input: String, + output: [String], + file: StaticString = #file, + line: UInt = #line + ) async throws { + let sequence = asOneBytePerElementSequence(ArraySlice(input.utf8)) + .asParsedServerSentEventLines() + let lines = try await [ArraySlice](collecting: sequence) + XCTAssertEqual(lines.count, output.count, file: file, line: line) + for (index, linePair) in zip(lines, output).enumerated() { + let (actualLine, expectedLine) = linePair + XCTAssertEqualData(actualLine, expectedLine.utf8, "Line: \(index)", file: file, line: line) + } + } + + func test() async throws { + // LF + try await _test( + input: "hello\nworld\n", + output: [ + "hello", + "world" + ] + ) + // CR + try await _test( + input: "hello\rworld\r", + output: [ + "hello", + "world" + ] + ) + // CRLF + try await _test( + input: "hello\r\nworld\r\n", + output: [ + "hello", + "world" + ] + ) + } +} From e33fc0d6d973f31b4451090391edfb4335599b0b Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 15 Dec 2023 10:52:39 +0100 Subject: [PATCH 07/18] Added SSE decoding, plus JSON data support --- .../ServerSentEventsDecoding.swift | 359 ++++++++++++------ 1 file changed, 234 insertions(+), 125 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 26d6d221..185c0717 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -14,134 +14,243 @@ import Foundation -//struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { -// var upstream: Upstream -// init(upstream: Upstream) { -// self.upstream = upstream -// } -//} -// -//extension ServerSentEventsDeserializationSequence: AsyncSequence { -// typealias Element = ArraySlice -// -// struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { -// var upstream: UpstreamIterator -// var stateMachine: ServerSentEventsDeserializerStateMachine = .init() -// mutating func next() async throws -> ArraySlice? { -// while true { -// switch stateMachine.next() { -// case .returnNil: -// return nil -// case .returnLine(let line): -// return line -// case .needsMore: -// let value = try await upstream.next() -// switch stateMachine.receivedValue(value) { -// case .returnNil: -// return nil -// case .returnLine(let line): -// return line -// case .noop: -// continue -// } -// } -// } -// } -// } -// -// func makeAsyncIterator() -> Iterator { -// Iterator(upstream: upstream.makeAsyncIterator()) -// } -//} +/// https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events -// https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events +struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} -//struct ServerSentEventFrame { -// var name: String? -// var value: String? -//} -// -//struct ServerSentEvent { -// var event: String? -// var data: String? -// var id: String? -// var retry: Int64? -//} -// -//extension AsyncSequence where Element == ArraySlice { -// func asDecodedServerSentEvents( -// of eventType: Event.Type = Event.self, -// using decoder: JSONDecoder = .init() -// ) -> AsyncThrowingMapSequence, Event> { -// asParsedServerSentEvents().map { line in -// try decoder.decode(Event.self, from: Data(line)) -// } -// } -//} +extension ServerSentEventsDeserializationSequence: AsyncSequence { + typealias Element = ServerSentEvent + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ArraySlice { + var upstream: UpstreamIterator + var stateMachine: ServerSentEventsDeserializerStateMachine = .init() + mutating func next() async throws -> ServerSentEvent? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .emitEvent(let event): + return event + case .noop: + continue + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .noop: + continue + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ArraySlice { + + func asDecodedServerSentEvents() -> ServerSentEventsDeserializationSequence> { + .init(upstream: asParsedServerSentEventLines()) + } + + func asDecodedServerSentEventsWithJSONData( + of dataType: JSONDataType.Type = JSONDataType.self, + using decoder: JSONDecoder = .init() + ) -> AsyncThrowingMapSequence< + ServerSentEventsDeserializationSequence>, + ServerSentEventWithJSONData + > { + asDecodedServerSentEvents().map { event in + ServerSentEventWithJSONData( + event: event.event, + data: try event.data.flatMap { stringData in + try decoder.decode(JSONDataType.self, from: Data(stringData.utf8)) + }, + id: event.id, + retry: event.retry + ) + } + } +} + +/// An event sent by the server. +/// +/// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation +struct ServerSentEventWithJSONData: Sendable { + + /// A type of the event, helps inform how to interpret the data. + var event: String? + + /// The payload of the event. + var data: JSONDataType? + + /// A unique identifier of the event, can be used to resume an interrupted stream by + /// making a new request with the `Last-Event-ID` header field set to this value. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header + var id: String? + + /// The amount of time, in milliseconds, the client should wait before reconnecting in case + /// of an interruption. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface + var retry: Int64? +} + +/// An event sent by the server. +/// +/// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation +struct ServerSentEvent: Sendable { + + /// A type of the event, helps inform how to interpret the data. + var event: String? + + /// The payload of the event. + var data: String? + + /// A unique identifier of the event, can be used to resume an interrupted stream by + /// making a new request with the `Last-Event-ID` header field set to this value. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header + var id: String? + + /// The amount of time, in milliseconds, the client should wait before reconnecting in case + /// of an interruption. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface + var retry: Int64? +} -//struct ServerSentEventsDeserializerStateMachine { -// -// enum State { -// case waitingForDelimiter([UInt8]) -// case finished -// case mutating -// } -// private(set) var state: State = .waitingForDelimiter([]) -// -// enum NextAction { -// case returnNil -// case returnLine(ArraySlice) -// case needsMore -// } -// -// mutating func next() -> NextAction { -// switch state { -// case .waitingForDelimiter(var buffer): -// state = .mutating -// guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { -// state = .waitingForDelimiter(buffer) -// return .needsMore -// } -// let line = buffer[..) -// case noop -// } -// -// mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { -// switch state { -// case .waitingForDelimiter(var buffer): -// if let value { -// state = .mutating -// buffer.append(contentsOf: value) -// state = .waitingForDelimiter(buffer) -// return .noop -// } else { -// let line = ArraySlice(buffer) -// buffer = [] -// state = .finished -// if line.isEmpty { -// return .returnNil -// } else { -// return .returnLine(line) -// } -// } -// case .finished, .mutating: -// preconditionFailure("Invalid state") -// } -// } -//} +struct ServerSentEventsDeserializerStateMachine { + + enum State { + case accumulatingEvent(ServerSentEvent, buffer: [ArraySlice]) + case finished + case mutating + } + private(set) var state: State = .accumulatingEvent(.init(), buffer: []) + + enum NextAction { + case returnNil + case emitEvent(ServerSentEvent) + case needsMore + case noop + } + + mutating func next() -> NextAction { + switch state { + case .accumulatingEvent(var event, var buffer): + guard let line = buffer.first else { + return .needsMore + } + state = .mutating + buffer.removeFirst() + if line.isEmpty { + // Dispatch the accumulated event. + state = .accumulatingEvent(.init(), buffer: buffer) + // If the last character of data is a newline, strip it. + if event.data?.hasSuffix("\n") ?? false { + event.data?.removeLast() + } + return .emitEvent(event) + } + if line[0] == ASCII.colon { + // A comment, skip this line. + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } + // Parse the field name and value. + let field: String + let value: String? + if let indexOfFirstColon = line.firstIndex(of: ASCII.colon) { + field = String(decoding: line[.. + if valueBytes.isEmpty { + resolvedValueBytes = [] + } else if valueBytes[0] == ASCII.space { + resolvedValueBytes = valueBytes.dropFirst() + } else { + resolvedValueBytes = valueBytes + } + value = String(decoding: resolvedValueBytes, as: UTF8.self) + } else { + field = String(decoding: line, as: UTF8.self) + value = nil + } + guard let value else { + // An unknown type of event, skip. + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } + // Process the field. + switch field { + case "event": + event.event = value + case "data": + event.data?.append(value) + event.data?.append("\n") + case "id": + event.id = value + case "retry": + if let retry = Int64(value) { + event.retry = retry + } else { + // Skip this line. + fallthrough + } + default: + // An unknown or invalid field, skip. + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } + // Processed the field, continue. + state = .accumulatingEvent(event, buffer: buffer) + return .noop + case .finished: + return .returnNil + case .mutating: + preconditionFailure("Invalid state") + } + } + + enum ReceivedValueAction { + case returnNil + case noop + } + + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .accumulatingEvent(let event, var buffer): + if let value { + state = .mutating + buffer.append(value) + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } else { + // If no value is received, drop the existing event on the floor. + // The specification explicitly states this. + // > Once the end of the file is reached, any pending data must be discarded. + // > (If the file ends in the middle of an event, before the final empty line, + // > the incomplete event is not dispatched.) + // Source: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + state = .finished + return .returnNil + } + case .finished, .mutating: + preconditionFailure("Invalid state") + } + } +} struct ServerSentEventsLineDeserializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream From 307f9050df5dcd3f6bbbec744c2b90eabf201a70 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 15 Dec 2023 11:31:02 +0100 Subject: [PATCH 08/18] SSE decoding works --- .../EventStreams/JSONSequenceDecoding.swift | 2 +- .../ServerSentEventsDecoding.swift | 16 ++- .../Test_ServerSentEventsDecoding.swift | 125 ++++++++++++++++++ 3 files changed, 135 insertions(+), 8 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift index ff415032..e775e66a 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift @@ -115,7 +115,7 @@ struct JSONSequenceDeserializerStateMachine { guard !buffer.isEmpty else { return .needsMore } - guard buffer[0] == ASCII.rs else { + guard buffer.first! == ASCII.rs else { return .emitError(.missingInitialRS) } state = .mutating diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 185c0717..d49cec3d 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -85,7 +85,7 @@ extension AsyncSequence where Element == ArraySlice { /// An event sent by the server. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation -struct ServerSentEventWithJSONData: Sendable { +struct ServerSentEventWithJSONData: Sendable, Hashable { /// A type of the event, helps inform how to interpret the data. var event: String? @@ -109,7 +109,7 @@ struct ServerSentEventWithJSONData: Sendable /// An event sent by the server. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation -struct ServerSentEvent: Sendable { +struct ServerSentEvent: Sendable, Hashable { /// A type of the event, helps inform how to interpret the data. var event: String? @@ -163,7 +163,7 @@ struct ServerSentEventsDeserializerStateMachine { } return .emitEvent(event) } - if line[0] == ASCII.colon { + if line.first! == ASCII.colon { // A comment, skip this line. state = .accumulatingEvent(event, buffer: buffer) return .noop @@ -177,7 +177,7 @@ struct ServerSentEventsDeserializerStateMachine { let resolvedValueBytes: ArraySlice if valueBytes.isEmpty { resolvedValueBytes = [] - } else if valueBytes[0] == ASCII.space { + } else if valueBytes.first! == ASCII.space { resolvedValueBytes = valueBytes.dropFirst() } else { resolvedValueBytes = valueBytes @@ -197,8 +197,10 @@ struct ServerSentEventsDeserializerStateMachine { case "event": event.event = value case "data": - event.data?.append(value) - event.data?.append("\n") + var data = event.data ?? "" + data.append(value) + data.append("\n") + event.data = data case "id": event.id = value case "retry": @@ -356,7 +358,7 @@ struct ServerSentEventsLineDeserializerStateMachine { return .needsMore } state = .mutating - if buffer[0] == ASCII.lf { + if buffer.first! == ASCII.lf { buffer.removeFirst() } state = .waitingForEndOfLine(buffer) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift index 3135754f..50df5dd4 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift @@ -15,6 +15,131 @@ import XCTest @_spi(Generated) @testable import OpenAPIRuntime import Foundation +final class Test_ServerSentEventsDecoding: Test_Runtime { + + func _test( + input: String, + output: [ServerSentEvent], + file: StaticString = #file, + line: UInt = #line + ) async throws { + let sequence = asOneBytePerElementSequence(ArraySlice(input.utf8)) + .asDecodedServerSentEvents() + let events = try await [ServerSentEvent](collecting: sequence) + XCTAssertEqual(events.count, output.count, file: file, line: line) + for (index, linePair) in zip(events, output).enumerated() { + let (actualEvent, expectedEvent) = linePair + XCTAssertEqual(actualEvent, expectedEvent, "Event: \(index)", file: file, line: line) + } + } + + func test() async throws { + // Simple event. + try await _test( + input: #""" + data: hello + data: world + + + """#, + output: [ + .init(data: "hello\nworld") + ] + ) + // Two simple events. + try await _test( + input: #""" + data: hello + data: world + + data: hello2 + data: world2 + + + """#, + output: [ + .init(data: "hello\nworld"), + .init(data: "hello2\nworld2") + ] + ) + // Incomplete event is not emitted. + try await _test( + input: #""" + data: hello + """#, + output: [] + ) + // A few events. + try await _test( + input: #""" + retry: 5000 + + data: This is the first message. + + data: This is the second message. + + event: customEvent + data: This is a custom event message. + + id: 123 + data: This is a message with an ID. + + + """#, + output: [ + .init(retry: 5000), + .init(data: "This is the first message."), + .init(data: "This is the second message."), + .init(event: "customEvent", data: "This is a custom event message."), + .init(data: "This is a message with an ID.", id: "123") + ] + ) + } + + func _testJSONData( + input: String, + output: [ServerSentEventWithJSONData], + file: StaticString = #file, + line: UInt = #line + ) async throws { + let sequence = asOneBytePerElementSequence(ArraySlice(input.utf8)) + .asDecodedServerSentEventsWithJSONData(of: JSONType.self) + let events = try await [ServerSentEventWithJSONData](collecting: sequence) + XCTAssertEqual(events.count, output.count, file: file, line: line) + for (index, linePair) in zip(events, output).enumerated() { + let (actualEvent, expectedEvent) = linePair + XCTAssertEqual(actualEvent, expectedEvent, "Event: \(index)", file: file, line: line) + } + } + + struct TestEvent: Decodable, Hashable, Sendable { + var index: Int + } + + func testJSONData() async throws { + // Simple event. + try await _testJSONData( + input: #""" + event: event1 + id: 1 + data: {"index":1} + + event: event2 + id: 2 + data: { + data: "index": 2 + data: } + + + """#, + output: [ + .init(event: "event1", data: TestEvent(index: 1), id: "1"), + .init(event: "event2", data: TestEvent(index: 2), id: "2") + ] + ) + } +} + final class Test_ServerSentEventsDecoding_Lines: Test_Runtime { func _test( From 1bd58b9ac19eee118b7c277ebf9b4fa2220c9ffc Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 15 Dec 2023 12:01:49 +0100 Subject: [PATCH 09/18] Got SSE encoding workign --- .../EventStreams/ServerSentEvents.swift | 61 +++++++ .../ServerSentEventsDecoding.swift | 48 ------ .../ServerSentEventsEncoding.swift | 151 ++++++++++++++++++ .../Test_ServerSentEventsDecoding.swift | 9 +- .../Test_ServerSentEventsEncoding.swift | 125 +++++++++++++++ 5 files changed, 342 insertions(+), 52 deletions(-) create mode 100644 Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift create mode 100644 Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift create mode 100644 Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift new file mode 100644 index 00000000..041dacd2 --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift @@ -0,0 +1,61 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +/// An event sent by the server. +/// +/// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation +struct ServerSentEventWithJSONData: Sendable, Hashable { + + /// A type of the event, helps inform how to interpret the data. + var event: String? + + /// The payload of the event. + var data: JSONDataType? + + /// A unique identifier of the event, can be used to resume an interrupted stream by + /// making a new request with the `Last-Event-ID` header field set to this value. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header + var id: String? + + /// The amount of time, in milliseconds, the client should wait before reconnecting in case + /// of an interruption. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface + var retry: Int64? +} + +/// An event sent by the server. +/// +/// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation +struct ServerSentEvent: Sendable, Hashable { + + /// A unique identifier of the event, can be used to resume an interrupted stream by + /// making a new request with the `Last-Event-ID` header field set to this value. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header + var id: String? + + /// A type of the event, helps inform how to interpret the data. + var event: String? + + /// The payload of the event. + var data: String? + + /// The amount of time, in milliseconds, the client should wait before reconnecting in case + /// of an interruption. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface + var retry: Int64? +} diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index d49cec3d..3469bf71 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -82,54 +82,6 @@ extension AsyncSequence where Element == ArraySlice { } } -/// An event sent by the server. -/// -/// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation -struct ServerSentEventWithJSONData: Sendable, Hashable { - - /// A type of the event, helps inform how to interpret the data. - var event: String? - - /// The payload of the event. - var data: JSONDataType? - - /// A unique identifier of the event, can be used to resume an interrupted stream by - /// making a new request with the `Last-Event-ID` header field set to this value. - /// - /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header - var id: String? - - /// The amount of time, in milliseconds, the client should wait before reconnecting in case - /// of an interruption. - /// - /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface - var retry: Int64? -} - -/// An event sent by the server. -/// -/// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation -struct ServerSentEvent: Sendable, Hashable { - - /// A type of the event, helps inform how to interpret the data. - var event: String? - - /// The payload of the event. - var data: String? - - /// A unique identifier of the event, can be used to resume an interrupted stream by - /// making a new request with the `Last-Event-ID` header field set to this value. - /// - /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header - var id: String? - - /// The amount of time, in milliseconds, the client should wait before reconnecting in case - /// of an interruption. - /// - /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface - var retry: Int64? -} - struct ServerSentEventsDeserializerStateMachine { enum State { diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift new file mode 100644 index 00000000..c7ac332e --- /dev/null +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -0,0 +1,151 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import Foundation + +struct ServerSentEventsSerializationSequence: Sendable where Upstream.Element == ServerSentEvent { + var upstream: Upstream + init(upstream: Upstream) { + self.upstream = upstream + } +} + +extension ServerSentEventsSerializationSequence: AsyncSequence { + typealias Element = ArraySlice + + struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ServerSentEvent { + var upstream: UpstreamIterator + var stateMachine: ServerSentEventsSerializerStateMachine = .init() + mutating func next() async throws -> ArraySlice? { + while true { + switch stateMachine.next() { + case .returnNil: + return nil + case .needsMore: + let value = try await upstream.next() + switch stateMachine.receivedValue(value) { + case .returnNil: + return nil + case .returnBytes(let bytes): + return bytes + } + } + } + } + } + + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator()) + } +} + +extension AsyncSequence where Element == ServerSentEvent { + + func asEncodedServerSentEvents() -> ServerSentEventsSerializationSequence { + .init(upstream: self) + } +} + +extension AsyncSequence { + func asEncodedServerSentEventsWithJSONData( + using encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] + return encoder + }() + ) -> ServerSentEventsSerializationSequence> where Element == ServerSentEventWithJSONData + { + map { event in + ServerSentEvent( + id: event.id, + event: event.event, + data: try event.data.flatMap { try String(decoding: encoder.encode($0), as: UTF8.self) }, + retry: event.retry + ) + } + .asEncodedServerSentEvents() + } +} + +struct ServerSentEventsSerializerStateMachine { + + enum State { + case running + case finished + } + private(set) var state: State = .running + + enum NextAction { + case returnNil + case needsMore + } + + mutating func next() -> NextAction { + switch state { + case .running: + return .needsMore + case .finished: + return .returnNil + } + } + + enum ReceivedValueAction { + case returnNil + case returnBytes(ArraySlice) + } + + mutating func receivedValue(_ value: ServerSentEvent?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer: [UInt8] = [] + func encodeField(name: String, value: some StringProtocol) { + buffer.append(contentsOf: name.utf8) + buffer.append(ASCII.colon) + buffer.append(ASCII.space) + buffer.append(contentsOf: value.utf8) + buffer.append(ASCII.lf) + } + if let id = value.id { + encodeField(name: "id", value: id) + } + if let event = value.event { + encodeField(name: "event", value: event) + } + if let retry = value.retry { + encodeField(name: "retry", value: String(retry)) + } + if let data = value.data { + // Normalize the data section by replacing CRLF and CR with just LF. + // Then split the section into individual field/value pairs. + let lines = data + .replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") + .split(separator: "\n", omittingEmptySubsequences: false) + for line in lines { + encodeField(name: "data", value: line) + } + } + // End the event. + buffer.append(ASCII.lf) + return .returnBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: + preconditionFailure("Invalid state") + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift index 50df5dd4..3450f14f 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift @@ -76,7 +76,8 @@ final class Test_ServerSentEventsDecoding: Test_Runtime { data: This is the first message. - data: This is the second message. + data: This is the second + data: message. event: customEvent data: This is a custom event message. @@ -89,9 +90,9 @@ final class Test_ServerSentEventsDecoding: Test_Runtime { output: [ .init(retry: 5000), .init(data: "This is the first message."), - .init(data: "This is the second message."), + .init(data: "This is the second\nmessage."), .init(event: "customEvent", data: "This is a custom event message."), - .init(data: "This is a message with an ID.", id: "123") + .init(id: "123", data: "This is a message with an ID.") ] ) } @@ -130,7 +131,7 @@ final class Test_ServerSentEventsDecoding: Test_Runtime { data: "index": 2 data: } - + """#, output: [ .init(event: "event1", data: TestEvent(index: 1), id: "1"), diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift new file mode 100644 index 00000000..72106df9 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift @@ -0,0 +1,125 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftOpenAPIGenerator open source project +// +// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// +import XCTest +@_spi(Generated) @testable import OpenAPIRuntime +import Foundation + +final class Test_ServerSentEventsEncoding: Test_Runtime { + + func _test( + input: [ServerSentEvent], + output: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let sequence = WrappedSyncSequence( + sequence: input + ).asEncodedServerSentEvents() + try await XCTAssertEqualAsyncData(sequence, output.utf8, file: file, line: line) + } + + func test() async throws { + // Simple event. + try await _test( + input: [ + .init(data: "hello\nworld") + ], + output: #""" + data: hello + data: world + + + """# + ) + // Two simple events. + try await _test( + input: [ + .init(data: "hello\nworld"), + .init(data: "hello2\nworld2") + ], + output: #""" + data: hello + data: world + + data: hello2 + data: world2 + + + """# + ) + // A few events. + try await _test( + input: [ + .init(retry: 5000), + .init(data: "This is the first message."), + .init(data: "This is the second\nmessage."), + .init(event: "customEvent", data: "This is a custom event message."), + .init(id: "123", data: "This is a message with an ID.") + ], + output: #""" + retry: 5000 + + data: This is the first message. + + data: This is the second + data: message. + + event: customEvent + data: This is a custom event message. + + id: 123 + data: This is a message with an ID. + + + """# + ) + } + + func _testJSONData( + input: [ServerSentEventWithJSONData], + output: String, + file: StaticString = #file, + line: UInt = #line + ) async throws { + let sequence = WrappedSyncSequence( + sequence: input + ).asEncodedServerSentEventsWithJSONData() + try await XCTAssertEqualAsyncData(sequence, output.utf8, file: file, line: line) + } + + struct TestEvent: Encodable, Hashable, Sendable { + var index: Int + } + + func testJSONData() async throws { + // Simple event. + try await _testJSONData( + input: [ + .init(event: "event1", data: TestEvent(index: 1), id: "1"), + .init(event: "event2", data: TestEvent(index: 2), id: "2") + ], + output: #""" + id: 1 + event: event1 + data: {"index":1} + + id: 2 + event: event2 + data: {"index":2} + + + """# + ) + } +} From d21171fae8ede74d8a1bfeb6c842333c5a656ab6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 19 Dec 2023 13:58:39 +0100 Subject: [PATCH 10/18] Make some types public --- .../EventStreams/JSONLinesDecoding.swift | 14 ++++---- .../EventStreams/JSONLinesEncoding.swift | 14 ++++---- .../EventStreams/JSONSequenceDecoding.swift | 14 ++++---- .../EventStreams/JSONSequenceEncoding.swift | 14 ++++---- .../EventStreams/ServerSentEvents.swift | 34 +++++++++++++------ .../ServerSentEventsDecoding.swift | 28 +++++++-------- .../ServerSentEventsEncoding.swift | 16 ++++----- 7 files changed, 74 insertions(+), 60 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index 10d24d69..6884c0cb 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -14,20 +14,20 @@ import Foundation -struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { +public struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONLinesDeserializationSequence: AsyncSequence { - typealias Element = ArraySlice + public typealias Element = ArraySlice - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { var upstream: UpstreamIterator var stateMachine: JSONLinesDeserializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { + public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { case .returnNil: @@ -49,7 +49,7 @@ extension JSONLinesDeserializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } @@ -59,7 +59,7 @@ extension AsyncSequence where Element == ArraySlice { .init(upstream: self) } - func asDecodedJSONLines( + public func asDecodedJSONLines( of eventType: Event.Type = Event.self, using decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence, Event> { diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift index 1438c453..72dede3a 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -14,20 +14,20 @@ import Foundation -struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { +public struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONLinesSerializationSequence: AsyncSequence { - typealias Element = ArraySlice + public typealias Element = ArraySlice - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { var upstream: UpstreamIterator var stateMachine: JSONLinesSerializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { + public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { case .returnNil: @@ -45,7 +45,7 @@ extension JSONLinesSerializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } @@ -58,7 +58,7 @@ extension AsyncSequence where Element == ArraySlice { } extension AsyncSequence where Element: Encodable { - func asEncodedJSONLines( + public func asEncodedJSONLines( using encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift index e775e66a..7e80e874 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift @@ -14,15 +14,15 @@ import Foundation -struct JSONSequenceDeserializationSequence: Sendable where Upstream.Element == ArraySlice { +public struct JSONSequenceDeserializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONSequenceDeserializationSequence: AsyncSequence { - typealias Element = ArraySlice + public typealias Element = ArraySlice struct DeserializerError: Swift.Error, CustomStringConvertible, LocalizedError { @@ -38,10 +38,10 @@ extension JSONSequenceDeserializationSequence: AsyncSequence { var errorDescription: String? { description } } - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { var upstream: UpstreamIterator var stateMachine: JSONSequenceDeserializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { + public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { case .returnNil: @@ -67,7 +67,7 @@ extension JSONSequenceDeserializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } @@ -77,7 +77,7 @@ extension AsyncSequence where Element == ArraySlice { .init(upstream: self) } - func asDecodedJSONSequence( + public func asDecodedJSONSequence( of eventType: Event.Type = Event.self, using decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence, Event> { diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift index d717c0ee..99654b9a 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift @@ -14,20 +14,20 @@ import Foundation -struct JSONSequenceSerializationSequence: Sendable where Upstream.Element == ArraySlice { +public struct JSONSequenceSerializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONSequenceSerializationSequence: AsyncSequence { - typealias Element = ArraySlice + public typealias Element = ArraySlice - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { var upstream: UpstreamIterator var stateMachine: JSONSequenceSerializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { + public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { case .returnNil: @@ -45,7 +45,7 @@ extension JSONSequenceSerializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } @@ -58,7 +58,7 @@ extension AsyncSequence where Element == ArraySlice { } extension AsyncSequence where Element: Encodable { - func asEncodedJSONSequence( + public func asEncodedJSONSequence( using encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift index 041dacd2..55483ec3 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift @@ -15,47 +15,61 @@ /// An event sent by the server. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation -struct ServerSentEventWithJSONData: Sendable, Hashable { +public struct ServerSentEventWithJSONData: Sendable, Hashable { /// A type of the event, helps inform how to interpret the data. - var event: String? + public var event: String? /// The payload of the event. - var data: JSONDataType? + public var data: JSONDataType? /// A unique identifier of the event, can be used to resume an interrupted stream by /// making a new request with the `Last-Event-ID` header field set to this value. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header - var id: String? + public var id: String? /// The amount of time, in milliseconds, the client should wait before reconnecting in case /// of an interruption. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface - var retry: Int64? + public var retry: Int64? + + public init(event: String? = nil, data: JSONDataType? = nil, id: String? = nil, retry: Int64? = nil) { + self.event = event + self.data = data + self.id = id + self.retry = retry + } } /// An event sent by the server. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation -struct ServerSentEvent: Sendable, Hashable { +public struct ServerSentEvent: Sendable, Hashable { /// A unique identifier of the event, can be used to resume an interrupted stream by /// making a new request with the `Last-Event-ID` header field set to this value. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-last-event-id-header - var id: String? + public var id: String? /// A type of the event, helps inform how to interpret the data. - var event: String? + public var event: String? /// The payload of the event. - var data: String? + public var data: String? /// The amount of time, in milliseconds, the client should wait before reconnecting in case /// of an interruption. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface - var retry: Int64? + public var retry: Int64? + + public init(id: String? = nil, event: String? = nil, data: String? = nil, retry: Int64? = nil) { + self.id = id + self.event = event + self.data = data + self.retry = retry + } } diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 3469bf71..10efc0d1 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -16,20 +16,20 @@ import Foundation /// https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events -struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { +public struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension ServerSentEventsDeserializationSequence: AsyncSequence { - typealias Element = ServerSentEvent + public typealias Element = ServerSentEvent - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ArraySlice { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ArraySlice { var upstream: UpstreamIterator var stateMachine: ServerSentEventsDeserializerStateMachine = .init() - mutating func next() async throws -> ServerSentEvent? { + public mutating func next() async throws -> ServerSentEvent? { while true { switch stateMachine.next() { case .returnNil: @@ -51,18 +51,18 @@ extension ServerSentEventsDeserializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } extension AsyncSequence where Element == ArraySlice { - func asDecodedServerSentEvents() -> ServerSentEventsDeserializationSequence> { + public func asDecodedServerSentEvents() -> ServerSentEventsDeserializationSequence> { .init(upstream: asParsedServerSentEventLines()) } - func asDecodedServerSentEventsWithJSONData( + public func asDecodedServerSentEventsWithJSONData( of dataType: JSONDataType.Type = JSONDataType.self, using decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence< @@ -206,20 +206,20 @@ struct ServerSentEventsDeserializerStateMachine { } } -struct ServerSentEventsLineDeserializationSequence: Sendable where Upstream.Element == ArraySlice { +public struct ServerSentEventsLineDeserializationSequence: Sendable where Upstream.Element == ArraySlice { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension ServerSentEventsLineDeserializationSequence: AsyncSequence { - typealias Element = ArraySlice + public typealias Element = ArraySlice - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { var upstream: UpstreamIterator var stateMachine: ServerSentEventsLineDeserializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { + public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { case .returnNil: @@ -243,7 +243,7 @@ extension ServerSentEventsLineDeserializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift index c7ac332e..2bcca5c1 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -14,20 +14,20 @@ import Foundation -struct ServerSentEventsSerializationSequence: Sendable where Upstream.Element == ServerSentEvent { +public struct ServerSentEventsSerializationSequence: Sendable where Upstream.Element == ServerSentEvent { var upstream: Upstream - init(upstream: Upstream) { + public init(upstream: Upstream) { self.upstream = upstream } } extension ServerSentEventsSerializationSequence: AsyncSequence { - typealias Element = ArraySlice + public typealias Element = ArraySlice - struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ServerSentEvent { + public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ServerSentEvent { var upstream: UpstreamIterator var stateMachine: ServerSentEventsSerializerStateMachine = .init() - mutating func next() async throws -> ArraySlice? { + public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { case .returnNil: @@ -45,20 +45,20 @@ extension ServerSentEventsSerializationSequence: AsyncSequence { } } - func makeAsyncIterator() -> Iterator { + public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } extension AsyncSequence where Element == ServerSentEvent { - func asEncodedServerSentEvents() -> ServerSentEventsSerializationSequence { + public func asEncodedServerSentEvents() -> ServerSentEventsSerializationSequence { .init(upstream: self) } } extension AsyncSequence { - func asEncodedServerSentEventsWithJSONData( + public func asEncodedServerSentEventsWithJSONData( using encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] From 7f410b8cd84d96d52ea8203d574afd5c159edd7f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 19 Dec 2023 17:11:08 +0100 Subject: [PATCH 11/18] Adding docs, prettifying --- .../EventStreams/JSONLinesDecoding.swift | 206 +++++++++++------- .../EventStreams/JSONLinesEncoding.swift | 163 ++++++++------ .../EventStreams/JSONSequenceDecoding.swift | 2 +- .../EventStreams/JSONSequenceEncoding.swift | 2 +- .../ServerSentEventsDecoding.swift | 2 +- .../ServerSentEventsEncoding.swift | 2 +- .../MultipartFramesToBytesSequence.swift | 1 + .../EventStreams/Test_JSONLinesDecoding.swift | 3 +- .../EventStreams/Test_JSONLinesEncoding.swift | 5 +- 9 files changed, 232 insertions(+), 154 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index 6884c0cb..13725bd9 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -14,121 +14,163 @@ import Foundation -public struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } +/// A sequence that parses arbitrary byte chunks into lines separated by the `` character. +public struct JSONLinesDeserializationSequence: Sendable +where Upstream.Element == ArraySlice { + + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of arbitrary byte chunks. + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONLinesDeserializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ArraySlice - - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + + /// The iterator of `JSONLinesDeserializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == Element { + + /// The upstream iterator of arbitrary byte chunks. var upstream: UpstreamIterator - var stateMachine: JSONLinesDeserializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { - case .returnNil: - return nil - case .returnLine(let line): - return line + case .returnNil: return nil + case .emitLine(let line): return line case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .noop: - continue + case .returnNil: return nil + case .emitLine(let line): return line + case .noop: continue } } } } } - + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } extension AsyncSequence where Element == ArraySlice { - func asParsedJSONLines() -> JSONLinesDeserializationSequence { - .init(upstream: self) - } - + + /// Returns another sequence that decodes each JSON Lines event as the provided type using the provided decoder. + /// - Parameters: + /// - eventType: The type to decode the JSON event into. + /// - decoder: The JSON decoder to use. + /// - Returns: A sequence that provides the decoded JSON events. public func asDecodedJSONLines( of eventType: Event.Type = Event.self, - using decoder: JSONDecoder = .init() + decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence, Event> { - asParsedJSONLines().map { line in - try decoder.decode(Event.self, from: Data(line)) - } + JSONLinesDeserializationSequence(upstream: self) + .map { line in try decoder.decode(Event.self, from: Data(line)) } } } -struct JSONLinesDeserializerStateMachine { - - enum State { - case waitingForDelimiter([UInt8]) - case finished - case mutating - } - private(set) var state: State = .waitingForDelimiter([]) - - enum NextAction { - case returnNil - case returnLine(ArraySlice) - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .waitingForDelimiter(var buffer): - state = .mutating - guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { - state = .waitingForDelimiter(buffer) - return .needsMore - } - let line = buffer[..) - case noop - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .waitingForDelimiter(var buffer): - if let value { + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .waitingForDelimiter(buffer: []) } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit a full line. + case emitLine(ArraySlice) + + /// The line is not complete yet, needs more bytes. + case needsMore + } + + /// Read the next line parsed from upstream bytes. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .waitingForDelimiter(var buffer): state = .mutating - buffer.append(contentsOf: value) - state = .waitingForDelimiter(buffer) - return .noop - } else { - let line = ArraySlice(buffer) - buffer = [] - state = .finished - if line.isEmpty { - return .returnNil + guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else { + state = .waitingForDelimiter(buffer: buffer) + return .needsMore + } + let line = buffer[..) + + /// No action, rerun the parsing loop. + case noop + } + + /// Ingest the provided bytes. + /// - Parameter value: A new byte chunk. If `nil`, then the source of bytes is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForDelimiter(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForDelimiter(buffer: buffer) + return .noop } else { - return .returnLine(line) + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { return .returnNil } else { return .emitLine(line) } } + case .finished, .mutating: preconditionFailure("Invalid state") } - case .finished, .mutating: - preconditionFailure("Invalid state") } } } diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift index 72dede3a..91ec31f1 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -14,104 +14,137 @@ import Foundation -public struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } +/// A sequence that serializes lines by concatenating them using the `` character. +public struct JSONLinesSerializationSequence: Sendable +where Upstream.Element == ArraySlice { + + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of lines. + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONLinesSerializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ArraySlice - - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + + /// The iterator of `JSONLinesSerializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == Element { + + /// The upstream iterator of lines. var upstream: UpstreamIterator - var stateMachine: JSONLinesSerializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { - case .returnNil: - return nil + case .returnNil: return nil case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnBytes(let bytes): - return bytes + case .returnNil: return nil + case .emitBytes(let bytes): return bytes } } } } } - + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } -extension AsyncSequence where Element == ArraySlice { - - func asSerializedJSONLines() -> JSONLinesSerializationSequence { - .init(upstream: self) - } -} - extension AsyncSequence where Element: Encodable { + + /// Returns another sequence that encodes the events using the provided encoder into JSON Lines. + /// - Parameter encoder: The JSON encoder to use. + /// - Returns: A sequence that provides the serialized JSON Lines. public func asEncodedJSONLines( - using encoder: JSONEncoder = { + encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return encoder }() ) -> JSONLinesSerializationSequence>> { - map { event in - try ArraySlice(encoder.encode(event)) - } - .asSerializedJSONLines() + .init(upstream: map { event in try ArraySlice(encoder.encode(event)) }) } } -struct JSONLinesSerializerStateMachine { - - enum State { - case running - case finished - } - private(set) var state: State = .running - - enum NextAction { - case returnNil - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .running: - return .needsMore - case .finished: - return .returnNil +extension JSONLinesSerializationSequence.Iterator { + /// A state machine representing the JSON Lines serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State { + /// Is emitting serialized JSON Lines events. + case running + + /// Finished, the terminal state. + case finished } - } - - enum ReceivedValueAction { - case returnNil - case returnBytes(ArraySlice) - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .running: - if let value { - var buffer = value - buffer.append(ASCII.lf) - return .returnBytes(ArraySlice(buffer)) - } else { - state = .finished - return .returnNil + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .running } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Needs more bytes. + case needsMore + } + + /// Read the next byte chunk serialized from upstream lines. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .running: return .needsMore + case .finished: return .returnNil + } + } + + /// An action returned by the `receivedValue` method. + enum ReceivedValueAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit the provided bytes. + case emitBytes(ArraySlice) + } + + /// Ingest the provided line. + /// - Parameter value: A new line. If `nil`, then the source of line is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer = value + buffer.append(ASCII.lf) + return .emitBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: preconditionFailure("Invalid state") } - case .finished: - preconditionFailure("Invalid state") } } } diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift index 7e80e874..f729437b 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift @@ -79,7 +79,7 @@ extension AsyncSequence where Element == ArraySlice { public func asDecodedJSONSequence( of eventType: Event.Type = Event.self, - using decoder: JSONDecoder = .init() + decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence, Event> { asParsedJSONSequence().map { line in try decoder.decode(Event.self, from: Data(line)) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift index 99654b9a..6e649408 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift @@ -59,7 +59,7 @@ extension AsyncSequence where Element == ArraySlice { extension AsyncSequence where Element: Encodable { public func asEncodedJSONSequence( - using encoder: JSONEncoder = { + encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return encoder diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 10efc0d1..35c2ea1d 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -64,7 +64,7 @@ extension AsyncSequence where Element == ArraySlice { public func asDecodedServerSentEventsWithJSONData( of dataType: JSONDataType.Type = JSONDataType.self, - using decoder: JSONDecoder = .init() + decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence< ServerSentEventsDeserializationSequence>, ServerSentEventWithJSONData diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift index 2bcca5c1..29ef0cfc 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -59,7 +59,7 @@ extension AsyncSequence where Element == ServerSentEvent { extension AsyncSequence { public func asEncodedServerSentEventsWithJSONData( - using encoder: JSONEncoder = { + encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return encoder diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift index 441c85fd..07538233 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift @@ -90,6 +90,7 @@ struct MultipartSerializer { self.stateMachine = .init() self.outBuffer = [] } + /// Requests the next byte chunk. /// - Parameter fetchFrame: A closure that is called when the serializer is ready to serialize the next frame. /// - Returns: A byte chunk. diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift index dbcd6fe1..ad810eb2 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift @@ -18,7 +18,8 @@ import Foundation final class Test_JSONLinesDecoding: Test_Runtime { func testParsed() async throws { - let sequence = asOneBytePerElementSequence(ArraySlice("hello\nworld\n".utf8)).asParsedJSONLines() + let upstream = asOneBytePerElementSequence(ArraySlice("hello\nworld\n".utf8)) + let sequence = JSONLinesDeserializationSequence(upstream: upstream) let lines = try await [ArraySlice](collecting: sequence) XCTAssertEqual(lines.count, 2) XCTAssertEqualData(lines[0], "hello".utf8) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift index 794030c5..9a1762bf 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift @@ -18,12 +18,13 @@ import Foundation final class Test_JSONLinesEncoding: Test_Runtime { func testSerialized() async throws { - let sequence = WrappedSyncSequence( + let upstream = WrappedSyncSequence( sequence: [ ArraySlice("hello".utf8), ArraySlice("world".utf8) ] - ).asSerializedJSONLines() + ) + let sequence = JSONLinesSerializationSequence(upstream: upstream) try await XCTAssertEqualAsyncData(sequence, "hello\nworld\n".utf8) } From e5439b1cb44f3dfdc540cf2ba08f6bde35d77fc1 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 19 Dec 2023 18:53:56 +0100 Subject: [PATCH 12/18] Prettify JSONSequence --- .../EventStreams/JSONLinesDecoding.swift | 2 +- .../EventStreams/JSONLinesEncoding.swift | 4 +- .../EventStreams/JSONSequenceDecoding.swift | 294 ++++++++++-------- .../EventStreams/JSONSequenceEncoding.swift | 167 ++++++---- .../Test_JSONSequenceDecoding.swift | 3 +- .../Test_JSONSequenceEncoding.swift | 5 +- 6 files changed, 276 insertions(+), 199 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index 13725bd9..e40c16f0 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -14,7 +14,7 @@ import Foundation -/// A sequence that parses arbitrary byte chunks into lines separated by the `` character. +/// A sequence that parses arbitrary byte chunks into lines using the JSON Lines format. public struct JSONLinesDeserializationSequence: Sendable where Upstream.Element == ArraySlice { diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift index 91ec31f1..32244ee5 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -14,7 +14,7 @@ import Foundation -/// A sequence that serializes lines by concatenating them using the `` character. +/// A sequence that serializes lines by concatenating them using the JSON Lines format. public struct JSONLinesSerializationSequence: Sendable where Upstream.Element == ArraySlice { @@ -82,11 +82,13 @@ extension AsyncSequence where Element: Encodable { } extension JSONLinesSerializationSequence.Iterator { + /// A state machine representing the JSON Lines serializer. struct StateMachine { /// The possible states of the state machine. enum State { + /// Is emitting serialized JSON Lines events. case running diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift index f729437b..dfe960ef 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift @@ -14,177 +14,217 @@ import Foundation -public struct JSONSequenceDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } +/// A sequence that parses arbitrary byte chunks into lines using the JSON Sequence format. +public struct JSONSequenceDeserializationSequence: Sendable +where Upstream.Element == ArraySlice { + + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of arbitrary byte chunks. + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONSequenceDeserializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ArraySlice - - struct DeserializerError: Swift.Error, CustomStringConvertible, LocalizedError { - - let error: JSONSequenceDeserializerStateMachine.ActionError - + + /// An error thrown by the deserializer. + struct DeserializerError: Swift.Error, CustomStringConvertible, + LocalizedError + where UpstreamIterator.Element == Element { + + /// The underlying error emitted by the state machine. + let error: Iterator.StateMachine.ActionError + var description: String { switch error { - case .missingInitialRS: - return "Missing an initial character, the bytes might not be a JSON Sequence." + case .missingInitialRS: return "Missing an initial character, the bytes might not be a JSON Sequence." } } - + var errorDescription: String? { description } } - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + /// The iterator of `JSONSequenceDeserializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == Element { + + /// The upstream iterator of arbitrary byte chunks. var upstream: UpstreamIterator - var stateMachine: JSONSequenceDeserializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { - case .returnNil: - return nil - case .returnEvent(let line): - return line + case .returnNil: return nil + case .emitLine(let line): return line case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnEvent(let line): - return line - case .noop: - continue + case .returnNil: return nil + case .emitLine(let line): return line + case .noop: continue } - case .emitError(let error): - throw DeserializerError(error: error) - case .noop: - continue + case .emitError(let error): throw DeserializerError(error: error) + case .noop: continue } } } } - + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } extension AsyncSequence where Element == ArraySlice { - func asParsedJSONSequence() -> JSONSequenceDeserializationSequence { - .init(upstream: self) - } - + + /// Returns another sequence that decodes each JSON Sequence event as the provided type using the provided decoder. + /// - Parameters: + /// - eventType: The type to decode the JSON event into. + /// - decoder: The JSON decoder to use. + /// - Returns: A sequence that provides the decoded JSON events. public func asDecodedJSONSequence( of eventType: Event.Type = Event.self, decoder: JSONDecoder = .init() ) -> AsyncThrowingMapSequence, Event> { - asParsedJSONSequence().map { line in - try decoder.decode(Event.self, from: Data(line)) - } + JSONSequenceDeserializationSequence(upstream: self) + .map { line in try decoder.decode(Event.self, from: Data(line)) } } } -struct JSONSequenceDeserializerStateMachine { - - enum State { - case initial([UInt8]) - case parsingEvent([UInt8]) - case finished - case mutating - } - private(set) var state: State = .initial([]) - - enum ActionError { - case missingInitialRS - } - - enum NextAction { - case returnNil - case returnEvent(ArraySlice) - case emitError(ActionError) - case needsMore - case noop - } - - mutating func next() -> NextAction { - switch state { - case .initial(var buffer): - guard !buffer.isEmpty else { - return .needsMore - } - guard buffer.first! == ASCII.rs else { - return .emitError(.missingInitialRS) - } - state = .mutating - buffer.removeFirst() - state = .parsingEvent(buffer) - return .noop - case .parsingEvent(var buffer): - state = .mutating - guard let indexOfRecordSeparator = buffer.firstIndex(of: ASCII.rs) else { - state = .parsingEvent(buffer) - return .needsMore - } - let event = buffer[..) - case noop - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .initial(var buffer): - if let value { + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .initial(buffer: []) } + + /// An error returned by the state machine. + enum ActionError { + + /// The initial boundary `` was not found. + case missingInitialRS + } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit a full line. + case emitLine(ArraySlice) + + /// Emit an error. + case emitError(ActionError) + + /// The line is not complete yet, needs more bytes. + case needsMore + + /// Rerun the parsing loop. + case noop + } + + /// Read the next line parsed from upstream bytes. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .initial(var buffer): + guard !buffer.isEmpty else { return .needsMore } + guard buffer.first! == ASCII.rs else { return .emitError(.missingInitialRS) } state = .mutating - buffer.append(contentsOf: value) - state = .initial(buffer) + buffer.removeFirst() + state = .parsingLine(buffer: buffer) return .noop - } else { - let line = ArraySlice(buffer) - buffer = [] - state = .finished - if line.isEmpty { - return .returnNil - } else { - return .returnEvent(line) + case .parsingLine(var buffer): + state = .mutating + guard let indexOfRecordSeparator = buffer.firstIndex(of: ASCII.rs) else { + state = .parsingLine(buffer: buffer) + return .needsMore } + let line = buffer[..) + + /// No action, rerun the parsing loop. + case noop + } + + /// Ingest the provided bytes. + /// - Parameter value: A new byte chunk. If `nil`, then the source of bytes is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .initial(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .initial(buffer: buffer) + return .noop + } else { + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { return .returnNil } else { return .emitLine(line) } + } + case .parsingLine(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .parsingLine(buffer: buffer) + return .noop } else { - return .returnEvent(line) + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { return .returnNil } else { return .emitLine(line) } } + case .finished, .mutating: preconditionFailure("Invalid state") } - case .finished, .mutating: - preconditionFailure("Invalid state") } } } diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift index 6e649408..4e26888c 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift @@ -14,50 +14,62 @@ import Foundation -public struct JSONSequenceSerializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } +/// A sequence that serializes lines by concatenating them using the JSON Sequence format. +public struct JSONSequenceSerializationSequence: Sendable +where Upstream.Element == ArraySlice { + + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of lines. + public init(upstream: Upstream) { self.upstream = upstream } } extension JSONSequenceSerializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ArraySlice - - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + + /// The iterator of `JSONSequenceSerializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == Element { + + /// The upstream iterator of lines. var upstream: UpstreamIterator - var stateMachine: JSONSequenceSerializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { - case .returnNil: - return nil + case .returnNil: return nil case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnBytes(let bytes): - return bytes + case .returnNil: return nil + case .emitBytes(let bytes): return bytes } } } } } - + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } -extension AsyncSequence where Element == ArraySlice { - - func asSerializedJSONSequence() -> JSONSequenceSerializationSequence { - .init(upstream: self) - } -} - extension AsyncSequence where Element: Encodable { + + /// Returns another sequence that encodes the events using the provided encoder into a JSON Sequence. + /// - Parameter encoder: The JSON encoder to use. + /// - Returns: A sequence that provides the serialized JSON Sequence. public func asEncodedJSONSequence( encoder: JSONEncoder = { let encoder = JSONEncoder() @@ -65,56 +77,77 @@ extension AsyncSequence where Element: Encodable { return encoder }() ) -> JSONSequenceSerializationSequence>> { - map { event in - try ArraySlice(encoder.encode(event)) - } - .asSerializedJSONSequence() + .init(upstream: map { event in try ArraySlice(encoder.encode(event)) }) } } -struct JSONSequenceSerializerStateMachine { - - enum State { - case running - case finished - } - private(set) var state: State = .running - - enum NextAction { - case returnNil - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .running: - return .needsMore - case .finished: - return .returnNil +extension JSONSequenceSerializationSequence.Iterator { + + /// A state machine representing the JSON Sequence serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State { + + /// Is emitting serialized JSON Sequence events. + case running + + /// Finished, the terminal state. + case finished } - } - - enum ReceivedValueAction { - case returnNil - case returnBytes(ArraySlice) - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .running: - if let value { - var buffer: [UInt8] = [] - buffer.reserveCapacity(value.count + 2) - buffer.append(ASCII.rs) - buffer.append(contentsOf: value) - buffer.append(ASCII.lf) - return .returnBytes(ArraySlice(buffer)) - } else { - state = .finished - return .returnNil + + /// The current state of the state machine. + private(set) var state: State + /// Creates a new state machine. + init() { self.state = .running } + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Needs more bytes. + case needsMore + } + + /// Read the next byte chunk serialized from upstream lines. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .running: return .needsMore + case .finished: return .returnNil + } + } + + /// An action returned by the `receivedValue` method. + enum ReceivedValueAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit the provided bytes. + case emitBytes(ArraySlice) + } + + /// Ingest the provided line. + /// - Parameter value: A new line. If `nil`, then the source of line is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer: [UInt8] = [] + buffer.reserveCapacity(value.count + 2) + buffer.append(ASCII.rs) + buffer.append(contentsOf: value) + buffer.append(ASCII.lf) + return .emitBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil + } + case .finished: preconditionFailure("Invalid state") } - case .finished: - preconditionFailure("Invalid state") } } } diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift index b3c77231..faea5b03 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift @@ -18,7 +18,8 @@ import Foundation final class Test_JSONSequenceDecoding: Test_Runtime { func testParsed() async throws { - let sequence = testJSONSequenceOneBytePerElementSequence.asParsedJSONSequence() + let upstream = testJSONSequenceOneBytePerElementSequence + let sequence = JSONSequenceDeserializationSequence(upstream: upstream) let events = try await [ArraySlice](collecting: sequence) XCTAssertEqual(events.count, 2) XCTAssertEqualData(events[0], "{\"name\":\"Rover\"}\n".utf8) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift index 3cf4b235..6bc7c7ac 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift @@ -18,12 +18,13 @@ import Foundation final class Test_JSONSequenceEncoding: Test_Runtime { func testSerialized() async throws { - let sequence = WrappedSyncSequence( + let upstream = WrappedSyncSequence( sequence: [ ArraySlice(#"{"name":"Rover"}"#.utf8), ArraySlice(#"{"name":"Pancake"}"#.utf8) ] - ).asSerializedJSONSequence() + ) + let sequence = JSONSequenceSerializationSequence(upstream: upstream) try await XCTAssertEqualAsyncData(sequence, testJSONSequenceBytes) } From f7571bb2843ba09e63403507f2735bfcf231f142 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 20 Dec 2023 13:22:18 +0100 Subject: [PATCH 13/18] Cleaned up code --- .../OpenAPIRuntime/Base/ByteUtilities.swift | 14 +- .../EventStreams/ServerSentEvents.swift | 18 +- .../ServerSentEventsDecoding.swift | 607 ++++++++++-------- .../ServerSentEventsEncoding.swift | 224 ++++--- .../EventStreams/Test_JSONLinesDecoding.swift | 1 - .../EventStreams/Test_JSONLinesEncoding.swift | 8 +- .../Test_JSONSequenceDecoding.swift | 2 - .../Test_JSONSequenceEncoding.swift | 11 +- .../Test_ServerSentEventsDecoding.swift | 140 ++-- .../Test_ServerSentEventsEncoding.swift | 98 ++- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 24 +- 11 files changed, 582 insertions(+), 565 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift index ee7300a7..2c745977 100644 --- a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift @@ -26,7 +26,6 @@ enum ASCII { /// The record separator `` character. static let rs: UInt8 = 0x1e - /// The colon `:` character. static let colon: UInt8 = 0x3a @@ -138,19 +137,12 @@ enum MatchOfOneOfResult { extension RandomAccessCollection where Element: Equatable { - func matchOfOneOf( - first: Element, - second: Element - ) -> MatchOfOneOfResult { + func matchOfOneOf(first: Element, second: Element) -> MatchOfOneOfResult { var index = startIndex while index < endIndex { let element = self[index] - if element == first { - return .first(index) - } - if element == second { - return .second(index) - } + if element == first { return .first(index) } + if element == second { return .second(index) } formIndex(after: &index) } return .noMatch diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift index 55483ec3..7d4d85fd 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// -/// An event sent by the server. +/// An event sent by the server that has a JSON payload in the data field. /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation public struct ServerSentEventWithJSONData: Sendable, Hashable { @@ -34,7 +34,13 @@ public struct ServerSentEventWithJSONData: Se /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface public var retry: Int64? - + + /// Creates a new event. + /// - Parameters: + /// - event: A type of the event, helps inform how to interpret the data. + /// - data: The payload of the event. + /// - id: A unique identifier of the event. + /// - retry: The amount of time, in milliseconds, to wait before retrying. public init(event: String? = nil, data: JSONDataType? = nil, id: String? = nil, retry: Int64? = nil) { self.event = event self.data = data @@ -65,7 +71,13 @@ public struct ServerSentEvent: Sendable, Hashable { /// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#the-eventsource-interface public var retry: Int64? - + + /// Creates a new event. + /// - Parameters: + /// - event: A type of the event, helps inform how to interpret the data. + /// - data: The payload of the event. + /// - id: A unique identifier of the event. + /// - retry: The amount of time, in milliseconds, to wait before retrying. public init(id: String? = nil, event: String? = nil, data: String? = nil, retry: Int64? = nil) { self.id = id self.event = event diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 35c2ea1d..6f32ab34 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -14,43 +14,56 @@ import Foundation +/// A sequence that parses arbitrary byte chunks into events using the Server-sent Events format. +/// /// https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events +public struct ServerSentEventsDeserializationSequence: Sendable +where Upstream.Element == ArraySlice { -public struct ServerSentEventsDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of arbitrary byte chunks. + public init(upstream: Upstream) { self.upstream = upstream } } extension ServerSentEventsDeserializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ServerSentEvent - - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ArraySlice { + + /// The iterator of `ServerSentEventsDeserializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == ArraySlice { + + /// The upstream iterator of arbitrary byte chunks. var upstream: UpstreamIterator - var stateMachine: ServerSentEventsDeserializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ServerSentEvent? { while true { switch stateMachine.next() { - case .returnNil: - return nil - case .emitEvent(let event): - return event - case .noop: - continue + case .returnNil: return nil + case .emitEvent(let event): return event + case .noop: continue case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .noop: - continue + case .returnNil: return nil + case .noop: continue } } } } } - + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } @@ -58,10 +71,21 @@ extension ServerSentEventsDeserializationSequence: AsyncSequence { extension AsyncSequence where Element == ArraySlice { - public func asDecodedServerSentEvents() -> ServerSentEventsDeserializationSequence> { - .init(upstream: asParsedServerSentEventLines()) - } + /// Returns another sequence that decodes each event's data as the provided type using the provided decoder. + /// + /// Use this method if the event's `data` field is not JSON, or if you don't want to parse it using `asDecodedServerSentEventsWithJSONData`. + /// - Returns: A sequence that provides the events. + public func asDecodedServerSentEvents() -> ServerSentEventsDeserializationSequence< + ServerSentEventsLineDeserializationSequence + > { .init(upstream: ServerSentEventsLineDeserializationSequence(upstream: self)) } + /// Returns another sequence that decodes each event's data as the provided type using the provided decoder. + /// + /// Use this method if the event's `data` field is JSON. + /// - Parameters: + /// - dataType: The type to decode the JSON data into. + /// - decoder: The JSON decoder to use. + /// - Returns: A sequence that provides the events with the decoded JSON data. public func asDecodedServerSentEventsWithJSONData( of dataType: JSONDataType.Type = JSONDataType.self, decoder: JSONDecoder = .init() @@ -69,301 +93,354 @@ extension AsyncSequence where Element == ArraySlice { ServerSentEventsDeserializationSequence>, ServerSentEventWithJSONData > { - asDecodedServerSentEvents().map { event in - ServerSentEventWithJSONData( - event: event.event, - data: try event.data.flatMap { stringData in - try decoder.decode(JSONDataType.self, from: Data(stringData.utf8)) - }, - id: event.id, - retry: event.retry - ) - } + asDecodedServerSentEvents() + .map { event in + ServerSentEventWithJSONData( + event: event.event, + data: try event.data.flatMap { stringData in + try decoder.decode(JSONDataType.self, from: Data(stringData.utf8)) + }, + id: event.id, + retry: event.retry + ) + } } } -struct ServerSentEventsDeserializerStateMachine { - - enum State { - case accumulatingEvent(ServerSentEvent, buffer: [ArraySlice]) - case finished - case mutating - } - private(set) var state: State = .accumulatingEvent(.init(), buffer: []) - - enum NextAction { - case returnNil - case emitEvent(ServerSentEvent) - case needsMore - case noop - } - - mutating func next() -> NextAction { - switch state { - case .accumulatingEvent(var event, var buffer): - guard let line = buffer.first else { - return .needsMore - } - state = .mutating - buffer.removeFirst() - if line.isEmpty { - // Dispatch the accumulated event. - state = .accumulatingEvent(.init(), buffer: buffer) - // If the last character of data is a newline, strip it. - if event.data?.hasSuffix("\n") ?? false { - event.data?.removeLast() +extension ServerSentEventsDeserializationSequence.Iterator { + + /// A state machine representing the Server-sent Events deserializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Accumulating an event, which hasn't been emitted yet. + case accumulatingEvent(ServerSentEvent, buffer: [ArraySlice]) + + /// Finished, the terminal state. + case finished + + /// Helper state to avoid copy-on-write copies. + case mutating + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .accumulatingEvent(.init(), buffer: []) } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit a completed event. + case emitEvent(ServerSentEvent) + + /// The line is not complete yet, needs more bytes. + case needsMore + + /// Rerun the parsing loop. + case noop + } + + /// Read the next line parsed from upstream bytes. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .accumulatingEvent(var event, var buffer): + guard let line = buffer.first else { return .needsMore } + state = .mutating + buffer.removeFirst() + if line.isEmpty { + // Dispatch the accumulated event. + state = .accumulatingEvent(.init(), buffer: buffer) + // If the last character of data is a newline, strip it. + if event.data?.hasSuffix("\n") ?? false { event.data?.removeLast() } + return .emitEvent(event) } - return .emitEvent(event) - } - if line.first! == ASCII.colon { - // A comment, skip this line. - state = .accumulatingEvent(event, buffer: buffer) - return .noop - } - // Parse the field name and value. - let field: String - let value: String? - if let indexOfFirstColon = line.firstIndex(of: ASCII.colon) { - field = String(decoding: line[.. - if valueBytes.isEmpty { - resolvedValueBytes = [] - } else if valueBytes.first! == ASCII.space { - resolvedValueBytes = valueBytes.dropFirst() - } else { - resolvedValueBytes = valueBytes + if line.first! == ASCII.colon { + // A comment, skip this line. + state = .accumulatingEvent(event, buffer: buffer) + return .noop } - value = String(decoding: resolvedValueBytes, as: UTF8.self) - } else { - field = String(decoding: line, as: UTF8.self) - value = nil - } - guard let value else { - // An unknown type of event, skip. - state = .accumulatingEvent(event, buffer: buffer) - return .noop - } - // Process the field. - switch field { - case "event": - event.event = value - case "data": - var data = event.data ?? "" - data.append(value) - data.append("\n") - event.data = data - case "id": - event.id = value - case "retry": - if let retry = Int64(value) { - event.retry = retry + // Parse the field name and value. + let field: String + let value: String? + if let indexOfFirstColon = line.firstIndex(of: ASCII.colon) { + field = String(decoding: line[.. + if valueBytes.isEmpty { + resolvedValueBytes = [] + } else if valueBytes.first! == ASCII.space { + resolvedValueBytes = valueBytes.dropFirst() + } else { + resolvedValueBytes = valueBytes + } + value = String(decoding: resolvedValueBytes, as: UTF8.self) } else { - // Skip this line. - fallthrough + field = String(decoding: line, as: UTF8.self) + value = nil } - default: - // An unknown or invalid field, skip. + guard let value else { + // An unknown type of event, skip. + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } + // Process the field. + switch field { + case "event": event.event = value + case "data": + var data = event.data ?? "" + data.append(value) + data.append("\n") + event.data = data + case "id": event.id = value + case "retry": + if let retry = Int64(value) { + event.retry = retry + } else { + // Skip this line. + fallthrough + } + default: + // An unknown or invalid field, skip. + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } + // Processed the field, continue. state = .accumulatingEvent(event, buffer: buffer) return .noop + case .finished: return .returnNil + case .mutating: preconditionFailure("Invalid state") } - // Processed the field, continue. - state = .accumulatingEvent(event, buffer: buffer) - return .noop - case .finished: - return .returnNil - case .mutating: - preconditionFailure("Invalid state") } - } - - enum ReceivedValueAction { - case returnNil - case noop - } - - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .accumulatingEvent(let event, var buffer): - if let value { - state = .mutating - buffer.append(value) - state = .accumulatingEvent(event, buffer: buffer) - return .noop - } else { - // If no value is received, drop the existing event on the floor. - // The specification explicitly states this. - // > Once the end of the file is reached, any pending data must be discarded. - // > (If the file ends in the middle of an event, before the final empty line, - // > the incomplete event is not dispatched.) - // Source: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation - state = .finished - return .returnNil + + /// An action returned by the `receivedValue` method. + enum ReceivedValueAction { + + /// Return nil to the caller, no more lines. + case returnNil + + /// No action, rerun the parsing loop. + case noop + } + + /// Ingest the provided bytes. + /// - Parameter value: A new byte chunk. If `nil`, then the source of bytes is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .accumulatingEvent(let event, var buffer): + if let value { + state = .mutating + buffer.append(value) + state = .accumulatingEvent(event, buffer: buffer) + return .noop + } else { + // If no value is received, drop the existing event on the floor. + // The specification explicitly states this. + // > Once the end of the file is reached, any pending data must be discarded. + // > (If the file ends in the middle of an event, before the final empty line, + // > the incomplete event is not dispatched.) + // Source: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + state = .finished + return .returnNil + } + case .finished, .mutating: preconditionFailure("Invalid state") } - case .finished, .mutating: - preconditionFailure("Invalid state") } } } -public struct ServerSentEventsLineDeserializationSequence: Sendable where Upstream.Element == ArraySlice { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } +/// A sequence that parses arbitrary byte chunks into lines using the Server-sent Events format. +public struct ServerSentEventsLineDeserializationSequence: Sendable +where Upstream.Element == ArraySlice { + + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of arbitrary byte chunks. + public init(upstream: Upstream) { self.upstream = upstream } } extension ServerSentEventsLineDeserializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ArraySlice - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == Element { + /// The iterator of `ServerSentEventsLineDeserializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == Element { + + /// The upstream iterator of arbitrary byte chunks. var upstream: UpstreamIterator - var stateMachine: ServerSentEventsLineDeserializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .noop: - continue + case .returnNil: return nil + case .returnLine(let line): return line + case .noop: continue case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnLine(let line): - return line - case .noop: - continue + case .returnNil: return nil + case .returnLine(let line): return line + case .noop: continue } } } } } + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } -extension AsyncSequence where Element == ArraySlice { - func asParsedServerSentEventLines() -> ServerSentEventsLineDeserializationSequence { - .init(upstream: self) - } -} +extension ServerSentEventsLineDeserializationSequence.Iterator { -/// A state machine for parsing lines in server-sent events. -/// -/// https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream -/// -/// This is not trivial to do with a streaming parser, as the end of line can be: -/// - LF -/// - CR -/// - CRLF -/// -/// So when we get CR, but have no more data, we want to be able to emit the previous line, -/// however we need to discard a LF if one comes. -struct ServerSentEventsLineDeserializerStateMachine { - - enum State { - case waitingForEndOfLine([UInt8]) - case consumedCR([UInt8]) - case finished - case mutating - } - private(set) var state: State = .waitingForEndOfLine([]) + /// A state machine for parsing lines in Server-Sent Events. + /// + /// https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream + /// + /// This is not trivial to do with a streaming parser, as the end of line can be: + /// - LF + /// - CR + /// - CRLF + /// + /// So when we get CR, but have no more data, we want to be able to emit the previous line, + /// however we need to discard a LF if one comes. + struct StateMachine { - enum NextAction { - case returnNil - case returnLine(ArraySlice) - case needsMore - case noop - } + /// A state machine representing the Server-sent Events deserializer. + enum State { - mutating func next() -> NextAction { - switch state { - case .waitingForEndOfLine(var buffer): - switch buffer.matchOfOneOf(first: ASCII.lf, second: ASCII.cr) { - case .noMatch: - return .needsMore - case .first(let index): - // Just a LF, so consume the line and move onto the next line. - state = .mutating - let line = buffer[..` character, so possibly the end of line. + case consumedCR(buffer: [UInt8]) + + /// Finished, the terminal state. + case finished + + /// Helper state to avoid copy-on-write copies. + case mutating } - } - enum ReceivedValueAction { - case returnNil - case returnLine(ArraySlice) - case noop - } + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .waitingForEndOfLine(buffer: []) } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil - mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { - switch state { - case .waitingForEndOfLine(var buffer): - if let value { + /// Emit a full line. + case emitLine(ArraySlice) + + /// The line is not complete yet, needs more bytes. + case needsMore + + /// No action, rerun the parsing loop. + case noop + } + + mutating func next() -> NextAction { + switch state { + case .waitingForEndOfLine(var buffer): + switch buffer.matchOfOneOf(first: ASCII.lf, second: ASCII.cr) { + case .noMatch: return .needsMore + case .first(let index): + // Just a LF, so consume the line and move onto the next line. + state = .mutating + let line = buffer[..) + + /// No action, rerun the parsing loop. + case noop + } + + /// Ingest the provided bytes. + /// - Parameter value: A new byte chunk. If `nil`, then the source of bytes is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ArraySlice?) -> ReceivedValueAction { + switch state { + case .waitingForEndOfLine(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .waitingForEndOfLine(buffer: buffer) + return .noop } else { - return .returnLine(line) + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { return .returnNil } else { return .emitLine(line) } } - } - case .consumedCR(var buffer): - if let value { - state = .mutating - buffer.append(contentsOf: value) - state = .consumedCR(buffer) - return .noop - } else { - let line = ArraySlice(buffer) - buffer = [] - state = .finished - if line.isEmpty { - return .returnNil + case .consumedCR(var buffer): + if let value { + state = .mutating + buffer.append(contentsOf: value) + state = .consumedCR(buffer: buffer) + return .noop } else { - return .returnLine(line) + let line = ArraySlice(buffer) + buffer = [] + state = .finished + if line.isEmpty { return .returnNil } else { return .emitLine(line) } } + case .finished, .mutating: preconditionFailure("Invalid state") } - case .finished, .mutating: - preconditionFailure("Invalid state") } } } diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift index 29ef0cfc..47a52918 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -14,138 +14,168 @@ import Foundation -public struct ServerSentEventsSerializationSequence: Sendable where Upstream.Element == ServerSentEvent { - var upstream: Upstream - public init(upstream: Upstream) { - self.upstream = upstream - } +/// A sequence that serializes Server-sent Events. +public struct ServerSentEventsSerializationSequence: Sendable +where Upstream.Element == ServerSentEvent { + + /// The upstream sequence. + private let upstream: Upstream + + /// Creates a new sequence. + /// - Parameter upstream: The upstream sequence of events. + public init(upstream: Upstream) { self.upstream = upstream } } extension ServerSentEventsSerializationSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. public typealias Element = ArraySlice - - public struct Iterator: AsyncIteratorProtocol where UpstreamIterator.Element == ServerSentEvent { + + /// The iterator of `ServerSentEventsSerializationSequence`. + public struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == ServerSentEvent { + + /// The upstream iterator of lines. var upstream: UpstreamIterator - var stateMachine: ServerSentEventsSerializerStateMachine = .init() + + /// The state machine of the iterator. + var stateMachine: StateMachine = .init() + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. public mutating func next() async throws -> ArraySlice? { while true { switch stateMachine.next() { - case .returnNil: - return nil + case .returnNil: return nil case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { - case .returnNil: - return nil - case .returnBytes(let bytes): - return bytes + case .returnNil: return nil + case .returnBytes(let bytes): return bytes } } } } } - + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. public func makeAsyncIterator() -> Iterator { Iterator(upstream: upstream.makeAsyncIterator()) } } -extension AsyncSequence where Element == ServerSentEvent { - - public func asEncodedServerSentEvents() -> ServerSentEventsSerializationSequence { - .init(upstream: self) - } -} - extension AsyncSequence { + + /// Returns another sequence that encodes Server-sent Events that have a JSON value in the data field. + /// - Parameter encoder: The JSON encoder to use. + /// - Returns: A sequence that provides the serialized JSON Lines. public func asEncodedServerSentEventsWithJSONData( encoder: JSONEncoder = { let encoder = JSONEncoder() encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes] return encoder }() - ) -> ServerSentEventsSerializationSequence> where Element == ServerSentEventWithJSONData - { - map { event in - ServerSentEvent( - id: event.id, - event: event.event, - data: try event.data.flatMap { try String(decoding: encoder.encode($0), as: UTF8.self) }, - retry: event.retry - ) - } - .asEncodedServerSentEvents() + ) -> ServerSentEventsSerializationSequence> + where Element == ServerSentEventWithJSONData { + ServerSentEventsSerializationSequence( + upstream: map { event in + ServerSentEvent( + id: event.id, + event: event.event, + data: try event.data.flatMap { try String(decoding: encoder.encode($0), as: UTF8.self) }, + retry: event.retry + ) + } + ) } } -struct ServerSentEventsSerializerStateMachine { - - enum State { - case running - case finished - } - private(set) var state: State = .running - - enum NextAction { - case returnNil - case needsMore - } - - mutating func next() -> NextAction { - switch state { - case .running: - return .needsMore - case .finished: - return .returnNil +extension ServerSentEventsSerializationSequence.Iterator { + + /// A state machine representing the JSON Lines serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State { + + /// Is emitting serialized JSON Lines events. + case running + + /// Finished, the terminal state. + case finished } - } - - enum ReceivedValueAction { - case returnNil - case returnBytes(ArraySlice) - } - - mutating func receivedValue(_ value: ServerSentEvent?) -> ReceivedValueAction { - switch state { - case .running: - if let value { - var buffer: [UInt8] = [] - func encodeField(name: String, value: some StringProtocol) { - buffer.append(contentsOf: name.utf8) - buffer.append(ASCII.colon) - buffer.append(ASCII.space) - buffer.append(contentsOf: value.utf8) - buffer.append(ASCII.lf) - } - if let id = value.id { - encodeField(name: "id", value: id) - } - if let event = value.event { - encodeField(name: "event", value: event) - } - if let retry = value.retry { - encodeField(name: "retry", value: String(retry)) - } - if let data = value.data { - // Normalize the data section by replacing CRLF and CR with just LF. - // Then split the section into individual field/value pairs. - let lines = data - .replacingOccurrences(of: "\r\n", with: "\n") - .replacingOccurrences(of: "\r", with: "\n") - .split(separator: "\n", omittingEmptySubsequences: false) - for line in lines { - encodeField(name: "data", value: line) + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .running } + + /// An action returned by the `next` method. + enum NextAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Needs more bytes. + case needsMore + } + + /// Read the next byte chunk serialized from upstream lines. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .running: return .needsMore + case .finished: return .returnNil + } + } + + /// An action returned by the `receivedValue` method. + enum ReceivedValueAction { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit the provided bytes. + case returnBytes(ArraySlice) + } + + /// Ingest the provided event. + /// - Parameter value: A new event. If `nil`, then the source of events is finished. + /// - Returns: An action to perform. + mutating func receivedValue(_ value: ServerSentEvent?) -> ReceivedValueAction { + switch state { + case .running: + if let value { + var buffer: [UInt8] = [] + func encodeField(name: String, value: some StringProtocol) { + buffer.append(contentsOf: name.utf8) + buffer.append(ASCII.colon) + buffer.append(ASCII.space) + buffer.append(contentsOf: value.utf8) + buffer.append(ASCII.lf) } + if let id = value.id { encodeField(name: "id", value: id) } + if let event = value.event { encodeField(name: "event", value: event) } + if let retry = value.retry { encodeField(name: "retry", value: String(retry)) } + if let data = value.data { + // Normalize the data section by replacing CRLF and CR with just LF. + // Then split the section into individual field/value pairs. + let lines = data.replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") + .split(separator: "\n", omittingEmptySubsequences: false) + for line in lines { encodeField(name: "data", value: line) } + } + // End the event. + buffer.append(ASCII.lf) + return .returnBytes(ArraySlice(buffer)) + } else { + state = .finished + return .returnNil } - // End the event. - buffer.append(ASCII.lf) - return .returnBytes(ArraySlice(buffer)) - } else { - state = .finished - return .returnNil + case .finished: preconditionFailure("Invalid state") } - case .finished: - preconditionFailure("Invalid state") } } } diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift index ad810eb2..a841dc67 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesDecoding.swift @@ -16,7 +16,6 @@ import XCTest import Foundation final class Test_JSONLinesDecoding: Test_Runtime { - func testParsed() async throws { let upstream = asOneBytePerElementSequence(ArraySlice("hello\nworld\n".utf8)) let sequence = JSONLinesDeserializationSequence(upstream: upstream) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift index 9a1762bf..d7a44319 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONLinesEncoding.swift @@ -16,14 +16,8 @@ import XCTest import Foundation final class Test_JSONLinesEncoding: Test_Runtime { - func testSerialized() async throws { - let upstream = WrappedSyncSequence( - sequence: [ - ArraySlice("hello".utf8), - ArraySlice("world".utf8) - ] - ) + let upstream = WrappedSyncSequence(sequence: [ArraySlice("hello".utf8), ArraySlice("world".utf8)]) let sequence = JSONLinesSerializationSequence(upstream: upstream) try await XCTAssertEqualAsyncData(sequence, "hello\nworld\n".utf8) } diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift index faea5b03..ab967e1e 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceDecoding.swift @@ -16,7 +16,6 @@ import XCTest import Foundation final class Test_JSONSequenceDecoding: Test_Runtime { - func testParsed() async throws { let upstream = testJSONSequenceOneBytePerElementSequence let sequence = JSONSequenceDeserializationSequence(upstream: upstream) @@ -25,7 +24,6 @@ final class Test_JSONSequenceDecoding: Test_Runtime { XCTAssertEqualData(events[0], "{\"name\":\"Rover\"}\n".utf8) XCTAssertEqualData(events[1], "{\"name\":\"Pancake\"}\n".utf8) } - func testTyped() async throws { let sequence = testJSONSequenceOneBytePerElementSequence.asDecodedJSONSequence(of: TestPet.self) let events = try await [TestPet](collecting: sequence) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift index 6bc7c7ac..f03e00cd 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_JSONSequenceEncoding.swift @@ -16,18 +16,13 @@ import XCTest import Foundation final class Test_JSONSequenceEncoding: Test_Runtime { - func testSerialized() async throws { - let upstream = WrappedSyncSequence( - sequence: [ - ArraySlice(#"{"name":"Rover"}"#.utf8), - ArraySlice(#"{"name":"Pancake"}"#.utf8) - ] - ) + let upstream = WrappedSyncSequence(sequence: [ + ArraySlice(#"{"name":"Rover"}"#.utf8), ArraySlice(#"{"name":"Pancake"}"#.utf8), + ]) let sequence = JSONSequenceSerializationSequence(upstream: upstream) try await XCTAssertEqualAsyncData(sequence, testJSONSequenceBytes) } - func testTyped() async throws { let sequence = testEventsAsyncSequence.asEncodedJSONSequence() try await XCTAssertEqualAsyncData(sequence, testJSONSequenceBytes) diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift index 3450f14f..be98e6f1 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsDecoding.swift @@ -16,15 +16,8 @@ import XCTest import Foundation final class Test_ServerSentEventsDecoding: Test_Runtime { - - func _test( - input: String, - output: [ServerSentEvent], - file: StaticString = #file, - line: UInt = #line - ) async throws { - let sequence = asOneBytePerElementSequence(ArraySlice(input.utf8)) - .asDecodedServerSentEvents() + func _test(input: String, output: [ServerSentEvent], file: StaticString = #file, line: UInt = #line) async throws { + let sequence = asOneBytePerElementSequence(ArraySlice(input.utf8)).asDecodedServerSentEvents() let events = try await [ServerSentEvent](collecting: sequence) XCTAssertEqual(events.count, output.count, file: file, line: line) for (index, linePair) in zip(events, output).enumerated() { @@ -32,71 +25,63 @@ final class Test_ServerSentEventsDecoding: Test_Runtime { XCTAssertEqual(actualEvent, expectedEvent, "Event: \(index)", file: file, line: line) } } - func test() async throws { // Simple event. try await _test( input: #""" - data: hello - data: world - - - """#, - output: [ - .init(data: "hello\nworld") - ] + data: hello + data: world + + + """#, + output: [.init(data: "hello\nworld")] ) // Two simple events. try await _test( input: #""" - data: hello - data: world - - data: hello2 - data: world2 - - - """#, - output: [ - .init(data: "hello\nworld"), - .init(data: "hello2\nworld2") - ] + data: hello + data: world + + data: hello2 + data: world2 + + + """#, + output: [.init(data: "hello\nworld"), .init(data: "hello2\nworld2")] ) // Incomplete event is not emitted. try await _test( input: #""" - data: hello - """#, + data: hello + """#, output: [] ) // A few events. try await _test( input: #""" - retry: 5000 + retry: 5000 + + data: This is the first message. - data: This is the first message. + data: This is the second + data: message. - data: This is the second - data: message. + event: customEvent + data: This is a custom event message. - event: customEvent - data: This is a custom event message. + id: 123 + data: This is a message with an ID. - id: 123 - data: This is a message with an ID. - - """#, + """#, output: [ - .init(retry: 5000), - .init(data: "This is the first message."), + .init(retry: 5000), .init(data: "This is the first message."), .init(data: "This is the second\nmessage."), .init(event: "customEvent", data: "This is a custom event message."), - .init(id: "123", data: "This is a message with an ID.") + .init(id: "123", data: "This is a message with an ID."), ] ) } - func _testJSONData( input: String, output: [ServerSentEventWithJSONData], @@ -112,45 +97,35 @@ final class Test_ServerSentEventsDecoding: Test_Runtime { XCTAssertEqual(actualEvent, expectedEvent, "Event: \(index)", file: file, line: line) } } - - struct TestEvent: Decodable, Hashable, Sendable { - var index: Int - } - + struct TestEvent: Decodable, Hashable, Sendable { var index: Int } func testJSONData() async throws { // Simple event. try await _testJSONData( input: #""" - event: event1 - id: 1 - data: {"index":1} + event: event1 + id: 1 + data: {"index":1} - event: event2 - id: 2 - data: { - data: "index": 2 - data: } + event: event2 + id: 2 + data: { + data: "index": 2 + data: } - """#, + """#, output: [ .init(event: "event1", data: TestEvent(index: 1), id: "1"), - .init(event: "event2", data: TestEvent(index: 2), id: "2") + .init(event: "event2", data: TestEvent(index: 2), id: "2"), ] ) } } final class Test_ServerSentEventsDecoding_Lines: Test_Runtime { - - func _test( - input: String, - output: [String], - file: StaticString = #file, - line: UInt = #line - ) async throws { - let sequence = asOneBytePerElementSequence(ArraySlice(input.utf8)) - .asParsedServerSentEventLines() + func _test(input: String, output: [String], file: StaticString = #file, line: UInt = #line) async throws { + let upstream = asOneBytePerElementSequence(ArraySlice(input.utf8)) + let sequence = ServerSentEventsLineDeserializationSequence(upstream: upstream) let lines = try await [ArraySlice](collecting: sequence) XCTAssertEqual(lines.count, output.count, file: file, line: line) for (index, linePair) in zip(lines, output).enumerated() { @@ -158,31 +133,12 @@ final class Test_ServerSentEventsDecoding_Lines: Test_Runtime { XCTAssertEqualData(actualLine, expectedLine.utf8, "Line: \(index)", file: file, line: line) } } - func test() async throws { // LF - try await _test( - input: "hello\nworld\n", - output: [ - "hello", - "world" - ] - ) + try await _test(input: "hello\nworld\n", output: ["hello", "world"]) // CR - try await _test( - input: "hello\rworld\r", - output: [ - "hello", - "world" - ] - ) + try await _test(input: "hello\rworld\r", output: ["hello", "world"]) // CRLF - try await _test( - input: "hello\r\nworld\r\n", - output: [ - "hello", - "world" - ] - ) + try await _test(input: "hello\r\nworld\r\n", output: ["hello", "world"]) } } diff --git a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift index 72106df9..db88cd60 100644 --- a/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift +++ b/Tests/OpenAPIRuntimeTests/EventStreams/Test_ServerSentEventsEncoding.swift @@ -16,110 +16,88 @@ import XCTest import Foundation final class Test_ServerSentEventsEncoding: Test_Runtime { - - func _test( - input: [ServerSentEvent], - output: String, - file: StaticString = #file, - line: UInt = #line - ) async throws { - let sequence = WrappedSyncSequence( - sequence: input - ).asEncodedServerSentEvents() + func _test(input: [ServerSentEvent], output: String, file: StaticString = #file, line: UInt = #line) async throws { + let sequence = WrappedSyncSequence(sequence: input).asEncodedServerSentEvents() try await XCTAssertEqualAsyncData(sequence, output.utf8, file: file, line: line) } - func test() async throws { // Simple event. try await _test( - input: [ - .init(data: "hello\nworld") - ], + input: [.init(data: "hello\nworld")], output: #""" - data: hello - data: world - - - """# + data: hello + data: world + + + """# ) // Two simple events. try await _test( - input: [ - .init(data: "hello\nworld"), - .init(data: "hello2\nworld2") - ], + input: [.init(data: "hello\nworld"), .init(data: "hello2\nworld2")], output: #""" - data: hello - data: world - - data: hello2 - data: world2 + data: hello + data: world + + data: hello2 + data: world2 - - """# + + """# ) // A few events. try await _test( input: [ - .init(retry: 5000), - .init(data: "This is the first message."), + .init(retry: 5000), .init(data: "This is the first message."), .init(data: "This is the second\nmessage."), .init(event: "customEvent", data: "This is a custom event message."), - .init(id: "123", data: "This is a message with an ID.") + .init(id: "123", data: "This is a message with an ID."), ], output: #""" - retry: 5000 + retry: 5000 + + data: This is the first message. - data: This is the first message. + data: This is the second + data: message. - data: This is the second - data: message. + event: customEvent + data: This is a custom event message. - event: customEvent - data: This is a custom event message. + id: 123 + data: This is a message with an ID. - id: 123 - data: This is a message with an ID. - - """# + """# ) } - func _testJSONData( input: [ServerSentEventWithJSONData], output: String, file: StaticString = #file, line: UInt = #line ) async throws { - let sequence = WrappedSyncSequence( - sequence: input - ).asEncodedServerSentEventsWithJSONData() + let sequence = WrappedSyncSequence(sequence: input).asEncodedServerSentEventsWithJSONData() try await XCTAssertEqualAsyncData(sequence, output.utf8, file: file, line: line) } - - struct TestEvent: Encodable, Hashable, Sendable { - var index: Int - } - + struct TestEvent: Encodable, Hashable, Sendable { var index: Int } func testJSONData() async throws { // Simple event. try await _testJSONData( input: [ .init(event: "event1", data: TestEvent(index: 1), id: "1"), - .init(event: "event2", data: TestEvent(index: 2), id: "2") + .init(event: "event2", data: TestEvent(index: 2), id: "2"), ], output: #""" - id: 1 - event: event1 - data: {"index":1} + id: 1 + event: event1 + data: {"index":1} - id: 2 - event: event2 - data: {"index":2} + id: 2 + event: event2 + data: {"index":2} - """# + """# ) } } diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index c3356f7f..7f1f2255 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -115,17 +115,13 @@ class Test_Runtime: XCTestCase { var testStructURLFormData: Data { Data(testStructURLFormString.utf8) } var testEvents: [TestPet] { [.init(name: "Rover"), .init(name: "Pancake")] } - - var testEventsAsyncSequence: WrappedSyncSequence<[TestPet]> { - WrappedSyncSequence(sequence: testEvents) - } + var testEventsAsyncSequence: WrappedSyncSequence<[TestPet]> { WrappedSyncSequence(sequence: testEvents) } var testJSONLinesBytes: ArraySlice { let encoder = JSONEncoder() let bytes = try! testEvents.map { try encoder.encode($0) + [ASCII.lf] }.joined() return ArraySlice(bytes) } - var testJSONSequenceBytes: ArraySlice { let encoder = JSONEncoder() let bytes = try! testEvents.map { try [ASCII.rs] + encoder.encode($0) + [ASCII.lf] }.joined() @@ -134,21 +130,13 @@ class Test_Runtime: XCTestCase { func asOneBytePerElementSequence(_ source: ArraySlice) -> HTTPBody { HTTPBody( - WrappedSyncSequence(sequence: source) - .map { ArraySlice([$0]) }, + WrappedSyncSequence(sequence: source).map { ArraySlice([$0]) }, length: .known(Int64(source.count)), iterationBehavior: .multiple ) } - - var testJSONLinesOneBytePerElementSequence: HTTPBody { - asOneBytePerElementSequence(testJSONLinesBytes) - } - - var testJSONSequenceOneBytePerElementSequence: HTTPBody { - asOneBytePerElementSequence(testJSONSequenceBytes) - } - + var testJSONLinesOneBytePerElementSequence: HTTPBody { asOneBytePerElementSequence(testJSONLinesBytes) } + var testJSONSequenceOneBytePerElementSequence: HTTPBody { asOneBytePerElementSequence(testJSONSequenceBytes) } @discardableResult func _testPrettyEncoded(_ value: Value, expectedJSON: String) throws -> String { let encoder = JSONEncoder() @@ -460,9 +448,7 @@ public func XCTAssertEqualData( extension Array { init(collecting source: Source) async throws where Source.Element == Element { var elements: [Element] = [] - for try await element in source { - elements.append(element) - } + for try await element in source { elements.append(element) } self = elements } } From ec1c6dc9bbb569dfe25e62dde752f105d66e73e5 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 20 Dec 2023 13:25:00 +0100 Subject: [PATCH 14/18] Add more docs --- Sources/OpenAPIRuntime/Base/ByteUtilities.swift | 11 ++++++++++- .../EventStreams/ServerSentEvents.swift | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift index 2c745977..52486cc3 100644 --- a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift @@ -26,6 +26,7 @@ enum ASCII { /// The record separator `` character. static let rs: UInt8 = 0x1e + /// The colon `:` character. static let colon: UInt8 = 0x3a @@ -131,12 +132,20 @@ enum MatchOfOneOfResult { /// No match found at any position in self. case noMatch + /// The first option matched. case first(C.Index) + + /// The second option matched. case second(C.Index) } extension RandomAccessCollection where Element: Equatable { - + + /// Returns the index of the first match of one of two elements. + /// - Parameters: + /// - first: The first element to match. + /// - second: The second element to match. + /// - Returns: The result. func matchOfOneOf(first: Element, second: Element) -> MatchOfOneOfResult { var index = startIndex while index < endIndex { diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift index 7d4d85fd..bced26f9 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEvents.swift @@ -74,9 +74,9 @@ public struct ServerSentEvent: Sendable, Hashable { /// Creates a new event. /// - Parameters: + /// - id: A unique identifier of the event. /// - event: A type of the event, helps inform how to interpret the data. /// - data: The payload of the event. - /// - id: A unique identifier of the event. /// - retry: The amount of time, in milliseconds, to wait before retrying. public init(id: String? = nil, event: String? = nil, data: String? = nil, retry: Int64? = nil) { self.id = id From 0e39db03a6ffb521ae18a82c562833395cdaab27 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 5 Jan 2024 10:06:22 +0100 Subject: [PATCH 15/18] Fixes --- .../EventStreams/ServerSentEventsDecoding.swift | 4 ++-- .../EventStreams/ServerSentEventsEncoding.swift | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index 6f32ab34..f3bcdc44 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -290,13 +290,13 @@ extension ServerSentEventsLineDeserializationSequence: AsyncSequence { while true { switch stateMachine.next() { case .returnNil: return nil - case .returnLine(let line): return line + case .emitLine(let line): return line case .noop: continue case .needsMore: let value = try await upstream.next() switch stateMachine.receivedValue(value) { case .returnNil: return nil - case .returnLine(let line): return line + case .emitLine(let line): return line case .noop: continue } } diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift index 47a52918..1d848433 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -67,9 +67,16 @@ extension ServerSentEventsSerializationSequence: AsyncSequence { extension AsyncSequence { + /// Returns another sequence that encodes Server-sent Events with generic data in the data field. + /// - Returns: A sequence that provides the serialized Server-sent Events. + public func asEncodedServerSentEvents() -> ServerSentEventsSerializationSequence + where Element == ServerSentEvent { + .init(upstream: self) + } + /// Returns another sequence that encodes Server-sent Events that have a JSON value in the data field. /// - Parameter encoder: The JSON encoder to use. - /// - Returns: A sequence that provides the serialized JSON Lines. + /// - Returns: A sequence that provides the serialized Server-sent Events. public func asEncodedServerSentEventsWithJSONData( encoder: JSONEncoder = { let encoder = JSONEncoder() From 73ab685539d65787fa92ab1284fc4c6496740dcd Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 5 Jan 2024 12:32:21 +0100 Subject: [PATCH 16/18] Fix up linux CI: --- .../OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift | 7 ++++++- .../OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift | 8 ++++++-- .../EventStreams/JSONSequenceDecoding.swift | 8 +++++++- .../EventStreams/JSONSequenceEncoding.swift | 8 ++++++-- .../EventStreams/ServerSentEventsDecoding.swift | 9 +++++++-- .../EventStreams/ServerSentEventsEncoding.swift | 6 +++++- 6 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift index e40c16f0..eed9acdc 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesDecoding.swift @@ -12,7 +12,12 @@ // //===----------------------------------------------------------------------===// -import Foundation +#if canImport(Darwin) +import class Foundation.JSONDecoder +#else +@preconcurrency import class Foundation.JSONDecoder +#endif +import struct Foundation.Data /// A sequence that parses arbitrary byte chunks into lines using the JSON Lines format. public struct JSONLinesDeserializationSequence: Sendable diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift index 32244ee5..f1d9b9b8 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONLinesEncoding.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// -import Foundation +#if canImport(Darwin) +import class Foundation.JSONEncoder +#else +@preconcurrency import class Foundation.JSONEncoder +#endif /// A sequence that serializes lines by concatenating them using the JSON Lines format. public struct JSONLinesSerializationSequence: Sendable @@ -65,7 +69,7 @@ extension JSONLinesSerializationSequence: AsyncSequence { } } -extension AsyncSequence where Element: Encodable { +extension AsyncSequence where Element: Encodable & Sendable, Self: Sendable { /// Returns another sequence that encodes the events using the provided encoder into JSON Lines. /// - Parameter encoder: The JSON encoder to use. diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift index dfe960ef..4b34658c 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceDecoding.swift @@ -12,7 +12,13 @@ // //===----------------------------------------------------------------------===// -import Foundation +#if canImport(Darwin) +import class Foundation.JSONDecoder +#else +@preconcurrency import class Foundation.JSONDecoder +#endif +import protocol Foundation.LocalizedError +import struct Foundation.Data /// A sequence that parses arbitrary byte chunks into lines using the JSON Sequence format. public struct JSONSequenceDeserializationSequence: Sendable diff --git a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift index 4e26888c..a6ffe940 100644 --- a/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/JSONSequenceEncoding.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// -import Foundation +#if canImport(Darwin) +import class Foundation.JSONEncoder +#else +@preconcurrency import class Foundation.JSONEncoder +#endif /// A sequence that serializes lines by concatenating them using the JSON Sequence format. public struct JSONSequenceSerializationSequence: Sendable @@ -65,7 +69,7 @@ extension JSONSequenceSerializationSequence: AsyncSequence { } } -extension AsyncSequence where Element: Encodable { +extension AsyncSequence where Element: Encodable & Sendable, Self: Sendable { /// Returns another sequence that encodes the events using the provided encoder into a JSON Sequence. /// - Parameter encoder: The JSON encoder to use. diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift index f3bcdc44..421e5319 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsDecoding.swift @@ -12,7 +12,12 @@ // //===----------------------------------------------------------------------===// -import Foundation +#if canImport(Darwin) +import class Foundation.JSONDecoder +#else +@preconcurrency import class Foundation.JSONDecoder +#endif +import struct Foundation.Data /// A sequence that parses arbitrary byte chunks into events using the Server-sent Events format. /// @@ -69,7 +74,7 @@ extension ServerSentEventsDeserializationSequence: AsyncSequence { } } -extension AsyncSequence where Element == ArraySlice { +extension AsyncSequence where Element == ArraySlice, Self: Sendable { /// Returns another sequence that decodes each event's data as the provided type using the provided decoder. /// diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift index 1d848433..1f659a14 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -12,7 +12,11 @@ // //===----------------------------------------------------------------------===// -import Foundation +#if canImport(Darwin) +import class Foundation.JSONEncoder +#else +@preconcurrency import class Foundation.JSONEncoder +#endif /// A sequence that serializes Server-sent Events. public struct ServerSentEventsSerializationSequence: Sendable From a967d0d8059d4dcb21ffe6b3b29224565343718f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 5 Jan 2024 13:40:50 +0100 Subject: [PATCH 17/18] fix soundness --- Sources/OpenAPIRuntime/Base/ByteUtilities.swift | 1 - .../EventStreams/ServerSentEventsEncoding.swift | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift index 52486cc3..657cd446 100644 --- a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift @@ -140,7 +140,6 @@ enum MatchOfOneOfResult { } extension RandomAccessCollection where Element: Equatable { - /// Returns the index of the first match of one of two elements. /// - Parameters: /// - first: The first element to match. diff --git a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift index 1f659a14..853d76d2 100644 --- a/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift +++ b/Sources/OpenAPIRuntime/EventStreams/ServerSentEventsEncoding.swift @@ -74,9 +74,7 @@ extension AsyncSequence { /// Returns another sequence that encodes Server-sent Events with generic data in the data field. /// - Returns: A sequence that provides the serialized Server-sent Events. public func asEncodedServerSentEvents() -> ServerSentEventsSerializationSequence - where Element == ServerSentEvent { - .init(upstream: self) - } + where Element == ServerSentEvent { .init(upstream: self) } /// Returns another sequence that encodes Server-sent Events that have a JSON value in the data field. /// - Parameter encoder: The JSON encoder to use. From dc3ae77dc2bf39fd93433260d9612b7e355ea058 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 5 Jan 2024 18:22:25 +0100 Subject: [PATCH 18/18] Apply suggestions from code review Co-authored-by: Si Beaumont --- Sources/OpenAPIRuntime/Base/ByteUtilities.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift index 657cd446..039c03f2 100644 --- a/Sources/OpenAPIRuntime/Base/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Base/ByteUtilities.swift @@ -126,7 +126,7 @@ extension RandomAccessCollection where Element: Equatable { } } -/// A value returned by the `longestMatchOfOneOf` method. +/// A value returned by the `matchOfOneOf` method. enum MatchOfOneOfResult { /// No match found at any position in self.