Skip to content

Commit 6be221f

Browse files
[Runtime] SOAR-0010: Event streams sequences (#91)
### Motivation Land changes approved in apple/swift-openapi-generator#495. ### Modifications Introduced the new APIs. ### Result Easy use of event streams. ### Test Plan Added unit tests for all. --------- Co-authored-by: Si Beaumont <[email protected]>
1 parent fd101c3 commit 6be221f

16 files changed

+1915
-8
lines changed

Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift renamed to Sources/OpenAPIRuntime/Base/ByteUtilities.swift

+34
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ enum ASCII {
2424
/// The line feed `<LF>` character.
2525
static let lf: UInt8 = 0x0a
2626

27+
/// The record separator `<RS>` character.
28+
static let rs: UInt8 = 0x1e
29+
2730
/// The colon `:` character.
2831
static let colon: UInt8 = 0x3a
2932

@@ -122,3 +125,34 @@ extension RandomAccessCollection where Element: Equatable {
122125
return .noMatch
123126
}
124127
}
128+
129+
/// A value returned by the `matchOfOneOf` method.
130+
enum MatchOfOneOfResult<C: RandomAccessCollection> {
131+
132+
/// No match found at any position in self.
133+
case noMatch
134+
135+
/// The first option matched.
136+
case first(C.Index)
137+
138+
/// The second option matched.
139+
case second(C.Index)
140+
}
141+
142+
extension RandomAccessCollection where Element: Equatable {
143+
/// Returns the index of the first match of one of two elements.
144+
/// - Parameters:
145+
/// - first: The first element to match.
146+
/// - second: The second element to match.
147+
/// - Returns: The result.
148+
func matchOfOneOf(first: Element, second: Element) -> MatchOfOneOfResult<Self> {
149+
var index = startIndex
150+
while index < endIndex {
151+
let element = self[index]
152+
if element == first { return .first(index) }
153+
if element == second { return .second(index) }
154+
formIndex(after: &index)
155+
}
156+
return .noMatch
157+
}
158+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(Darwin)
16+
import class Foundation.JSONDecoder
17+
#else
18+
@preconcurrency import class Foundation.JSONDecoder
19+
#endif
20+
import struct Foundation.Data
21+
22+
/// A sequence that parses arbitrary byte chunks into lines using the JSON Lines format.
23+
public struct JSONLinesDeserializationSequence<Upstream: AsyncSequence & Sendable>: Sendable
24+
where Upstream.Element == ArraySlice<UInt8> {
25+
26+
/// The upstream sequence.
27+
private let upstream: Upstream
28+
29+
/// Creates a new sequence.
30+
/// - Parameter upstream: The upstream sequence of arbitrary byte chunks.
31+
public init(upstream: Upstream) { self.upstream = upstream }
32+
}
33+
34+
extension JSONLinesDeserializationSequence: AsyncSequence {
35+
36+
/// The type of element produced by this asynchronous sequence.
37+
public typealias Element = ArraySlice<UInt8>
38+
39+
/// The iterator of `JSONLinesDeserializationSequence`.
40+
public struct Iterator<UpstreamIterator: AsyncIteratorProtocol>: AsyncIteratorProtocol
41+
where UpstreamIterator.Element == Element {
42+
43+
/// The upstream iterator of arbitrary byte chunks.
44+
var upstream: UpstreamIterator
45+
46+
/// The state machine of the iterator.
47+
var stateMachine: StateMachine = .init()
48+
49+
/// Asynchronously advances to the next element and returns it, or ends the
50+
/// sequence if there is no next element.
51+
public mutating func next() async throws -> ArraySlice<UInt8>? {
52+
while true {
53+
switch stateMachine.next() {
54+
case .returnNil: return nil
55+
case .emitLine(let line): return line
56+
case .needsMore:
57+
let value = try await upstream.next()
58+
switch stateMachine.receivedValue(value) {
59+
case .returnNil: return nil
60+
case .emitLine(let line): return line
61+
case .noop: continue
62+
}
63+
}
64+
}
65+
}
66+
}
67+
68+
/// Creates the asynchronous iterator that produces elements of this
69+
/// asynchronous sequence.
70+
public func makeAsyncIterator() -> Iterator<Upstream.AsyncIterator> {
71+
Iterator(upstream: upstream.makeAsyncIterator())
72+
}
73+
}
74+
75+
extension AsyncSequence where Element == ArraySlice<UInt8> {
76+
77+
/// Returns another sequence that decodes each JSON Lines event as the provided type using the provided decoder.
78+
/// - Parameters:
79+
/// - eventType: The type to decode the JSON event into.
80+
/// - decoder: The JSON decoder to use.
81+
/// - Returns: A sequence that provides the decoded JSON events.
82+
public func asDecodedJSONLines<Event: Decodable>(
83+
of eventType: Event.Type = Event.self,
84+
decoder: JSONDecoder = .init()
85+
) -> AsyncThrowingMapSequence<JSONLinesDeserializationSequence<Self>, Event> {
86+
JSONLinesDeserializationSequence(upstream: self)
87+
.map { line in try decoder.decode(Event.self, from: Data(line)) }
88+
}
89+
}
90+
91+
extension JSONLinesDeserializationSequence.Iterator {
92+
93+
/// A state machine representing the JSON Lines deserializer.
94+
struct StateMachine {
95+
96+
/// The possible states of the state machine.
97+
enum State: Hashable {
98+
99+
/// Is waiting for the end of line.
100+
case waitingForDelimiter(buffer: [UInt8])
101+
102+
/// Finished, the terminal state.
103+
case finished
104+
105+
/// Helper state to avoid copy-on-write copies.
106+
case mutating
107+
}
108+
109+
/// The current state of the state machine.
110+
private(set) var state: State
111+
112+
/// Creates a new state machine.
113+
init() { self.state = .waitingForDelimiter(buffer: []) }
114+
115+
/// An action returned by the `next` method.
116+
enum NextAction {
117+
118+
/// Return nil to the caller, no more bytes.
119+
case returnNil
120+
121+
/// Emit a full line.
122+
case emitLine(ArraySlice<UInt8>)
123+
124+
/// The line is not complete yet, needs more bytes.
125+
case needsMore
126+
}
127+
128+
/// Read the next line parsed from upstream bytes.
129+
/// - Returns: An action to perform.
130+
mutating func next() -> NextAction {
131+
switch state {
132+
case .waitingForDelimiter(var buffer):
133+
state = .mutating
134+
guard let indexOfNewline = buffer.firstIndex(of: ASCII.lf) else {
135+
state = .waitingForDelimiter(buffer: buffer)
136+
return .needsMore
137+
}
138+
let line = buffer[..<indexOfNewline]
139+
buffer.removeSubrange(...indexOfNewline)
140+
state = .waitingForDelimiter(buffer: buffer)
141+
return .emitLine(line)
142+
case .finished: return .returnNil
143+
case .mutating: preconditionFailure("Invalid state")
144+
}
145+
}
146+
147+
/// An action returned by the `receivedValue` method.
148+
enum ReceivedValueAction {
149+
150+
/// Return nil to the caller, no more lines.
151+
case returnNil
152+
153+
/// Emit a full line.
154+
case emitLine(ArraySlice<UInt8>)
155+
156+
/// No action, rerun the parsing loop.
157+
case noop
158+
}
159+
160+
/// Ingest the provided bytes.
161+
/// - Parameter value: A new byte chunk. If `nil`, then the source of bytes is finished.
162+
/// - Returns: An action to perform.
163+
mutating func receivedValue(_ value: ArraySlice<UInt8>?) -> ReceivedValueAction {
164+
switch state {
165+
case .waitingForDelimiter(var buffer):
166+
if let value {
167+
state = .mutating
168+
buffer.append(contentsOf: value)
169+
state = .waitingForDelimiter(buffer: buffer)
170+
return .noop
171+
} else {
172+
let line = ArraySlice(buffer)
173+
buffer = []
174+
state = .finished
175+
if line.isEmpty { return .returnNil } else { return .emitLine(line) }
176+
}
177+
case .finished, .mutating: preconditionFailure("Invalid state")
178+
}
179+
}
180+
}
181+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftOpenAPIGenerator open source project
4+
//
5+
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
#if canImport(Darwin)
16+
import class Foundation.JSONEncoder
17+
#else
18+
@preconcurrency import class Foundation.JSONEncoder
19+
#endif
20+
21+
/// A sequence that serializes lines by concatenating them using the JSON Lines format.
22+
public struct JSONLinesSerializationSequence<Upstream: AsyncSequence & Sendable>: Sendable
23+
where Upstream.Element == ArraySlice<UInt8> {
24+
25+
/// The upstream sequence.
26+
private let upstream: Upstream
27+
28+
/// Creates a new sequence.
29+
/// - Parameter upstream: The upstream sequence of lines.
30+
public init(upstream: Upstream) { self.upstream = upstream }
31+
}
32+
33+
extension JSONLinesSerializationSequence: AsyncSequence {
34+
35+
/// The type of element produced by this asynchronous sequence.
36+
public typealias Element = ArraySlice<UInt8>
37+
38+
/// The iterator of `JSONLinesSerializationSequence`.
39+
public struct Iterator<UpstreamIterator: AsyncIteratorProtocol>: AsyncIteratorProtocol
40+
where UpstreamIterator.Element == Element {
41+
42+
/// The upstream iterator of lines.
43+
var upstream: UpstreamIterator
44+
45+
/// The state machine of the iterator.
46+
var stateMachine: StateMachine = .init()
47+
48+
/// Asynchronously advances to the next element and returns it, or ends the
49+
/// sequence if there is no next element.
50+
public mutating func next() async throws -> ArraySlice<UInt8>? {
51+
while true {
52+
switch stateMachine.next() {
53+
case .returnNil: return nil
54+
case .needsMore:
55+
let value = try await upstream.next()
56+
switch stateMachine.receivedValue(value) {
57+
case .returnNil: return nil
58+
case .emitBytes(let bytes): return bytes
59+
}
60+
}
61+
}
62+
}
63+
}
64+
65+
/// Creates the asynchronous iterator that produces elements of this
66+
/// asynchronous sequence.
67+
public func makeAsyncIterator() -> Iterator<Upstream.AsyncIterator> {
68+
Iterator(upstream: upstream.makeAsyncIterator())
69+
}
70+
}
71+
72+
extension AsyncSequence where Element: Encodable & Sendable, Self: Sendable {
73+
74+
/// Returns another sequence that encodes the events using the provided encoder into JSON Lines.
75+
/// - Parameter encoder: The JSON encoder to use.
76+
/// - Returns: A sequence that provides the serialized JSON Lines.
77+
public func asEncodedJSONLines(
78+
encoder: JSONEncoder = {
79+
let encoder = JSONEncoder()
80+
encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
81+
return encoder
82+
}()
83+
) -> JSONLinesSerializationSequence<AsyncThrowingMapSequence<Self, ArraySlice<UInt8>>> {
84+
.init(upstream: map { event in try ArraySlice(encoder.encode(event)) })
85+
}
86+
}
87+
88+
extension JSONLinesSerializationSequence.Iterator {
89+
90+
/// A state machine representing the JSON Lines serializer.
91+
struct StateMachine {
92+
93+
/// The possible states of the state machine.
94+
enum State {
95+
96+
/// Is emitting serialized JSON Lines events.
97+
case running
98+
99+
/// Finished, the terminal state.
100+
case finished
101+
}
102+
103+
/// The current state of the state machine.
104+
private(set) var state: State
105+
106+
/// Creates a new state machine.
107+
init() { self.state = .running }
108+
109+
/// An action returned by the `next` method.
110+
enum NextAction {
111+
112+
/// Return nil to the caller, no more bytes.
113+
case returnNil
114+
115+
/// Needs more bytes.
116+
case needsMore
117+
}
118+
119+
/// Read the next byte chunk serialized from upstream lines.
120+
/// - Returns: An action to perform.
121+
mutating func next() -> NextAction {
122+
switch state {
123+
case .running: return .needsMore
124+
case .finished: return .returnNil
125+
}
126+
}
127+
128+
/// An action returned by the `receivedValue` method.
129+
enum ReceivedValueAction {
130+
131+
/// Return nil to the caller, no more bytes.
132+
case returnNil
133+
134+
/// Emit the provided bytes.
135+
case emitBytes(ArraySlice<UInt8>)
136+
}
137+
138+
/// Ingest the provided line.
139+
/// - Parameter value: A new line. If `nil`, then the source of line is finished.
140+
/// - Returns: An action to perform.
141+
mutating func receivedValue(_ value: ArraySlice<UInt8>?) -> ReceivedValueAction {
142+
switch state {
143+
case .running:
144+
if let value {
145+
var buffer = value
146+
buffer.append(ASCII.lf)
147+
return .emitBytes(ArraySlice(buffer))
148+
} else {
149+
state = .finished
150+
return .returnNil
151+
}
152+
case .finished: preconditionFailure("Invalid state")
153+
}
154+
}
155+
}
156+
}

0 commit comments

Comments
 (0)