Skip to content

Commit

Permalink
ios: update unary/streaming & add test (#354)
Browse files Browse the repository at this point in the history
Per the discussion [here](envoyproxy/envoy-mobile#312 (comment)), updating the unary and streaming interfaces.

- Moved the non-streaming convenience function into an extension on the protocol type so that it's available to all consumers
- Added a test to validate the default behavior of this extension function
- Added a `CancelableStream` protocol which includes a subset of functionality from the `StreamEmitter`, allowing consumers of the unary function to cancel requests without having the ability to send additional data into the stream

Signed-off-by: Michael Rebello <me@michaelrebello.com>
Signed-off-by: JP Simard <jp@jpsim.com>
  • Loading branch information
rebello95 authored and jpsim committed Nov 28, 2022
1 parent 63bf1e8 commit 23d3dfc
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 30 deletions.
2 changes: 1 addition & 1 deletion mobile/examples/swift/hello_world/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response, RequestError>) {
Expand Down
31 changes: 18 additions & 13 deletions mobile/library/swift/src/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
13 changes: 1 addition & 12 deletions mobile/library/swift/src/Envoy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
12 changes: 8 additions & 4 deletions mobile/library/swift/src/StreamEmitter.swift
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions mobile/library/swift/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
72 changes: 72 additions & 0 deletions mobile/library/swift/test/ClientTests.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit 23d3dfc

Please sign in to comment.