diff --git a/library/swift/src/BUILD b/library/swift/src/BUILD index f8076db604..e6b0da93c2 100644 --- a/library/swift/src/BUILD +++ b/library/swift/src/BUILD @@ -10,10 +10,14 @@ swift_static_framework( name = "ios_framework", srcs = [ "Envoy.swift", + "EnvoyResult.swift", "LogLevel.swift", + "NetworkError.swift", "Request.swift", "RequestBuilder.swift", "RequestMethod.swift", + "Response.swift", + "ResponseBuilder.swift", "RetryPolicy.swift", ], module_name = "Envoy", diff --git a/library/swift/src/EnvoyResult.swift b/library/swift/src/EnvoyResult.swift new file mode 100644 index 0000000000..4731867d2e --- /dev/null +++ b/library/swift/src/EnvoyResult.swift @@ -0,0 +1,18 @@ +import Foundation + +/// A result returned from Envoy. +@objcMembers +public final class Result: NSObject { + /// The response returned from the server. + public let response: Response? + /// An error that was encountered on the network (i.e., going offline). + public let error: NetworkError? + + /// Designated initializer. + public init(response: Response?, + error: NetworkError?) + { + self.response = response + self.error = error + } +} diff --git a/library/swift/src/NetworkError.swift b/library/swift/src/NetworkError.swift new file mode 100644 index 0000000000..21bd9a188c --- /dev/null +++ b/library/swift/src/NetworkError.swift @@ -0,0 +1,14 @@ +import Foundation + +/// Error representing cases when no response was received from the server. +/// I.e., the client went offline or became disconnected. +@objcMembers +public final class NetworkError: NSObject, Swift.Error { + /// Message describing this error. + public let message: String? + + /// Designated initializer. + public init(message: String?) { + self.message = message + } +} diff --git a/library/swift/src/Request.swift b/library/swift/src/Request.swift index 3270bc1f2f..b3c2661273 100644 --- a/library/swift/src/Request.swift +++ b/library/swift/src/Request.swift @@ -13,9 +13,9 @@ public final class Request: NSObject { /// Trailers to send with the request. /// Multiple values for a given name are valid, and will be sent as comma-separated values. public let trailers: [String: [String]] - // Serialized data to send as the body of the request. + /// Serialized data to send as the body of the request. public let body: Data? - // Retry policy to use for this request. + /// Retry policy to use for this request. public let retryPolicy: RetryPolicy? /// Converts the request back to a builder so that it can be modified (i.e., by a filter). diff --git a/library/swift/src/RequestBuilder.swift b/library/swift/src/RequestBuilder.swift index 31133e7869..fabe62db6f 100644 --- a/library/swift/src/RequestBuilder.swift +++ b/library/swift/src/RequestBuilder.swift @@ -13,9 +13,9 @@ public final class RequestBuilder: NSObject { /// Trailers to send with the request. /// Multiple values for a given name are valid, and will be sent as comma-separated values. public private(set) var trailers: [String: [String]] = [:] - // Serialized data to send as the body of the request. + /// Serialized data to send as the body of the request. public private(set) var body: Data? - // Retry policy to use for this request. + /// Retry policy to use for this request. public private(set) var retryPolicy: RetryPolicy? // MARK: - Initializers diff --git a/library/swift/src/Response.swift b/library/swift/src/Response.swift new file mode 100644 index 0000000000..d3a3661411 --- /dev/null +++ b/library/swift/src/Response.swift @@ -0,0 +1,35 @@ +import Foundation + +/// Represents an Envoy HTTP response. Use `ResponseBuilder` to construct new instances. +@objcMembers +public final class Response: NSObject { + /// Status code returned with the response. + public let statusCode: Int + /// Headers returned with the response. + /// Multiple values for a given name are valid, and will be sent as comma-separated values. + public let headers: [String: [String]] + /// Trailers returned with the response. + /// Multiple values for a given name are valid, and will be sent as comma-separated values. + public let trailers: [String: [String]] + /// Serialized data returned as the body of the response. + public let body: Data? + + /// Converts the response back to a builder so that it can be modified (i.e., by a filter). + /// + /// - returns: A new builder including all the properties of this response. + public func newBuilder() -> ResponseBuilder { + return ResponseBuilder(response: self) + } + + /// Internal initializer called from the builder to create a new response. + init(statusCode: Int, + headers: [String: [String]] = [:], + trailers: [String: [String]] = [:], + body: Data?) + { + self.statusCode = statusCode + self.headers = headers + self.trailers = trailers + self.body = body + } +} diff --git a/library/swift/src/ResponseBuilder.swift b/library/swift/src/ResponseBuilder.swift new file mode 100644 index 0000000000..0eefc9dff2 --- /dev/null +++ b/library/swift/src/ResponseBuilder.swift @@ -0,0 +1,112 @@ +import Foundation + +/// Builder used for constructing instances of `Response` types. +@objcMembers +public final class ResponseBuilder: NSObject { + /// Status code returned with the response. + public private(set) var statusCode: Int = 200 + /// Headers returned with the response. + /// Multiple values for a given name are valid, and will be sent as comma-separated values. + public private(set) var headers: [String: [String]] = [:] + /// Trailers returned with the response. + /// Multiple values for a given name are valid, and will be sent as comma-separated values. + public private(set) var trailers: [String: [String]] = [:] + /// Serialized data returned as the body of the response. + public private(set) var body: Data? + + // MARK: - Initializers + + /// Internal initializer used for converting a response back to a builder. + convenience init(response: Response) { + self.init() + self.statusCode = response.statusCode + self.headers = response.headers + self.trailers = response.trailers + self.body = response.body + } + + // MARK: - Builder functions + + @discardableResult + public func addStatusCode(_ statusCode: Int) -> ResponseBuilder { + self.statusCode = statusCode + return self + } + + @discardableResult + public func addHeader(name: String, value: String) -> ResponseBuilder { + self.headers[name, default: []].append(value) + return self + } + + @discardableResult + public func removeHeaders(name: String) -> ResponseBuilder { + self.headers.removeValue(forKey: name) + return self + } + + @discardableResult + public func removeHeader(name: String, value: String) -> ResponseBuilder { + self.headers[name]?.removeAll(where: { $0 == value }) + if self.headers[name]?.isEmpty == true { + self.headers.removeValue(forKey: name) + } + + return self + } + + @discardableResult + public func addTrailer(name: String, value: String) -> ResponseBuilder { + self.trailers[name, default: []].append(value) + return self + } + + @discardableResult + public func removeTrailer(name: String) -> ResponseBuilder { + self.trailers.removeValue(forKey: name) + return self + } + + @discardableResult + public func removeTrailers(named name: String, value: String) -> ResponseBuilder { + self.trailers[name]?.removeAll(where: { $0 == value }) + if self.trailers[name]?.isEmpty == true { + self.trailers.removeValue(forKey: name) + } + + return self + } + + @discardableResult + public func addBody(_ body: Data?) -> ResponseBuilder { + self.body = body + return self + } + + public func build() -> Response { + return Response(statusCode: self.statusCode, + headers: self.headers, + trailers: self.trailers, + body: self.body) + } +} + +// MARK: - Objective-C helpers + +extension Response { + /// Convenience builder function to allow for cleaner Objective-C syntax. + /// + /// For example: + /// + /// Response *res = [Response withBuild:^(ResponseBuilder *builder) { + /// [builder addBody:bodyData]; + /// [builder addHeaderWithName:@"x-some-header" value:@"foo"]; + /// [builder addTrailerWithName:@"x-some-trailer" value:@"foo"]; + /// }]; + @objc + public static func with(build: (ResponseBuilder) -> Void) -> Response { + let builder = ResponseBuilder() + build(builder) + return builder.build() + } +}