From fd8ee212b95dda30cc6bbb3419279c1a6ff8b8da Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 15 Nov 2023 12:50:20 +0100 Subject: [PATCH 1/2] [Multipart] Add the frame -> bytes serializer --- .../Multipart/ByteUtilities.swift | 5 +- .../MultipartFramesToBytesSequence.swift | 71 +++++ .../Multipart/MultipartParser.swift | 2 +- .../Multipart/MultipartSerializer.swift | 260 ++++++++++++++++++ .../Test_MultipartBytesToFramesSequence.swift | 14 +- .../Test_MultipartFramesToBytesSequence.swift | 36 +++ .../Multipart/Test_MultipartSerializer.swift | 79 ++++++ Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 59 ++++ 8 files changed, 513 insertions(+), 13 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift create mode 100644 Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift create mode 100644 Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift diff --git a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift index 05c47f1c..9ae1c6a5 100644 --- a/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift +++ b/Sources/OpenAPIRuntime/Multipart/ByteUtilities.swift @@ -36,9 +36,12 @@ enum ASCII { /// Two dash characters. static let dashes: [UInt8] = [dash, dash] - /// The `` character follow by the `` character. + /// The `` character followed by the `` character. static let crlf: [UInt8] = [cr, lf] + /// The colon character followed by the space character. + static let colonSpace: [UInt8] = [colon, space] + /// The characters that represent optional whitespace (OWS). static let optionalWhitespace: Set = [space, tab] diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift new file mode 100644 index 00000000..e1d55542 --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartFramesToBytesSequence.swift @@ -0,0 +1,71 @@ +//===----------------------------------------------------------------------===// +// +// 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 HTTPTypes + +/// A sequence that serializes multipart frames into bytes. +struct MultipartFramesToBytesSequence: Sendable +where Upstream.Element == MultipartFrame { + + /// The source of multipart frames. + var upstream: Upstream + + /// The boundary string used to separate multipart parts. + var boundary: String +} + +extension MultipartFramesToBytesSequence: AsyncSequence { + + /// The type of element produced by this asynchronous sequence. + typealias Element = ArraySlice + + /// Creates the asynchronous iterator that produces elements of this + /// asynchronous sequence. + /// + /// - Returns: An instance of the `AsyncIterator` type used to produce + /// elements of the asynchronous sequence. + func makeAsyncIterator() -> Iterator { + Iterator(upstream: upstream.makeAsyncIterator(), boundary: boundary) + } + + /// An iterator that pulls frames from the upstream iterator and provides + /// serialized byte chunks. + struct Iterator: AsyncIteratorProtocol + where UpstreamIterator.Element == MultipartFrame { + + /// The iterator that provides the multipart frames. + private var upstream: UpstreamIterator + + /// The multipart frame serializer. + private var serializer: MultipartSerializer + + /// Creates a new iterator from the provided source of frames and a boundary string. + /// - Parameters: + /// - upstream: The iterator that provides the multipart frames. + /// - boundary: The boundary separating the multipart parts. + init(upstream: UpstreamIterator, boundary: String) { + self.upstream = upstream + self.serializer = .init(boundary: boundary) + } + + /// Asynchronously advances to the next element and returns it, or ends the + /// sequence if there is no next element. + /// + /// - Returns: The next element, if it exists, or `nil` to signal the end of + /// the sequence. + mutating func next() async throws -> ArraySlice? { + try await serializer.next { try await upstream.next() } + } + } +} diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift index 87267a6c..d98db13e 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartParser.swift @@ -15,7 +15,7 @@ import Foundation import HTTPTypes -/// A parser of mutlipart frames from bytes. +/// A parser of multipart frames from bytes. struct MultipartParser { /// The underlying state machine. diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift new file mode 100644 index 00000000..b990d1ed --- /dev/null +++ b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift @@ -0,0 +1,260 @@ +//===----------------------------------------------------------------------===// +// +// 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 +import HTTPTypes + +/// A serializer of multipart frames into bytes. +struct MultipartSerializer { + + /// The boundary that separates parts. + private let boundary: ArraySlice + + /// The underlying state machine. + private var stateMachine: StateMachine + + /// The buffer of bytes ready to be written out. + private var outBuffer: [UInt8] + + /// Creates a new serializer. + /// - Parameter boundary: The boundary that separates parts. + init(boundary: String) { + self.boundary = ArraySlice(boundary.utf8) + 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. + /// - Throws: When a serialization error is encountered. + mutating func next(_ fetchFrame: () async throws -> MultipartFrame?) async throws -> ArraySlice? { + + func flushedBytes() -> ArraySlice { + let outChunk = ArraySlice(outBuffer) + outBuffer.removeAll(keepingCapacity: true) + return outChunk + } + + while true { + switch stateMachine.next() { + case .returnNil: return nil + case .emitStart: + emitStart() + return flushedBytes() + case .needsMore: + let frame = try await fetchFrame() + switch stateMachine.receivedFrame(frame) { + case .returnNil: return nil + case .emitEvents(let events): + for event in events { + switch event { + case .headerFields(let headerFields): emitHeaders(headerFields) + case .bodyChunk(let chunk): emitBodyChunk(chunk) + case .endOfPart: emitEndOfPart() + case .start: emitStart() + case .end: emitEnd() + } + } + return flushedBytes() + case .emitError(let error): throw SerializerError(error: error) + } + } + } + } +} + +extension MultipartSerializer { + + /// An error thrown by the serializer. + struct SerializerError: Swift.Error, CustomStringConvertible, LocalizedError { + + /// The underlying error emitted by the state machine. + var error: StateMachine.ActionError + + var description: String { + switch error { + case .noHeaderFieldsAtStart: return "No header fields found at the start of the multipart body." + } + } + + var errorDescription: String? { description } + } +} + +extension MultipartSerializer { + + /// Writes the provided header fields into the buffer. + /// - Parameter headerFields: The header fields to serialize. + private mutating func emitHeaders(_ headerFields: HTTPFields) { + outBuffer.append(contentsOf: ASCII.crlf) + let sortedHeaders = headerFields.sorted { a, b in a.name.canonicalName < b.name.canonicalName } + for headerField in sortedHeaders { + outBuffer.append(contentsOf: headerField.name.canonicalName.utf8) + outBuffer.append(contentsOf: ASCII.colonSpace) + outBuffer.append(contentsOf: headerField.value.utf8) + outBuffer.append(contentsOf: ASCII.crlf) + } + outBuffer.append(contentsOf: ASCII.crlf) + } + + /// Writes the part body chunk into the buffer. + /// - Parameter bodyChunk: The body chunk to write. + private mutating func emitBodyChunk(_ bodyChunk: ArraySlice) { outBuffer.append(contentsOf: bodyChunk) } + + /// Writes an end of part boundary into the buffer. + private mutating func emitEndOfPart() { + outBuffer.append(contentsOf: ASCII.crlf) + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: boundary) + } + + /// Writes the start boundary into the buffer. + private mutating func emitStart() { + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: boundary) + } + + /// Writes the end double dash to the buffer. + private mutating func emitEnd() { + outBuffer.append(contentsOf: ASCII.dashes) + outBuffer.append(contentsOf: ASCII.crlf) + outBuffer.append(contentsOf: ASCII.crlf) + } +} + +extension MultipartSerializer { + + /// A state machine representing the multipart frame serializer. + struct StateMachine { + + /// The possible states of the state machine. + enum State: Hashable { + + /// Has not yet written any bytes. + case initial + + /// Emitted start, but no frames yet. + case startedNothingEmittedYet + + /// Finished, the terminal state. + case finished + + /// Last emitted a header fields frame. + case emittedHeaders + + /// Last emitted a part body chunk frame. + case emittedBodyChunk + } + + /// The current state of the state machine. + private(set) var state: State + + /// Creates a new state machine. + init() { self.state = .initial } + + /// An error returned by the state machine. + enum ActionError: Hashable { + + /// The first frame from upstream was not a header fields frame. + case noHeaderFieldsAtStart + } + + /// An action returned by the `next` method. + enum NextAction: Hashable { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Emit the initial boundary. + case emitStart + + /// Ready for the next frame. + case needsMore + } + + /// Read the next byte chunk serialized from upstream frames. + /// - Returns: An action to perform. + mutating func next() -> NextAction { + switch state { + case .initial: + state = .startedNothingEmittedYet + return .emitStart + case .finished: return .returnNil + case .startedNothingEmittedYet, .emittedHeaders, .emittedBodyChunk: return .needsMore + } + } + + /// An event to serialize to bytes. + enum Event: Hashable { + + /// The header fields of a part. + case headerFields(HTTPFields) + + /// A byte chunk of a part. + case bodyChunk(ArraySlice) + + /// A boundary between parts. + case endOfPart + + /// The initial boundary. + case start + + /// The final dashes. + case end + } + + /// An action returned by the `receivedFrame` method. + enum ReceivedFrameAction: Hashable { + + /// Return nil to the caller, no more bytes. + case returnNil + + /// Write the provided events as bytes. + case emitEvents([Event]) + + /// Throw the provided error. + case emitError(ActionError) + } + + /// Ingest the provided frame. + /// - Parameter frame: A new frame. If `nil`, then the source of frames is finished. + /// - Returns: An action to perform. + mutating func receivedFrame(_ frame: MultipartFrame?) -> ReceivedFrameAction { + switch state { + case .initial: preconditionFailure("Invalid state: \(state)") + case .finished: return .returnNil + case .startedNothingEmittedYet, .emittedHeaders, .emittedBodyChunk: break + } + switch (state, frame) { + case (.initial, _), (.finished, _): preconditionFailure("Already handled above.") + case (_, .none): + state = .finished + return .emitEvents([.endOfPart, .end]) + case (.startedNothingEmittedYet, .headerFields(let headerFields)): + state = .emittedHeaders + return .emitEvents([.headerFields(headerFields)]) + case (.startedNothingEmittedYet, .bodyChunk): + state = .finished + return .emitError(.noHeaderFieldsAtStart) + case (.emittedHeaders, .headerFields(let headerFields)), + (.emittedBodyChunk, .headerFields(let headerFields)): + state = .emittedHeaders + return .emitEvents([.endOfPart, .headerFields(headerFields)]) + case (.emittedHeaders, .bodyChunk(let bodyChunk)), (.emittedBodyChunk, .bodyChunk(let bodyChunk)): + state = .emittedBodyChunk + return .emitEvents([.bodyChunk(bodyChunk)]) + } + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift index 7229e45b..88036301 100644 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartBytesToFramesSequence.swift @@ -17,20 +17,12 @@ import Foundation final class Test_MultipartBytesToFramesSequence: Test_Runtime { func test() async throws { - var chunk = chunkFromStringLines([ + let chunk = chunkFromStringLines([ "--__abcd__", #"Content-Disposition: form-data; name="name""#, "", "24", "--__abcd__", #"Content-Disposition: form-data; name="info""#, "", "{}", "--__abcd__--", ]) - let next: () async throws -> ArraySlice? = { - if let first = chunk.first { - let out: ArraySlice = [first] - chunk = chunk.dropFirst() - return out - } else { - return nil - } - } - let upstream = HTTPBody(AsyncThrowingStream(unfolding: next), length: .unknown, iterationBehavior: .single) + var iterator = chunk.makeIterator() + let upstream = AsyncStream { iterator.next().map { ArraySlice([$0]) } } let sequence = MultipartBytesToFramesSequence(upstream: upstream, boundary: "__abcd__") var frames: [MultipartFrame] = [] for try await frame in sequence { frames.append(frame) } diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift new file mode 100644 index 00000000..257c9614 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartFramesToBytesSequence.swift @@ -0,0 +1,36 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartFramesToBytesSequence: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var iterator = frames.makeIterator() + let upstream = AsyncStream { iterator.next() } + let sequence = MultipartFramesToBytesSequence(upstream: upstream, boundary: "__abcd__") + var bytes: ArraySlice = [] + for try await chunk in sequence { bytes.append(contentsOf: chunk) } + let expectedBytes = chunkFromStringLines([ + "--__abcd__", #"content-disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"content-disposition: form-data; name="info""#, "", "{}", "--__abcd__--", "", + ]) + XCTAssertEqualData(bytes, expectedBytes) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift new file mode 100644 index 00000000..ac1689e9 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift @@ -0,0 +1,79 @@ +//===----------------------------------------------------------------------===// +// +// 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_MultipartSerializer: Test_Runtime { + func test() async throws { + let frames: [MultipartFrame] = [ + .headerFields([.contentDisposition: #"form-data; name="name""#]), .bodyChunk(chunkFromString("2")), + .bodyChunk(chunkFromString("4")), .headerFields([.contentDisposition: #"form-data; name="info""#]), + .bodyChunk(chunkFromString("{")), .bodyChunk(chunkFromString("}")), + ] + var serializer = MultipartSerializer(boundary: "__abcd__") + var iterator = frames.makeIterator() + var bytes: [UInt8] = [] + while let chunk = try await serializer.next({ iterator.next() }) { bytes.append(contentsOf: chunk) } + let expectedBytes = chunkFromStringLines([ + "--__abcd__", #"content-disposition: form-data; name="name""#, "", "24", "--__abcd__", + #"content-disposition: form-data; name="info""#, "", "{}", "--__abcd__--", "", + ]) + XCTAssertEqualData(bytes, expectedBytes) + } +} + +private func newStateMachine() -> MultipartSerializer.StateMachine { .init() } + +final class Test_MultipartSerializerStateMachine: Test_Runtime { + + func testInvalidFirstFrame() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.next(), .emitStart) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual(stateMachine.receivedFrame(.bodyChunk([])), .emitError(.noHeaderFieldsAtStart)) + } + + func testTwoParts() throws { + var stateMachine = newStateMachine() + XCTAssertEqual(stateMachine.state, .initial) + XCTAssertEqual(stateMachine.next(), .emitStart) + XCTAssertEqual(stateMachine.state, .startedNothingEmittedYet) + XCTAssertEqual( + stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="name""#])), + .emitEvents([.headerFields([.contentDisposition: #"form-data; name="name""#])]) + ) + XCTAssertEqual(stateMachine.state, .emittedHeaders) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.bodyChunk(chunkFromString("24"))), + .emitEvents([.bodyChunk(chunkFromString("24"))]) + ) + XCTAssertEqual(stateMachine.state, .emittedBodyChunk) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="info""#])), + .emitEvents([.endOfPart, .headerFields([.contentDisposition: #"form-data; name="info""#])]) + ) + XCTAssertEqual(stateMachine.state, .emittedHeaders) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual( + stateMachine.receivedFrame(.bodyChunk(chunkFromString("{}"))), + .emitEvents([.bodyChunk(chunkFromString("{}"))]) + ) + XCTAssertEqual(stateMachine.state, .emittedBodyChunk) + XCTAssertEqual(stateMachine.next(), .needsMore) + XCTAssertEqual(stateMachine.receivedFrame(nil), .emitEvents([.endOfPart, .end])) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index e2fe87c0..2e6d386e 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -280,3 +280,62 @@ public func XCTAssertEqualStringifiedData( if let body = try expression1() { data = try await Data(collecting: body, upTo: .max) } else { data = .init() } XCTAssertEqualStringifiedData(data, try expression2(), message(), file: file, line: line) } + +fileprivate extension UInt8 { + var asHex: String { + let original: String + switch self { + case 0x0d: original = "CR" + case 0x0a: original = "LF" + default: original = "\(UnicodeScalar(self)) " + } + return String(format: "%02x \(original)", self) + } +} +/// Asserts that the data matches the expected value. +public func XCTAssertEqualData( + _ expression1: @autoclosure () throws -> C1?, + _ expression2: @autoclosure () throws -> C2, + _ message: @autoclosure () -> String = "Data doesn't match.", + file: StaticString = #filePath, + line: UInt = #line +) where C1.Element == UInt8, C2.Element == UInt8 { + do { + guard let actualBytes = try expression1() else { + XCTFail("First value is nil", file: file, line: line) + return + } + let expectedBytes = try expression2() + if ArraySlice(actualBytes) == ArraySlice(expectedBytes) { return } + let actualCount = actualBytes.count + let expectedCount = expectedBytes.count + let minCount = min(actualCount, expectedCount) + print("Printing both byte sequences, first is the actual value and second is the expected one.") + for (index, byte) in zip(actualBytes.prefix(minCount), expectedBytes.prefix(minCount)).enumerated() { + print("\(String(format: "%04d", index)): \(byte.0 != byte.1 ? "x" : " ") \(byte.0.asHex) | \(byte.1.asHex)") + } + let direction: String + let extraBytes: ArraySlice + if actualCount > expectedCount { + direction = "Actual bytes has extra bytes" + extraBytes = ArraySlice(actualBytes.dropFirst(minCount)) + } else if expectedCount > actualCount { + direction = "Actual bytes is missing expected bytes" + extraBytes = ArraySlice(expectedBytes.dropFirst(minCount)) + } else { + direction = "" + extraBytes = [] + } + if !extraBytes.isEmpty { + print("\(direction):") + for (index, byte) in extraBytes.enumerated() { + print("\(String(format: "%04d", minCount + index)): \(byte.asHex)") + } + } + XCTFail( + "Actual stringified data '\(String(decoding: actualBytes, as: UTF8.self))' doesn't equal to expected stringified data '\(String(decoding: expectedBytes, as: UTF8.self))'. Details: \(message())", + file: file, + line: line + ) + } catch { XCTFail(error.localizedDescription, file: file, line: line) } +} From 786d00e0dbf1ab040fd59e7e9286bb85b5d193cc Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 16 Nov 2023 14:55:23 +0100 Subject: [PATCH 2/2] PR feedback --- .../Multipart/MultipartSerializer.swift | 12 ++++++------ .../Multipart/Test_MultipartSerializer.swift | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift index b990d1ed..8f744784 100644 --- a/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift +++ b/Sources/OpenAPIRuntime/Multipart/MultipartSerializer.swift @@ -145,7 +145,7 @@ extension MultipartSerializer { case initial /// Emitted start, but no frames yet. - case startedNothingEmittedYet + case emittedStart /// Finished, the terminal state. case finished @@ -188,10 +188,10 @@ extension MultipartSerializer { mutating func next() -> NextAction { switch state { case .initial: - state = .startedNothingEmittedYet + state = .emittedStart return .emitStart case .finished: return .returnNil - case .startedNothingEmittedYet, .emittedHeaders, .emittedBodyChunk: return .needsMore + case .emittedStart, .emittedHeaders, .emittedBodyChunk: return .needsMore } } @@ -234,17 +234,17 @@ extension MultipartSerializer { switch state { case .initial: preconditionFailure("Invalid state: \(state)") case .finished: return .returnNil - case .startedNothingEmittedYet, .emittedHeaders, .emittedBodyChunk: break + case .emittedStart, .emittedHeaders, .emittedBodyChunk: break } switch (state, frame) { case (.initial, _), (.finished, _): preconditionFailure("Already handled above.") case (_, .none): state = .finished return .emitEvents([.endOfPart, .end]) - case (.startedNothingEmittedYet, .headerFields(let headerFields)): + case (.emittedStart, .headerFields(let headerFields)): state = .emittedHeaders return .emitEvents([.headerFields(headerFields)]) - case (.startedNothingEmittedYet, .bodyChunk): + case (.emittedStart, .bodyChunk): state = .finished return .emitError(.noHeaderFieldsAtStart) case (.emittedHeaders, .headerFields(let headerFields)), diff --git a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift index ac1689e9..7dd96a64 100644 --- a/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift +++ b/Tests/OpenAPIRuntimeTests/Multipart/Test_MultipartSerializer.swift @@ -49,7 +49,7 @@ final class Test_MultipartSerializerStateMachine: Test_Runtime { var stateMachine = newStateMachine() XCTAssertEqual(stateMachine.state, .initial) XCTAssertEqual(stateMachine.next(), .emitStart) - XCTAssertEqual(stateMachine.state, .startedNothingEmittedYet) + XCTAssertEqual(stateMachine.state, .emittedStart) XCTAssertEqual( stateMachine.receivedFrame(.headerFields([.contentDisposition: #"form-data; name="name""#])), .emitEvents([.headerFields([.contentDisposition: #"form-data; name="name""#])])