diff --git a/mobile/examples/swift/hello_world/ViewController.swift b/mobile/examples/swift/hello_world/ViewController.swift index 0de67483c9c5..18c4f9e4d4aa 100644 --- a/mobile/examples/swift/hello_world/ViewController.swift +++ b/mobile/examples/swift/hello_world/ViewController.swift @@ -67,7 +67,7 @@ final class ViewController: UITableViewController { message: "failed within Envoy library"))) } - envoy.sendUnary(request, handler: handler) + envoy.send(request, data: nil, handler: handler) } private func add(result: Result) { diff --git a/mobile/library/swift/src/Client.swift b/mobile/library/swift/src/Client.swift index 3ef269099704..3cacb4f784e3 100644 --- a/mobile/library/swift/src/Client.swift +++ b/mobile/library/swift/src/Client.swift @@ -9,23 +9,28 @@ public protocol Client { /// - parameter handler: Handler for receiving stream events. /// /// - returns: Emitter for sending streaming data outward. - func startStream(with request: Request, handler: ResponseHandler) -> StreamEmitter - - /// Convenience function for sending a unary request. - /// - /// - parameter request: The request to send. - /// - parameter body: Serialized data to send as the body of the request. - /// - parameter trailers: Trailers to send with the request. - func sendUnary(_ request: Request, body: Data?, - trailers: [String: [String]], handler: ResponseHandler) + func send(_ request: Request, handler: ResponseHandler) -> StreamEmitter } extension Client { - /// Convenience function for sending a unary request without trailers. + /// Convenience function for sending a complete (non-streamed) request. /// /// - parameter request: The request to send. - /// - parameter body: Serialized data to send as the body of the request. - public func sendUnary(_ request: Request, body: Data?, handler: ResponseHandler) { - self.sendUnary(request, body: body, trailers: [:], handler: handler) + /// - parameter data: Serialized data to send as the body of the request. + /// - parameter trailers: Trailers to send with the request. + /// + /// - returns: A cancelable request. + @discardableResult + public func send(_ request: Request, data: Data?, + trailers: [String: [String]] = [:], handler: ResponseHandler) + -> CancelableStream + { + let emitter = self.send(request, handler: handler) + if let data = data { + emitter.sendData(data) + } + + emitter.close(trailers: trailers) + return emitter } } diff --git a/mobile/library/swift/src/Envoy.swift b/mobile/library/swift/src/Envoy.swift index 2bbe461190b5..aa1775c16fbb 100644 --- a/mobile/library/swift/src/Envoy.swift +++ b/mobile/library/swift/src/Envoy.swift @@ -47,20 +47,9 @@ public final class Envoy: NSObject { } extension Envoy: Client { - public func startStream(with request: Request, handler: ResponseHandler) -> StreamEmitter { + public func send(_ request: Request, handler: ResponseHandler) -> StreamEmitter { let httpStream = self.engine.startStream(with: handler.underlyingObserver) httpStream.sendHeaders(request.outboundHeaders(), close: false) return EnvoyStreamEmitter(stream: httpStream) } - - public func sendUnary(_ request: Request, body: Data?, - trailers: [String: [String]], handler: ResponseHandler) - { - let emitter = self.startStream(with: request, handler: handler) - if let body = body { - emitter.sendData(body) - } - - emitter.close(trailers: trailers) - } } diff --git a/mobile/library/swift/src/StreamEmitter.swift b/mobile/library/swift/src/StreamEmitter.swift index 65e7da99412b..028d4dab34f3 100644 --- a/mobile/library/swift/src/StreamEmitter.swift +++ b/mobile/library/swift/src/StreamEmitter.swift @@ -1,8 +1,15 @@ import Foundation +/// Interface for a stream that may be canceled. +@objc +public protocol CancelableStream { + /// Cancel and end the associated stream. + func cancel() +} + /// Interface allowing for sending/emitting data on an Envoy stream. @objc -public protocol StreamEmitter { +public protocol StreamEmitter: CancelableStream { /// Send data over the associated stream. /// /// - parameter data: Data to send over the stream. @@ -23,9 +30,6 @@ public protocol StreamEmitter { /// /// - parameter trailers: Trailers to send over the stream. func close(trailers: [String: [String]]) - - /// Cancel and end the associated stream. - func cancel() } extension StreamEmitter { diff --git a/mobile/library/swift/test/BUILD b/mobile/library/swift/test/BUILD index 88cb24fafa84..b3134d7332ee 100644 --- a/mobile/library/swift/test/BUILD +++ b/mobile/library/swift/test/BUILD @@ -2,6 +2,13 @@ licenses(["notice"]) # Apache 2 load("//bazel:swift_test.bzl", "envoy_mobile_swift_test") +envoy_mobile_swift_test( + name = "client_tests", + srcs = [ + "ClientTests.swift", + ], +) + envoy_mobile_swift_test( name = "envoy_builder_tests", srcs = [ diff --git a/mobile/library/swift/test/ClientTests.swift b/mobile/library/swift/test/ClientTests.swift new file mode 100644 index 000000000000..a503d1877037 --- /dev/null +++ b/mobile/library/swift/test/ClientTests.swift @@ -0,0 +1,72 @@ +import Envoy +import Foundation +import XCTest + +private final class MockStreamEmitter: StreamEmitter { + var onData: ((Data?) -> Void)? + var onTrailers: (([String: [String]]) -> Void)? + + func sendData(_ data: Data) -> StreamEmitter { + self.onData?(data) + return self + } + + func sendMetadata(_ metadata: [String: [String]]) -> StreamEmitter { + return self + } + + func close(trailers: [String: [String]]) { + self.onTrailers?(trailers) + } + + func cancel() {} +} + +private final class MockClient: Client { + var onRequest: ((Request) -> Void)? + var onData: ((Data?) -> Void)? + var onTrailers: (([String: [String]]) -> Void)? + + func send(_ request: Request, handler: ResponseHandler) -> StreamEmitter { + self.onRequest?(request) + let emitter = MockStreamEmitter() + emitter.onData = self.onData + emitter.onTrailers = self.onTrailers + return emitter + } +} + +final class ClientTests: XCTestCase { + func testNonStreamingExtensionSendsRequestDetailsThroughStream() { + let requestExpectation = self.expectation(description: "Sends request") + let dataExpectation = self.expectation(description: "Sends data") + let closeExpectation = self.expectation(description: "Calls close") + + let expectedRequest = RequestBuilder( + method: .get, scheme: "https", authority: "www.envoyproxy.io", path: "/docs") + .build() + let expectedData = Data([1, 2, 3]) + let expectedTrailers = ["foo": ["bar", "baz"]] + + let mockClient = MockClient() + mockClient.onRequest = { request in + XCTAssertEqual(expectedRequest, request) + requestExpectation.fulfill() + } + + mockClient.onData = { data in + XCTAssertEqual(expectedData, data) + dataExpectation.fulfill() + } + + mockClient.onTrailers = { trailers in + XCTAssertEqual(expectedTrailers, trailers) + closeExpectation.fulfill() + } + + mockClient.send(expectedRequest, data: expectedData, trailers: expectedTrailers, + handler: ResponseHandler()) + self.wait(for: [requestExpectation, dataExpectation, closeExpectation], + timeout: 0.1, enforceOrder: true) + } +}