Skip to content

Commit

Permalink
ios: finish bridging between ObjC/Swift (#312)
Browse files Browse the repository at this point in the history
This PR ties together the upstream Objective-C/++ implementation with the Swift interfaces/bridging implementation. After this change, end-to-end calls from the native Swift library into the Envoy core should be supported.

- Implements the streaming and unary request methods
- Adds a simple `EnvoyStreamEmitter` which sends requests from Swift down into Objective-C
- Updates the header conversion functions and their tests to convert to the proper types (`[String: [String]]` versus `[String: String]`)
- Implements `ResponseHandler` as a class which provides a pass-through for setting Objective-C blocks directly from Swift for handling responses
- Adds a parser for status codes, as well as tests

#349 will use this interface with the Swift demo apps.

Signed-off-by: Michael Rebello <me@michaelrebello.com>
  • Loading branch information
rebello95 authored Aug 20, 2019
1 parent afc6b7a commit cb7c0e1
Show file tree
Hide file tree
Showing 12 changed files with 199 additions and 77 deletions.
3 changes: 2 additions & 1 deletion library/swift/src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ swift_static_framework(
"Client.swift",
"Envoy.swift",
"EnvoyBuilder.swift",
"Error.swift",
"EnvoyError.swift",
"EnvoyStreamEmitter.swift",
"LogLevel.swift",
"Request.swift",
"RequestBuilder.swift",
Expand Down
22 changes: 22 additions & 0 deletions library/swift/src/Envoy.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import Foundation

/// Envoy's implementation of `Client`, buildable using `EnvoyBuilder`.
@objcMembers
public final class Envoy: NSObject {
private let engine: EnvoyEngine
private let runner: RunnerThread

/// Indicates whether this Envoy instance is currently active and running.
Expand All @@ -20,6 +22,7 @@ public final class Envoy: NSObject {
/// - parameter logLevel: Log level to use for this instance.
/// - parameter engine: The underlying engine to use for starting Envoy.
init(configYAML: String, logLevel: LogLevel = .info, engine: EnvoyEngine) {
self.engine = engine
self.runner = RunnerThread(configYAML: configYAML, logLevel: logLevel, engine: engine)
self.runner.start()
}
Expand All @@ -42,3 +45,22 @@ public final class Envoy: NSObject {
}
}
}

extension Envoy: Client {
public func startStream(with 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)
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import Foundation

@objcMembers
public final class Error: NSObject, Swift.Error {
public final class EnvoyError: NSObject, Error {
/// Error code associated with the exception that occurred.
public let errorCode: Int
/// A description of what exception that occurred.
public let message: String
/// Optional cause for the error.
public let cause: Swift.Error?
public let cause: Error?

init(errorCode: Int, message: String, cause: Swift.Error?) {
self.errorCode = errorCode
Expand Down
31 changes: 31 additions & 0 deletions library/swift/src/EnvoyStreamEmitter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

/// Default implementation of the `StreamEmitter` interface.
@objcMembers
final class EnvoyStreamEmitter {
private let stream: EnvoyHTTPStream

init(stream: EnvoyHTTPStream) {
self.stream = stream
}
}

extension EnvoyStreamEmitter: StreamEmitter {
func sendData(_ data: Data) -> StreamEmitter {
self.stream.send(data, close: false)
return self
}

func sendMetadata(_ metadata: [String: [String]]) -> StreamEmitter {
self.stream.sendMetadata(metadata)
return self
}

func close(trailers: [String: [String]]) {
self.stream.sendTrailers(trailers)
}

func cancel() {
_ = self.stream.cancel()
}
}
11 changes: 5 additions & 6 deletions library/swift/src/RequestMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ extension Request {
/// information on the URL, method, and additional headers.
///
/// - returns: Outbound headers to send with an HTTP request.
func outboundHeaders() -> [String: String] {
func outboundHeaders() -> [String: [String]] {
var headers = self.headers
.filter { !$0.key.hasPrefix(kRestrictedHeaderPrefix) }
.mapValues { $0.joined(separator: ",") }
.reduce(into: [
":method": self.method.stringValue,
":scheme": self.scheme,
":authority": self.authority,
":path": self.path,
":method": [self.method.stringValue],
":scheme": [self.scheme],
":authority": [self.authority],
":path": [self.path],
]) { $0[$1.key] = $1.value }

if let retryPolicy = self.retryPolicy {
Expand Down
108 changes: 82 additions & 26 deletions library/swift/src/ResponseHandler.swift
Original file line number Diff line number Diff line change
@@ -1,42 +1,98 @@
import Foundation

/// Callback interface for receiving stream events.
@objc
public protocol ResponseHandler {
/// Dispatch queue upon which callbacks will be called.
var dispatchQueue: DispatchQueue { get }
@objcMembers
public final class ResponseHandler: NSObject {
/// Underlying observer which will be passed to the Envoy Engine.
let underlyingObserver = EnvoyObserver()

/// Called when response headers are received by the stream.
/// Initialize a new instance of the handler.
///
/// - parameter headers: The headers of the response.
/// - parameter statusCode: The status code of the response.
func onHeaders(_ headers: [String: [String]], statusCode: Int)
/// - parameter queue: Dispatch queue upon which callbacks will be called.
public init(queue: DispatchQueue = .main) {
self.underlyingObserver.dispatchQueue = queue
}

/// Called when a data frame is received by the stream.
/// Specify a callback for when response headers are received by the stream.
/// If `endStream` is `true`, the stream is complete.
///
/// - parameter data: Bytes in the response.
/// - parameter endStream: True if the stream is complete.
func onData(_ data: Data, endStream: Bool)
/// - parameter closure: Closure which will receive the headers, status code,
/// and flag indicating if the stream is headers-only.
@discardableResult
public func onHeaders(_ closure:
@escaping (_ headers: [String: [String]], _ statusCode: Int, _ endStream: Bool) -> Void)
-> ResponseHandler
{
self.underlyingObserver.onHeaders = { headers, endStream in
closure(headers, ResponseHandler.statusCode(fromHeaders: headers), endStream)
}

/// Called when response metadata is received by the stream.
return self
}

/// Specify a callback for when a data frame is received by the stream.
/// If `endStream` is `true`, the stream is complete.
///
/// - parameter closure: Closure which will receive the data,
/// and flag indicating if the stream is complete.
@discardableResult
public func onData(_ closure:
@escaping (_ data: Data, _ endStream: Bool) -> Void)
-> ResponseHandler
{
self.underlyingObserver.onData = closure
return self
}

/// Specify a callback for when trailers are received by the stream.
/// If the closure is called, the stream is complete.
///
/// - parameter metadata: The metadata of the response.
/// - parameter endStream: True if the stream is complete.
func onMetadata(_ metadata: [String: [String]], endStream: Bool)
/// - parameter closure: Closure which will receive the trailers.
@discardableResult
public func onTrailers(_ closure:
@escaping (_ trailers: [String: [String]]) -> Void)
-> ResponseHandler
{
self.underlyingObserver.onTrailers = closure
return self
}

/// Called when response trailers are received by the stream.
/// Specify a callback for when an internal Envoy exception occurs with the stream.
/// If the closure is called, the stream is complete.
///
/// - parameter trailers: The trailers of the response.
func onTrailers(_ trailers: [String: [String]])
/// - parameter closure: Closure which will be called when an error occurs.
@discardableResult
public func onError(_ closure:
@escaping () -> Void)
-> ResponseHandler
{
self.underlyingObserver.onError = closure
return self
}

/// Called when an internal Envoy exception occurs with the stream.
/// Specify a callback for when the stream is canceled.
/// If the closure is called, the stream is complete.
///
/// - parameter error: The error that occurred with the stream.
func onError(_ error: Error)
/// - parameter closure: Closure which will be called when the stream is canceled.
@discardableResult
public func onCancel(_ closure:
@escaping () -> Void)
-> ResponseHandler
{
self.underlyingObserver.onCancel = closure
return self
}

/// Called when the stream is canceled.
func onCanceled()
// MARK: - Helpers

/// Called when the stream has been completed.
func onCompletion()
/// Parses out the status code from the provided HTTP headers.
///
/// - parameter headers: The headers from which to obtain the status.
///
/// - returns: The HTTP status code from the headers, or 0 if none is set.
static func statusCode(fromHeaders headers: [String: [String]]) -> Int {
return headers[":status"]?
.compactMap(Int.init)
.first ?? 0
}
}
9 changes: 4 additions & 5 deletions library/swift/src/RetryPolicyMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@ extension RetryPolicy {
/// Converts the retry policy to a set of headers recognized by Envoy.
///
/// - returns: The header representation of the retry policy.
func outboundHeaders() -> [String: String] {
func outboundHeaders() -> [String: [String]] {
var headers = [
"x-envoy-max-retries": "\(self.maxRetryCount)",
"x-envoy-max-retries": ["\(self.maxRetryCount)"],
"x-envoy-retry-on": self.retryOn
.lazy
.map { $0.stringValue }
.joined(separator: ","),
.map { $0.stringValue },
]

if let perRetryTimeoutMS = self.perRetryTimeoutMS {
headers["x-envoy-upstream-rq-per-try-timeout-ms"] = "\(perRetryTimeoutMS)"
headers["x-envoy-upstream-rq-per-try-timeout-ms"] = ["\(perRetryTimeoutMS)"]
}

return headers
Expand Down
28 changes: 6 additions & 22 deletions library/swift/src/StreamEmitter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,34 @@ import Foundation
/// Interface allowing for sending/emitting data on an Envoy stream.
@objc
public protocol StreamEmitter {
/// Returns whether the emitter is still active and can
/// perform communications with the associated stream.
///
/// - returns: True if the stream is active.
func isActive() -> Bool

/// Send data over the associated stream.
///
/// - parameter data: Data to send over the stream.
///
/// - throws: `Envoy.Error` when the stream is inactive or data can't be sent.
///
/// - returns: The stream emitter, for chaining syntax.
@discardableResult
func sendData(_ data: Data) throws -> StreamEmitter
func sendData(_ data: Data) -> StreamEmitter

/// Send metadata over the associated stream.
///
/// - parameter metadata: Metadata to send over the stream.
///
/// - throws: `Envoy.Error` when the stream is inactive or data can't be sent.
///
/// - returns: The stream emitter, for chaining syntax.
@discardableResult
func sendMetadata(_ metadata: [String: [String]]) throws -> StreamEmitter
func sendMetadata(_ metadata: [String: [String]]) -> StreamEmitter

/// End the stream after sending any provided trailers.
///
/// - parameter trailers: Trailers to send over the stream.
///
/// - throws: `Envoy.Error` when the stream is inactive or data can't be sent.
func close(trailers: [String: [String]]) throws
func close(trailers: [String: [String]])

/// Cancel and end the associated stream.
///
/// - throws: `Envoy.Error` when the stream is inactive or data can't be sent.
func cancel() throws
func cancel()
}

extension StreamEmitter {
/// Convenience function for ending the stream without sending any trailers.
///
/// - throws: `Envoy.Error` when the stream is inactive or data can't be sent.
public func close() throws {
try self.close(trailers: [:])
public func close() {
self.close(trailers: [:])
}
}
7 changes: 7 additions & 0 deletions library/swift/test/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ envoy_mobile_swift_test(
],
)

envoy_mobile_swift_test(
name = "response_handler_tests",
srcs = [
"ResponseHandlerTests.swift",
],
)

envoy_mobile_swift_test(
name = "retry_policy_tests",
srcs = [
Expand Down
Loading

0 comments on commit cb7c0e1

Please sign in to comment.