Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ios: finish bridging between ObjC/Swift #312

Merged
merged 33 commits into from
Aug 20, 2019
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
441baea
bazel: update to Xcode 10.3
rebello95 Jul 26, 2019
c69387a
WIP
rebello95 Jul 27, 2019
78cdb91
wip so i can switch branches
rebello95 Aug 8, 2019
69bd771
implement EnvoyBuilder
rebello95 Aug 16, 2019
423a736
lazily initialize engine
rebello95 Aug 16, 2019
11f7d48
tests
rebello95 Aug 16, 2019
7f01381
cleanup
rebello95 Aug 16, 2019
3b8162c
Merge remote-tracking branch 'origin/envoybuilder' into objc-bridging
rebello95 Aug 16, 2019
33b627e
fixup
rebello95 Aug 16, 2019
f24c31b
updates
rebello95 Aug 17, 2019
fcf1de8
Merge remote-tracking branch 'origin/master' into objc-bridging
rebello95 Aug 17, 2019
7dcf9a5
fixup
rebello95 Aug 17, 2019
67bce92
build
rebello95 Aug 17, 2019
3782a3e
fixup
rebello95 Aug 17, 2019
95a32b3
Merge remote-tracking branch 'origin/master' into objc-bridging
rebello95 Aug 19, 2019
4192cb2
WIP demo testing E2E
rebello95 Aug 19, 2019
abe0e25
call setup
rebello95 Aug 19, 2019
fee0aa1
Update ViewController.swift
rebello95 Aug 19, 2019
793b81b
more
rebello95 Aug 19, 2019
64067c2
tweaks
rebello95 Aug 19, 2019
2d0d22c
tests
rebello95 Aug 19, 2019
2065e15
send body
rebello95 Aug 19, 2019
078b257
remove trailers
rebello95 Aug 19, 2019
39ab2c7
fixup
rebello95 Aug 19, 2019
df6f6e2
send correct headers
rebello95 Aug 19, 2019
43de33e
update for objc
rebello95 Aug 19, 2019
af8c50a
add unary
rebello95 Aug 19, 2019
d8fbb0f
tests
rebello95 Aug 19, 2019
9a494d9
Merge remote-tracking branch 'origin/master' into objc-bridging
rebello95 Aug 20, 2019
075659e
cleanup
rebello95 Aug 20, 2019
fd9ac92
drop demo changes for this diff
rebello95 Aug 20, 2019
7a2013b
cleanup
rebello95 Aug 20, 2019
abc7a71
suggestion
rebello95 Aug 20, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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?,
Copy link
Contributor

@goaway goaway Aug 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of startStream and sendUnary, I'd suggest:
send(request:handler:) and send(request:body:handler:)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or sendRequest(_:handler:) / sendRequest(_:body:handler:)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather lean towards being clear on the intent (stream/unary) for the end consumer's sake, rather than making them assume based on function parameter signatures

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And of course trailers can be in there too, but since trailers are so rare in normal cases, I'd probably still suggest a signature which excludes or defaults them.

Copy link
Contributor

@goaway goaway Aug 20, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the argument about clarity/specificity, but I doubt most end-users are likely to be thinking in these terms, and I have misgivings because I don't really think this actually is more accurate.

The startStream interface is a perfectly appropriate interface for sending a "unary" request, and the current naming sort of makes it sound like you should only use it when you're trying to stream something (a naive user might even think that this is to stream a response, which is completely unrelated).

In fact, all HTTP requests are "unary" in the sense that they have a single request and response. I feel like these are semi-GRPC concepts that are leaking in because GRPC layers a stream encoding into a "unary" HTTP request.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess in general, since this is a top level interface, I'm pushing back a little because I feel the naming should be recognizable and understandable to users who don't have a deep familiarity with the underlying concerns.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Talked offline - this makes sense. Going to update in a separate PR after merging this

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
}

/// 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 an internal Envoy exception occurs with the stream.
rebello95 marked this conversation as resolved.
Show resolved Hide resolved
/// 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