From 25b7add6137bc01bd05d2bc66256ee264db493d3 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Wed, 25 Jan 2023 17:58:56 -0800 Subject: [PATCH 1/5] wip --- .../Implementation/Codecs/Envelope.swift | 30 +------ .../Compression/GzipCompressionPool.swift | 2 +- .../Compression/IdentityCompressionPool.swift | 34 -------- .../ConnectInterceptor.swift} | 44 +++------- .../GRPCWebInterceptor.swift} | 36 ++------ .../{ => Interceptors}/InterceptorChain.swift | 0 .../Options/GzipCompressionOption.swift | 29 ------- .../Options/GzipRequestOption.swift | 40 --------- .../Options/IdentityCompressionOption.swift | 26 ------ .../Options/InterceptorsOption.swift | 31 ------- .../Options/ProtoClientOption.swift | 24 ------ .../Implementation/ProtocolClient.swift | 69 ++------------- .../Implementation/ProtocolClientConfig.swift | 85 ++++++++++++------- .../Connect/Interfaces/CompressionPool.swift | 2 +- .../NetworkProtocol.swift} | 17 ++-- .../Interfaces/ProtocolClientOption.swift | 25 ------ .../ConnectCrosstests/CrosstestClients.swift | 42 +++++---- .../ProtocolClientConfigTests.swift | 37 +------- 18 files changed, 121 insertions(+), 452 deletions(-) delete mode 100644 Libraries/Connect/Implementation/Compression/IdentityCompressionPool.swift rename Libraries/Connect/Implementation/{Options/ConnectClientOption.swift => Interceptors/ConnectInterceptor.swift} (81%) rename Libraries/Connect/Implementation/{Options/GRPCWebClientOption.swift => Interceptors/GRPCWebInterceptor.swift} (91%) rename Libraries/Connect/Implementation/{ => Interceptors}/InterceptorChain.swift (100%) delete mode 100644 Libraries/Connect/Implementation/Options/GzipCompressionOption.swift delete mode 100644 Libraries/Connect/Implementation/Options/GzipRequestOption.swift delete mode 100644 Libraries/Connect/Implementation/Options/IdentityCompressionOption.swift delete mode 100644 Libraries/Connect/Implementation/Options/InterceptorsOption.swift delete mode 100644 Libraries/Connect/Implementation/Options/ProtoClientOption.swift rename Libraries/Connect/{Implementation/Options/JSONClientOption.swift => Interfaces/NetworkProtocol.swift} (66%) delete mode 100644 Libraries/Connect/Interfaces/ProtocolClientOption.swift diff --git a/Libraries/Connect/Implementation/Codecs/Envelope.swift b/Libraries/Connect/Implementation/Codecs/Envelope.swift index a6b91f2b..5d581725 100644 --- a/Libraries/Connect/Implementation/Codecs/Envelope.swift +++ b/Libraries/Connect/Implementation/Codecs/Envelope.swift @@ -49,25 +49,6 @@ public enum Envelope { return Int(messageLength) } - /// Determines whether message data should be compressed. - /// - /// - parameter source: The message payload to optionally be compressed. - /// - parameter compressionMinBytes: The minimum size of the input message for compression to be - /// applied. - /// - /// - returns: Whether the message should be compressed. - public static func shouldCompress(_ source: Data, compressionMinBytes: Int?) -> Bool { - if source.isEmpty { - return false - } - - if let minBytes = compressionMinBytes, source.count >= minBytes { - return true - } - - return false - } - /// Packs a message into an "envelope", adding required header bytes and optionally /// applying compression. /// @@ -75,18 +56,15 @@ public enum Envelope { /// And gRPC: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests /// /// - parameter source: The input message data. - /// - parameter compressionPool: A compression pool that can be used for this envelope. - /// - parameter compressionMinBytes: The minimum size of the input message for compression to be - /// applied. + /// - parameter compression: Configuration to use for compressing the message. /// /// - returns: Serialized/enveloped data for transmission. public static func packMessage( - _ source: Data, compressionPool: CompressionPool?, compressionMinBytes: Int? + _ source: Data, using compression: ProtocolClientConfig.RequestCompression? ) -> Data { var buffer = Data() - if self.shouldCompress(source, compressionMinBytes: compressionMinBytes), - let compressionPool = compressionPool, - let compressedSource = try? compressionPool.compress(data: source) + if let compression = compression, compression.shouldCompress(source), + let compressedSource = try? compression.pool.compress(data: source) { buffer.append(0b00000001) // 1 byte with the compression bit active self.write(lengthOf: compressedSource, to: &buffer) diff --git a/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift b/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift index 46d5a0f0..a6b8376a 100644 --- a/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift +++ b/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift @@ -26,7 +26,7 @@ public struct GzipCompressionPool { } extension GzipCompressionPool: CompressionPool { - public static func name() -> String { + public func name() -> String { return "gzip" } diff --git a/Libraries/Connect/Implementation/Compression/IdentityCompressionPool.swift b/Libraries/Connect/Implementation/Compression/IdentityCompressionPool.swift deleted file mode 100644 index 429128e4..00000000 --- a/Libraries/Connect/Implementation/Compression/IdentityCompressionPool.swift +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - -/// Compression pool that keeps data in its default untransformed ("identity") state. -public struct IdentityCompressionPool { - public init() {} -} - -extension IdentityCompressionPool: CompressionPool { - public static func name() -> String { - return "identity" - } - - public func compress(data: Data) throws -> Data { - return data - } - - public func decompress(data: Data) throws -> Data { - return data - } -} diff --git a/Libraries/Connect/Implementation/Options/ConnectClientOption.swift b/Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift similarity index 81% rename from Libraries/Connect/Implementation/Options/ConnectClientOption.swift rename to Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift index 990bec1d..ae908476 100644 --- a/Libraries/Connect/Implementation/Options/ConnectClientOption.swift +++ b/Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift @@ -14,23 +14,9 @@ import Foundation -/// Enables the client to speak using the Connect protocol: -/// https://connect.build/docs -/// -/// Should not be specified alongside other options like `GRPCWebClientOption`, as only one protocol -/// should be used per `ProtocolClient`. -public struct ConnectClientOption { - public init() {} -} - -extension ConnectClientOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - return config.clone(interceptors: [ConnectInterceptor.init] + config.interceptors) - } -} - -/// The Connect protocol is implemented as an interceptor in the request/response chain. -private struct ConnectInterceptor { +/// Implementation of the Connect protocol as an interceptor. +/// The Connect protocol: https://connect.build/docs/protocol +struct ConnectInterceptor { private let config: ProtocolClientConfig private static let protocolVersion = "1" @@ -50,14 +36,12 @@ extension ConnectInterceptor: Interceptor { let requestBody = request.message ?? Data() let finalRequestBody: Data - if Envelope.shouldCompress( - requestBody, compressionMinBytes: self.config.compressionMinBytes - ), let compressionPool = self.config.requestCompressionPool() { + if let compression = self.config.requestCompression, + compression.shouldCompress(requestBody) + { do { - headers[HeaderConstants.contentEncoding] = [ - type(of: compressionPool).name(), - ] - finalRequestBody = try compressionPool.compress(data: requestBody) + headers[HeaderConstants.contentEncoding] = [compression.pool.name()] + finalRequestBody = try compression.pool.compress(data: requestBody) } catch { finalRequestBody = requestBody } @@ -87,7 +71,7 @@ extension ConnectInterceptor: Interceptor { }) if let encoding = response.headers[HeaderConstants.contentEncoding]?.first, - let compressionPool = self.config.compressionPools[encoding], + let compressionPool = self.config.responseCompressionPool(forName: encoding), let message = response.message.flatMap({ data in return try? compressionPool.decompress(data: data) }) @@ -119,7 +103,7 @@ extension ConnectInterceptor: Interceptor { var headers = request.headers headers[HeaderConstants.connectProtocolVersion] = [Self.protocolVersion] headers[HeaderConstants.connectStreamingContentEncoding] = self.config - .compressionName.map { [$0] } + .requestCompression.map { [$0.pool.name()] } headers[HeaderConstants.connectStreamingAcceptEncoding] = self.config .acceptCompressionPoolNames() return HTTPRequest( @@ -130,11 +114,7 @@ extension ConnectInterceptor: Interceptor { ) }, requestDataFunction: { data in - return Envelope.packMessage( - data, - compressionPool: self.config.requestCompressionPool(), - compressionMinBytes: self.config.compressionMinBytes - ) + return Envelope.packMessage(data, using: self.config.requestCompression) }, streamResultFunc: { result in switch result { @@ -146,7 +126,7 @@ extension ConnectInterceptor: Interceptor { do { let responseCompressionPool = responseHeaders?[ HeaderConstants.connectStreamingContentEncoding - ]?.first.flatMap { self.config.compressionPools[$0] } + ]?.first.flatMap { self.config.responseCompressionPool(forName: $0) } let (headerByte, message) = try Envelope.unpackMessage( data, compressionPool: responseCompressionPool ) diff --git a/Libraries/Connect/Implementation/Options/GRPCWebClientOption.swift b/Libraries/Connect/Implementation/Interceptors/GRPCWebInterceptor.swift similarity index 91% rename from Libraries/Connect/Implementation/Options/GRPCWebClientOption.swift rename to Libraries/Connect/Implementation/Interceptors/GRPCWebInterceptor.swift index 989745ba..7351cab4 100644 --- a/Libraries/Connect/Implementation/Options/GRPCWebClientOption.swift +++ b/Libraries/Connect/Implementation/Interceptors/GRPCWebInterceptor.swift @@ -14,23 +14,9 @@ import Foundation -/// Enables the client to speak using the gRPC Web protocol: +/// Implementation of the gRPC-Web protocol as an interceptor. /// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md -/// -/// Should not be specified alongside other options like `ConnectClientOption`, as only one protocol -/// should be used per `ProtocolClient`. -public struct GRPCWebClientOption { - public init() {} -} - -extension GRPCWebClientOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - return config.clone(interceptors: [GRPCWebInterceptor.init] + config.interceptors) - } -} - -/// The gRPC Web protocol is implemented as an interceptor in the request/response chain. -private struct GRPCWebInterceptor { +struct GRPCWebInterceptor { private let config: ProtocolClientConfig init(config: ProtocolClientConfig) { @@ -44,9 +30,7 @@ extension GRPCWebInterceptor: Interceptor { requestFunction: { request in // GRPC unary payloads are enveloped. let envelopedRequestBody = Envelope.packMessage( - request.message ?? Data(), - compressionPool: self.config.requestCompressionPool(), - compressionMinBytes: self.config.compressionMinBytes + request.message ?? Data(), using: self.config.requestCompression ) return HTTPRequest( @@ -78,7 +62,7 @@ extension GRPCWebInterceptor: Interceptor { let compressionPool = response.headers[HeaderConstants.grpcContentEncoding]? .first - .flatMap { self.config.compressionPools[$0] } + .flatMap { self.config.responseCompressionPool(forName: $0) } do { // gRPC Web returns data in 2 chunks (either/both of which may be compressed): // 1. OPTIONAL (when not trailers-only): The (headers and length prefixed) @@ -135,11 +119,7 @@ extension GRPCWebInterceptor: Interceptor { ) }, requestDataFunction: { data in - return Envelope.packMessage( - data, - compressionPool: self.config.requestCompressionPool(), - compressionMinBytes: self.config.compressionMinBytes - ) + return Envelope.packMessage(data, using: self.config.requestCompression) }, streamResultFunc: { result in switch result { @@ -160,7 +140,7 @@ extension GRPCWebInterceptor: Interceptor { do { let responseCompressionPool = responseHeaders?[ HeaderConstants.grpcContentEncoding - ]?.first.flatMap { self.config.compressionPools[$0] } + ]?.first.flatMap { self.config.responseCompressionPool(forName: $0) } let (headerByte, unpackedData) = try Envelope.unpackMessage( data, compressionPool: responseCompressionPool ) @@ -204,8 +184,8 @@ private extension Headers { var headers = self headers[HeaderConstants.grpcAcceptEncoding] = config .acceptCompressionPoolNames() - headers[HeaderConstants.grpcContentEncoding] = config.requestCompressionPool() - .map { [type(of: $0).name()] } + headers[HeaderConstants.grpcContentEncoding] = config.requestCompression + .map { [$0.pool.name()] } headers[HeaderConstants.grpcTE] = ["trailers"] // Note that we do not comply with the recommended structure for user-agent: diff --git a/Libraries/Connect/Implementation/InterceptorChain.swift b/Libraries/Connect/Implementation/Interceptors/InterceptorChain.swift similarity index 100% rename from Libraries/Connect/Implementation/InterceptorChain.swift rename to Libraries/Connect/Implementation/Interceptors/InterceptorChain.swift diff --git a/Libraries/Connect/Implementation/Options/GzipCompressionOption.swift b/Libraries/Connect/Implementation/Options/GzipCompressionOption.swift deleted file mode 100644 index 71208162..00000000 --- a/Libraries/Connect/Implementation/Options/GzipCompressionOption.swift +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Provides an implementation of gzip for encoding/decoding, allowing the client to compress -/// and decompress requests/responses using gzip. -/// -/// To compress outbound requests, specify the `GzipRequestOption`. -public struct GzipCompressionOption { - public init() {} -} - -extension GzipCompressionOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - var compressionPools = config.compressionPools - compressionPools[GzipCompressionPool.name()] = GzipCompressionPool() - return config.clone(compressionPools: compressionPools) - } -} diff --git a/Libraries/Connect/Implementation/Options/GzipRequestOption.swift b/Libraries/Connect/Implementation/Options/GzipRequestOption.swift deleted file mode 100644 index 63317c7f..00000000 --- a/Libraries/Connect/Implementation/Options/GzipRequestOption.swift +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Enables gzip compression on outbound requests using a compression pool registered for "gzip" -/// (i.e., the one provided by `GzipCompressionOption`). -/// -/// If present, `ProtocolClientInterface` implementations should respect the -/// `ProtocolClientConfig.compressionMinBytes` configuration when compressing. -public struct GzipRequestOption { - private let compressionMinBytes: Int - - /// Designated initializer. - /// - /// - parameter compressionMinBytes: If a request message payload exceeds this number of bytes, - /// the payload will be compressed. Smaller payload messages - /// will not be compressed. - public init(compressionMinBytes: Int) { - self.compressionMinBytes = compressionMinBytes - } -} - -extension GzipRequestOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - return config.clone( - compressionMinBytes: self.compressionMinBytes, - compressionName: GzipCompressionPool.name() - ) - } -} diff --git a/Libraries/Connect/Implementation/Options/IdentityCompressionOption.swift b/Libraries/Connect/Implementation/Options/IdentityCompressionOption.swift deleted file mode 100644 index ec2cb8b0..00000000 --- a/Libraries/Connect/Implementation/Options/IdentityCompressionOption.swift +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Provides a no-op default implementation for encoding/decoding. -public struct IdentityCompressionOption { - public init() {} -} - -extension IdentityCompressionOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - var compressionPools = config.compressionPools - compressionPools[IdentityCompressionPool.name()] = IdentityCompressionPool() - return config.clone(compressionPools: compressionPools) - } -} diff --git a/Libraries/Connect/Implementation/Options/InterceptorsOption.swift b/Libraries/Connect/Implementation/Options/InterceptorsOption.swift deleted file mode 100644 index 0f49f090..00000000 --- a/Libraries/Connect/Implementation/Options/InterceptorsOption.swift +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Adds interceptors to requests/responses. -/// -/// If multiple `InterceptorsOption` instances are specified, the interceptors from each will -/// be added in the order specified. -public struct InterceptorsOption { - private let interceptors: [(ProtocolClientConfig) -> Interceptor] - - public init(interceptors: [(ProtocolClientConfig) -> Interceptor]) { - self.interceptors = interceptors - } -} - -extension InterceptorsOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - return config.clone(interceptors: config.interceptors + self.interceptors) - } -} diff --git a/Libraries/Connect/Implementation/Options/ProtoClientOption.swift b/Libraries/Connect/Implementation/Options/ProtoClientOption.swift deleted file mode 100644 index 616b0524..00000000 --- a/Libraries/Connect/Implementation/Options/ProtoClientOption.swift +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Enables Protobuf binary as the serialization method for requests/responses. -public struct ProtoClientOption { - public init() {} -} - -extension ProtoClientOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - return config.clone(codec: ProtoCodec()) - } -} diff --git a/Libraries/Connect/Implementation/ProtocolClient.swift b/Libraries/Connect/Implementation/ProtocolClient.swift index ee74ce52..af140ec8 100644 --- a/Libraries/Connect/Implementation/ProtocolClient.swift +++ b/Libraries/Connect/Implementation/ProtocolClient.swift @@ -19,71 +19,18 @@ import SwiftProtobuf /// Concrete implementation of the `ProtocolClientInterface`. public final class ProtocolClient { private let config: ProtocolClientConfig + private let httpClient: HTTPClientInterface - /// Instantiate a new client. + /// Designated initializer. /// - /// - parameter host: The target host (e.g., https://buf.build). - /// - parameter httpClient: HTTP client to use for performing requests. - /// - parameter options: Series of options with which to configure the client. - /// Identity and gzip compression implementations are provided by default - /// via `IdentityCompressionOption` and `GzipCompressionOption`, and - /// encoding requests with gzip can be enabled using `GzipRequestOption`. - /// Additional compression implementations may be specified using custom - /// options. - public init( - host: String, httpClient: HTTPClientInterface, _ options: ProtocolClientOption... - ) { - var config = ProtocolClientConfig.withDefaultOptions( - andHost: host, httpClient: httpClient - ) - for option in options { - config = option.apply(config) - } - self.config = config - } - - /// Instantiate a new client. - /// - /// - parameter host: The target host (e.g., https://buf.build). - /// - parameter httpClient: HTTP client to use for performing requests. - /// - parameter options: Series of options with which to configure the client. - /// Identity and gzip compression implementations are provided by default - /// via `IdentityCompressionOption` and `GzipCompressionOption`, and - /// encoding requests with gzip can be enabled using `GzipRequestOption`. - /// Additional compression implementations may be specified using custom - /// options. - public init( - host: String, httpClient: HTTPClientInterface, options: [ProtocolClientOption] - ) { - var config = ProtocolClientConfig.withDefaultOptions( - andHost: host, httpClient: httpClient - ) - for option in options { - config = option.apply(config) - } + /// - parameter httpClient: The HTTP client to use for sending requests and starting streams. + /// - parameter config: The configuration to use for making requests. + public init(httpClient: HTTPClientInterface, config: ProtocolClientConfig) { + self.httpClient = httpClient self.config = config } } -private extension ProtocolClientConfig { - static func withDefaultOptions( - andHost host: String, httpClient: HTTPClientInterface - ) -> Self { - var config = ProtocolClientConfig( - host: host, - httpClient: httpClient, - compressionMinBytes: nil, - compressionName: nil, - compressionPools: [:], - codec: JSONCodec(), - interceptors: [] - ) - config = IdentityCompressionOption().apply(config) - config = GzipCompressionOption().apply(config) - return config - } -} - extension ProtocolClient: ProtocolClientInterface { // MARK: - Callbacks @@ -119,7 +66,7 @@ extension ProtocolClient: ProtocolClientInterface { headers: headers, message: data )) - return self.config.httpClient.unary(request: request) { response in + return self.httpClient.unary(request: request) { response in let response = chain.responseFunction(response) let responseMessage: ResponseMessage if response.code != .ok { @@ -330,7 +277,7 @@ extension ProtocolClient: ProtocolClientInterface { } } ) - let httpRequestCallbacks = self.config.httpClient.stream( + let httpRequestCallbacks = self.httpClient.stream( request: request, responseCallbacks: responseCallbacks ) diff --git a/Libraries/Connect/Implementation/ProtocolClientConfig.swift b/Libraries/Connect/Implementation/ProtocolClientConfig.swift index a093df12..951569d2 100644 --- a/Libraries/Connect/Implementation/ProtocolClientConfig.swift +++ b/Libraries/Connect/Implementation/ProtocolClientConfig.swift @@ -14,51 +14,70 @@ import Foundation -/// Set of configuration (usually modified through `ClientOption` types) used to set up clients. +/// Configuration used to set up `ProtocolClient` instances. public struct ProtocolClientConfig { - /// The target host (e.g., https://buf.build). + /// The target host (e.g., `https://buf.build`). public let host: String - /// The client to use for performing requests. - public let httpClient: HTTPClientInterface - /// The minimum number of bytes that a request message should be for compression to be used. - public let compressionMinBytes: Int? - /// The compression type that should be used (e.g., "gzip"). - /// Requires a matching `compressionPools` entry. - public let compressionName: String? - /// Compression pools that provide support for the provided `compressionName`, as well as any - /// other compression methods that need to be supported for inbound responses. - public let compressionPools: [String: CompressionPool] - /// Codec to use for serializing/deserializing requests/responses. + /// The protocol to use for requests and streams. + public let networkProtocol: NetworkProtocol + /// Codec to use for serializing requests and deserializing responses. public let codec: Codec + /// Compression settings to use for oubound requests. + public let requestCompression: RequestCompression? + /// Compression pools that can be used to decompress responses based on + /// the `content-encoding` response header. + public let responseCompressionPools: [CompressionPool] /// Set of interceptors that should be invoked with requests/responses. public let interceptors: [(ProtocolClientConfig) -> Interceptor] - public func clone( - compressionMinBytes: Int? = nil, - compressionName: String? = nil, - compressionPools: [String: CompressionPool]? = nil, - codec: Codec? = nil, - interceptors: [(ProtocolClientConfig) -> Interceptor]? = nil - ) -> Self { - return .init( - host: self.host, - httpClient: self.httpClient, - compressionMinBytes: compressionMinBytes ?? self.compressionMinBytes, - compressionName: compressionName ?? self.compressionName, - compressionPools: compressionPools ?? self.compressionPools, - codec: codec ?? self.codec, - interceptors: interceptors ?? self.interceptors - ) + /// Configuration used to specify if/how request should be compressed. + public struct RequestCompression { + /// The minimum number of bytes that a request message should be for compression to be used. + public let minBytes: Int + /// The compression pool that should be used for compressing outbound requests. + public let pool: CompressionPool + + public func shouldCompress(_ data: Data) -> Bool { + return data.count >= self.minBytes + } + + public init(minBytes: Int, pool: CompressionPool) { + self.minBytes = minBytes + self.pool = pool + } + } + + public init( + host: String, + networkProtocol: NetworkProtocol = .connect, + codec: Codec = JSONCodec(), + requestCompression: RequestCompression? = nil, + responseCompressionPools: [CompressionPool] = [GzipCompressionPool()], + interceptors: [(ProtocolClientConfig) -> Interceptor] = [] + ) { + self.host = host + self.networkProtocol = networkProtocol + self.codec = codec + self.requestCompression = requestCompression + self.responseCompressionPools = responseCompressionPools + + switch networkProtocol { + case .connect: + self.interceptors = interceptors + [ConnectInterceptor.init] + case .grpcWeb: + self.interceptors = interceptors + [GRPCWebInterceptor.init] + } } } extension ProtocolClientConfig { - func requestCompressionPool() -> CompressionPool? { - return self.compressionName.flatMap { self.compressionPools[$0] } + func acceptCompressionPoolNames() -> [String] { + return self.responseCompressionPools.map { $0.name() } } - func acceptCompressionPoolNames() -> [String] { - return self.compressionPools.keys.filter { $0 != IdentityCompressionPool.name() } + func responseCompressionPool(forName name: String) -> CompressionPool? { + return self.responseCompressionPools + .first { $0.name() == name } } func createInterceptorChain() -> InterceptorChain { diff --git a/Libraries/Connect/Interfaces/CompressionPool.swift b/Libraries/Connect/Interfaces/CompressionPool.swift index a3832db8..8e0445d5 100644 --- a/Libraries/Connect/Interfaces/CompressionPool.swift +++ b/Libraries/Connect/Interfaces/CompressionPool.swift @@ -28,7 +28,7 @@ public protocol CompressionPool { /// /// - returns: The name of the compression pool that can be used with the `content-encoding` /// header. - static func name() -> String + func name() -> String /// Compress an outbound request message. /// diff --git a/Libraries/Connect/Implementation/Options/JSONClientOption.swift b/Libraries/Connect/Interfaces/NetworkProtocol.swift similarity index 66% rename from Libraries/Connect/Implementation/Options/JSONClientOption.swift rename to Libraries/Connect/Interfaces/NetworkProtocol.swift index e66adee8..b98baba7 100644 --- a/Libraries/Connect/Implementation/Options/JSONClientOption.swift +++ b/Libraries/Connect/Interfaces/NetworkProtocol.swift @@ -12,13 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -/// Enables JSON as the serialization method for requests/responses. -public struct JSONClientOption { - public init() {} -} - -extension JSONClientOption: ProtocolClientOption { - public func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig { - return config.clone(codec: JSONCodec()) - } +/// Protocols that are supported by the library. +public enum NetworkProtocol { + /// The Connect protocol: + /// https://connect.build/docs/protocol + case connect + /// The gRPC-Web protocol: + /// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md + case grpcWeb } diff --git a/Libraries/Connect/Interfaces/ProtocolClientOption.swift b/Libraries/Connect/Interfaces/ProtocolClientOption.swift deleted file mode 100644 index b0ce4907..00000000 --- a/Libraries/Connect/Interfaces/ProtocolClientOption.swift +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/// Interface for options that can be used to configure `ProtocolClientInterface` implementations. -/// External consumers can adopt this protocol to implement custom configurations. -public protocol ProtocolClientOption { - /// Invoked by `ProtocolClientInterface` implementations allowing the option to mutate the - /// configuration for the client. - /// - /// - parameter config: The current client configuration. - /// - /// - returns: The updated client configuration, with settings from this client option applied. - func apply(_ config: ProtocolClientConfig) -> ProtocolClientConfig -} diff --git a/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift b/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift index c3702ae1..433521b2 100644 --- a/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift +++ b/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift @@ -28,36 +28,42 @@ final class CrosstestClients { let host = "https://localhost:8081" self.connectJSONClient = ProtocolClient( - host: host, httpClient: httpClient, - ConnectClientOption(), - JSONClientOption(), - GzipRequestOption(compressionMinBytes: 10), - GzipCompressionOption() + config: ProtocolClientConfig( + host: host, + networkProtocol: .connect, + codec: JSONCodec(), + requestCompression: .init(minBytes: 10, pool: GzipCompressionPool()) + ) ) self.connectProtoClient = ProtocolClient( - host: host, httpClient: httpClient, - ConnectClientOption(), - ProtoClientOption(), - GzipRequestOption(compressionMinBytes: 10), - GzipCompressionOption() + config: ProtocolClientConfig( + host: host, + networkProtocol: .connect, + codec: ProtoCodec(), + requestCompression: .init(minBytes: 10, pool: GzipCompressionPool()) + ) ) self.grpcWebJSONClient = ProtocolClient( host: host, httpClient: httpClient, - GRPCWebClientOption(), - JSONClientOption(), - GzipRequestOption(compressionMinBytes: 10), - GzipCompressionOption() + config: ProtocolClientConfig( + host: host, + networkProtocol: .grpcWeb, + codec: JSONCodec(), + requestCompression: .init(minBytes: 10, pool: GzipCompressionPool()) + ) ) self.grpcWebProtoClient = ProtocolClient( host: host, httpClient: httpClient, - GRPCWebClientOption(), - ProtoClientOption(), - GzipRequestOption(compressionMinBytes: 10), - GzipCompressionOption() + config: ProtocolClientConfig( + host: host, + networkProtocol: .grpcWeb, + codec: ProtoCodec(), + requestCompression: .init(minBytes: 10, pool: GzipCompressionPool()) + ) ) } } diff --git a/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift b/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift index 8c412479..7088c757 100644 --- a/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift +++ b/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift @@ -17,20 +17,9 @@ import Foundation import XCTest final class ProtocolClientConfigTests: XCTestCase { - func testCompressionPoolsWithIdentityAndGzip() { - var config = ProtocolClientConfig( - host: "https://buf.build", httpClient: URLSessionHTTPClient(), - compressionMinBytes: nil, compressionName: nil, compressionPools: [:], - codec: ProtoCodec(), interceptors: [] - ) - config = IdentityCompressionOption().apply(config) - config = GzipCompressionOption().apply(config) - - XCTAssertTrue(config.compressionPools["identity"] is IdentityCompressionPool) - XCTAssertTrue(config.compressionPools["gzip"] is GzipCompressionPool) - - // Identity is omitted from "accept" since it's a no-op - XCTAssertEqual(config.acceptCompressionPoolNames(), ["gzip"]) + func testDefaultResponseCompressionPoolIncludesGzip() { + let config = ProtocolClientConfig(host: "https://buf.build") + XCTAssertEqual(config.responseCompressionPools.map { $0.name() }, ["gzip"]) } func testGzipRequestOptionUsesGzipCompressionPool() { @@ -76,24 +65,4 @@ final class ProtocolClientConfigTests: XCTestCase { XCTAssertTrue(interceptors[0] is InterceptorA) XCTAssertTrue(interceptors[1] is InterceptorB) } - - func testJSONClientOptionSetsJSONCodec() { - var config = ProtocolClientConfig( - host: "https://buf.build", httpClient: URLSessionHTTPClient(), - compressionMinBytes: nil, compressionName: nil, compressionPools: [:], - codec: ProtoCodec(), interceptors: [] - ) - config = JSONClientOption().apply(config) - XCTAssertTrue(config.codec is JSONCodec) - } - - func testProtoClientOptionSetsProtoCodec() { - var config = ProtocolClientConfig( - host: "https://buf.build", httpClient: URLSessionHTTPClient(), - compressionMinBytes: nil, compressionName: nil, compressionPools: [:], - codec: JSONCodec(), interceptors: [] - ) - config = ProtoClientOption().apply(config) - XCTAssertTrue(config.codec is ProtoCodec) - } } From 6057857b395c3325f3f113a92f083ba893f12d91 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Thu, 26 Jan 2023 12:25:41 -0800 Subject: [PATCH 2/5] green tests --- .../AppSources/MenuView.swift | 22 ++++- .../AppSources/MessagingViewModel.swift | 32 ++----- .../Compression/GzipCompressionPool.swift | 2 +- .../Implementation/ProtocolClientConfig.swift | 7 +- .../ConnectCrosstests/CrosstestClients.swift | 2 - .../ConnectTests/EnvelopeTests.swift | 25 +----- .../IdentityCompressionPoolTests.swift | 27 ------ .../ConnectTests/InterceptorChainTests.swift | 6 +- .../ProtocolClientConfigTests.swift | 83 ++++++++++--------- 9 files changed, 81 insertions(+), 125 deletions(-) delete mode 100644 Tests/ConnectLibraryTests/ConnectTests/IdentityCompressionPoolTests.swift diff --git a/Examples/ElizaSharedSources/AppSources/MenuView.swift b/Examples/ElizaSharedSources/AppSources/MenuView.swift index 243484b9..37f014b7 100644 --- a/Examples/ElizaSharedSources/AppSources/MenuView.swift +++ b/Examples/ElizaSharedSources/AppSources/MenuView.swift @@ -31,6 +31,20 @@ extension MessagingConnectionType: Identifiable { } struct MenuView: View { + private func createClient(withProtocol networkProtocol: NetworkProtocol) + -> Buf_Connect_Demo_Eliza_V1_ElizaServiceClient + { + let protocolClient = ProtocolClient( + httpClient: URLSessionHTTPClient(), + config: ProtocolClientConfig( + host: "https://demo.connect.build", + networkProtocol: networkProtocol, + codec: ProtoCodec() // Use Protobuf binary, not JSON + ) + ) + return Buf_Connect_Demo_Eliza_V1_ElizaServiceClient(client: protocolClient) + } + var body: some View { NavigationView { VStack(spacing: 15) { @@ -51,7 +65,7 @@ struct MenuView: View { destination: LazyNavigationView { MessagingView( viewModel: UnaryMessagingViewModel( - protocolOption: ConnectClientOption() + client: self.createClient(withProtocol: .connect) ) ) } @@ -64,7 +78,7 @@ struct MenuView: View { destination: LazyNavigationView { MessagingView( viewModel: BidirectionalStreamingMessagingViewModel( - protocolOption: ConnectClientOption() + client: self.createClient(withProtocol: .connect) ) ) } @@ -77,7 +91,7 @@ struct MenuView: View { destination: LazyNavigationView { MessagingView( viewModel: UnaryMessagingViewModel( - protocolOption: GRPCWebClientOption() + client: self.createClient(withProtocol: .grpcWeb) ) ) } @@ -90,7 +104,7 @@ struct MenuView: View { destination: LazyNavigationView { MessagingView( viewModel: BidirectionalStreamingMessagingViewModel( - protocolOption: GRPCWebClientOption() + client: self.createClient(withProtocol: .grpcWeb) ) ) } diff --git a/Examples/ElizaSharedSources/AppSources/MessagingViewModel.swift b/Examples/ElizaSharedSources/AppSources/MessagingViewModel.swift index e7343ac1..480839f7 100644 --- a/Examples/ElizaSharedSources/AppSources/MessagingViewModel.swift +++ b/Examples/ElizaSharedSources/AppSources/MessagingViewModel.swift @@ -41,27 +41,19 @@ protocol MessagingViewModel: ObservableObject { /// View model that uses unary requests for messaging. @MainActor final class UnaryMessagingViewModel: MessagingViewModel { - private let protocolClient: ProtocolClient - private lazy var elizaClient = Buf_Connect_Demo_Eliza_V1_ElizaServiceClient( - client: self.protocolClient - ) + private let client: Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface @Published private(set) var messages = [Message]() - init(protocolOption: ProtocolClientOption) { - self.protocolClient = ProtocolClient( - host: "https://demo.connect.build", - httpClient: URLSessionHTTPClient(), - ProtoClientOption(), // Send protobuf binary on the wire - protocolOption // Specify the protocol to use for the client - ) + init(client: Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface) { + self.client = client } func send(_ sentence: String) async { let request = SayRequest.with { $0.sentence = sentence } self.addMessage(Message(message: sentence, author: .user)) - let response = await self.elizaClient.say(request: request) + let response = await self.client.say(request: request, headers: [:]) os_log(.debug, "Eliza unary response: %@", String(describing: response)) self.addMessage(Message( message: response.message?.sentence ?? "No response", author: .eliza @@ -78,21 +70,13 @@ final class UnaryMessagingViewModel: MessagingViewModel { /// View model that uses bidirectional streaming for messaging. @MainActor final class BidirectionalStreamingMessagingViewModel: MessagingViewModel { - private let protocolClient: ProtocolClient - private lazy var elizaClient = Buf_Connect_Demo_Eliza_V1_ElizaServiceClient( - client: self.protocolClient - ) - private lazy var elizaStream = self.elizaClient.converse() + private let client: Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface + private lazy var elizaStream = self.client.converse(headers: [:]) @Published private(set) var messages = [Message]() - init(protocolOption: ProtocolClientOption) { - self.protocolClient = ProtocolClient( - host: "https://demo.connect.build", - httpClient: URLSessionHTTPClient(), - ProtoClientOption(), // Send protobuf binary on the wire - protocolOption // Specify the protocol to use for the client - ) + init(client: Buf_Connect_Demo_Eliza_V1_ElizaServiceClientInterface) { + self.client = client self.observeResponses() } diff --git a/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift b/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift index a6b8376a..bf85d955 100644 --- a/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift +++ b/Libraries/Connect/Implementation/Compression/GzipCompressionPool.swift @@ -16,7 +16,7 @@ import Foundation import zlib /// Compression pool that handles gzip compression/decompression. -public struct GzipCompressionPool { +public struct GzipCompressionPool: Sendable { public init() {} public enum GzipError: Error { diff --git a/Libraries/Connect/Implementation/ProtocolClientConfig.swift b/Libraries/Connect/Implementation/ProtocolClientConfig.swift index 951569d2..c7ce92d4 100644 --- a/Libraries/Connect/Implementation/ProtocolClientConfig.swift +++ b/Libraries/Connect/Implementation/ProtocolClientConfig.swift @@ -25,12 +25,12 @@ public struct ProtocolClientConfig { /// Compression settings to use for oubound requests. public let requestCompression: RequestCompression? /// Compression pools that can be used to decompress responses based on - /// the `content-encoding` response header. + /// response headers like `content-encoding`. public let responseCompressionPools: [CompressionPool] /// Set of interceptors that should be invoked with requests/responses. public let interceptors: [(ProtocolClientConfig) -> Interceptor] - /// Configuration used to specify if/how request should be compressed. + /// Configuration used to specify if/how requests should be compressed. public struct RequestCompression { /// The minimum number of bytes that a request message should be for compression to be used. public let minBytes: Int @@ -76,8 +76,7 @@ extension ProtocolClientConfig { } func responseCompressionPool(forName name: String) -> CompressionPool? { - return self.responseCompressionPools - .first { $0.name() == name } + return self.responseCompressionPools.first { $0.name() == name } } func createInterceptorChain() -> InterceptorChain { diff --git a/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift b/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift index 433521b2..269a70c2 100644 --- a/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift +++ b/Tests/ConnectLibraryTests/ConnectCrosstests/CrosstestClients.swift @@ -46,7 +46,6 @@ final class CrosstestClients { ) ) self.grpcWebJSONClient = ProtocolClient( - host: host, httpClient: httpClient, config: ProtocolClientConfig( host: host, @@ -56,7 +55,6 @@ final class CrosstestClients { ) ) self.grpcWebProtoClient = ProtocolClient( - host: host, httpClient: httpClient, config: ProtocolClientConfig( host: host, diff --git a/Tests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift b/Tests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift index 024eb662..96efd34c 100644 --- a/Tests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift +++ b/Tests/ConnectLibraryTests/ConnectTests/EnvelopeTests.swift @@ -16,25 +16,10 @@ import Connect import XCTest final class EnvelopeTests: XCTestCase { - func testShouldCompressDataLargerThanMinBytes() { - let data = Data(repeating: 0xa, count: 50) - XCTAssertTrue(Envelope.shouldCompress(data, compressionMinBytes: 10)) - } - - func testShouldNotCompressDataSmallerThanMinBytes() { - let data = Data(repeating: 0xa, count: 50) - XCTAssertFalse(Envelope.shouldCompress(data, compressionMinBytes: 100)) - } - - func testShouldNotCompressDataIfNoMinimumSpecified() { - let data = Data(repeating: 0xa, count: 50) - XCTAssertFalse(Envelope.shouldCompress(data, compressionMinBytes: nil)) - } - func testPackingAndUnpackingCompressedMessage() throws { let originalData = Data(repeating: 0xa, count: 50) let packed = Envelope.packMessage( - originalData, compressionPool: GzipCompressionPool(), compressionMinBytes: 10 + originalData, using: .init(minBytes: 10, pool: GzipCompressionPool()) ) let compressed = try GzipCompressionPool().compress(data: originalData) XCTAssertEqual(packed[0], 1) // Compression flag = true @@ -48,9 +33,7 @@ final class EnvelopeTests: XCTestCase { func testPackingAndUnpackingUncompressedMessageBecauseCompressionMinBytesIsNil() throws { let originalData = Data(repeating: 0xa, count: 50) - let packed = Envelope.packMessage( - originalData, compressionPool: nil, compressionMinBytes: nil - ) + let packed = Envelope.packMessage(originalData, using: nil) XCTAssertEqual(packed[0], 0) // Compression flag = false XCTAssertEqual(Envelope.messageLength(forPackedData: packed), originalData.count) XCTAssertEqual(packed[5...], originalData) // Post-prefix data should match compressed value @@ -64,7 +47,7 @@ final class EnvelopeTests: XCTestCase { func testPackingAndUnpackingUncompressedMessageBecauseMessageIsTooSmall() throws { let originalData = Data(repeating: 0xa, count: 50) let packed = Envelope.packMessage( - originalData, compressionPool: GzipCompressionPool(), compressionMinBytes: 100 + originalData, using: .init(minBytes: 100, pool: GzipCompressionPool()) ) XCTAssertEqual(packed[0], 0) // Compression flag = false XCTAssertEqual(Envelope.messageLength(forPackedData: packed), originalData.count) @@ -79,7 +62,7 @@ final class EnvelopeTests: XCTestCase { func testThrowsWhenUnpackingCompressedMessageWithoutDecompressionPool() throws { let originalData = Data(repeating: 0xa, count: 50) let packed = Envelope.packMessage( - originalData, compressionPool: GzipCompressionPool(), compressionMinBytes: 10 + originalData, using: .init(minBytes: 10, pool: GzipCompressionPool()) ) let compressed = try GzipCompressionPool().compress(data: originalData) XCTAssertEqual(packed[0], 1) // Compression flag = true diff --git a/Tests/ConnectLibraryTests/ConnectTests/IdentityCompressionPoolTests.swift b/Tests/ConnectLibraryTests/ConnectTests/IdentityCompressionPoolTests.swift deleted file mode 100644 index d1ffceab..00000000 --- a/Tests/ConnectLibraryTests/ConnectTests/IdentityCompressionPoolTests.swift +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2022-2023 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Connect -import Foundation -import XCTest - -final class IdentityCompressionPoolTests: XCTestCase { - func testCompressingAndDecompressingDataDoesNotMutateData() throws { - // The identity compression pool is a no-op that keeps data as-is - let original = Data(repeating: 0xa, count: 100) - let compressionPool = IdentityCompressionPool() - XCTAssertEqual(try compressionPool.compress(data: original), original) - XCTAssertEqual(try compressionPool.decompress(data: original), original) - } -} diff --git a/Tests/ConnectLibraryTests/ConnectTests/InterceptorChainTests.swift b/Tests/ConnectLibraryTests/ConnectTests/InterceptorChainTests.swift index a699a6c5..6ea6c97a 100644 --- a/Tests/ConnectLibraryTests/ConnectTests/InterceptorChainTests.swift +++ b/Tests/ConnectLibraryTests/ConnectTests/InterceptorChainTests.swift @@ -104,11 +104,7 @@ private struct MockStreamInterceptor: Interceptor { } final class InterceptorChainTests: XCTestCase { - private let config = ProtocolClientConfig( - host: "https://buf.build", httpClient: CrosstestHTTPClient(timeout: 60), - compressionMinBytes: nil, compressionName: nil, compressionPools: [:], - codec: JSONCodec(), interceptors: [] - ) + private let config = ProtocolClientConfig(host: "https://buf.build") func testUnary() throws { let aRequestExpectation = self.expectation(description: "Filter A called with request") diff --git a/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift b/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift index 7088c757..908e635a 100644 --- a/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift +++ b/Tests/ConnectLibraryTests/ConnectTests/ProtocolClientConfigTests.swift @@ -16,53 +16,62 @@ import Foundation import XCTest -final class ProtocolClientConfigTests: XCTestCase { - func testDefaultResponseCompressionPoolIncludesGzip() { - let config = ProtocolClientConfig(host: "https://buf.build") - XCTAssertEqual(config.responseCompressionPools.map { $0.name() }, ["gzip"]) +private struct NoopInterceptor: Interceptor { + func unaryFunction() -> UnaryFunction { + return .init(requestFunction: { $0 }, responseFunction: { $0 }) } - func testGzipRequestOptionUsesGzipCompressionPool() { - var config = ProtocolClientConfig( - host: "https://buf.build", httpClient: URLSessionHTTPClient(), - compressionMinBytes: nil, compressionName: nil, compressionPools: [:], - codec: ProtoCodec(), interceptors: [] + func streamFunction() -> StreamFunction { + return .init( + requestFunction: { $0 }, + requestDataFunction: { $0 }, + streamResultFunc: { $0 } ) - config = GzipCompressionOption().apply(config) - config = GzipRequestOption(compressionMinBytes: 10).apply(config) - XCTAssertTrue(config.requestCompressionPool() is GzipCompressionPool) } - func testInterceptorsOptionAddsToExistingInterceptorsIfCalledMultipleTimes() { - class NoopInterceptor: Interceptor { - func unaryFunction() -> UnaryFunction { - return .init(requestFunction: { $0 }, responseFunction: { $0 }) - } + init(config: ProtocolClientConfig) {} +} - func streamFunction() -> StreamFunction { - return .init( - requestFunction: { $0 }, - requestDataFunction: { $0 }, - streamResultFunc: { $0 } - ) - } +final class ProtocolClientConfigTests: XCTestCase { + func testDefaultResponseCompressionPoolIncludesGzip() { + let config = ProtocolClientConfig(host: "https://buf.build") + XCTAssertTrue(config.responseCompressionPools[0] is GzipCompressionPool) + XCTAssertEqual(config.acceptCompressionPoolNames(), ["gzip"]) + } - init(config: ProtocolClientConfig) {} - } + func testShouldCompressDataLargerThanMinBytes() { + let data = Data(repeating: 0xa, count: 50) + let compression = ProtocolClientConfig.RequestCompression( + minBytes: 10, pool: GzipCompressionPool() + ) + XCTAssertTrue(compression.shouldCompress(data)) + } - final class InterceptorA: NoopInterceptor {} - final class InterceptorB: NoopInterceptor {} + func testShouldNotCompressDataSmallerThanMinBytes() { + let data = Data(repeating: 0xa, count: 50) + let compression = ProtocolClientConfig.RequestCompression( + minBytes: 100, pool: GzipCompressionPool() + ) + XCTAssertFalse(compression.shouldCompress(data)) + } - var config = ProtocolClientConfig( - host: "https://buf.build", httpClient: URLSessionHTTPClient(), - compressionMinBytes: nil, compressionName: nil, compressionPools: [:], - codec: ProtoCodec(), interceptors: [] + func testAddsConnectInterceptorLastWhenUsingConnectProtocol() { + let config = ProtocolClientConfig( + host: "https://buf.build", + networkProtocol: .connect, + interceptors: [NoopInterceptor.init] ) - config = InterceptorsOption(interceptors: [InterceptorA.init]).apply(config) - config = InterceptorsOption(interceptors: [InterceptorB.init]).apply(config) + XCTAssertTrue(config.interceptors[0](config) is NoopInterceptor) + XCTAssertTrue(config.interceptors[1](config) is ConnectInterceptor) + } - let interceptors = config.interceptors.map { $0(config) } - XCTAssertTrue(interceptors[0] is InterceptorA) - XCTAssertTrue(interceptors[1] is InterceptorB) + func testAddsGRPCWebInterceptorLastWhenUsingGRPCWebProtocol() { + let config = ProtocolClientConfig( + host: "https://buf.build", + networkProtocol: .grpcWeb, + interceptors: [NoopInterceptor.init] + ) + XCTAssertTrue(config.interceptors[0](config) is NoopInterceptor) + XCTAssertTrue(config.interceptors[1](config) is GRPCWebInterceptor) } } From 7b88a68778772ed51b3c16e4347b2d68614a054a Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Thu, 26 Jan 2023 12:33:32 -0800 Subject: [PATCH 3/5] self review --- .../Implementation/Interceptors/ConnectInterceptor.swift | 4 ++-- Libraries/Connect/Implementation/ProtocolClient.swift | 2 +- .../Connect/Implementation/ProtocolClientConfig.swift | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift b/Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift index ae908476..cc75b219 100644 --- a/Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift +++ b/Libraries/Connect/Implementation/Interceptors/ConnectInterceptor.swift @@ -15,7 +15,7 @@ import Foundation /// Implementation of the Connect protocol as an interceptor. -/// The Connect protocol: https://connect.build/docs/protocol +/// https://connect.build/docs/protocol struct ConnectInterceptor { private let config: ProtocolClientConfig @@ -40,8 +40,8 @@ extension ConnectInterceptor: Interceptor { compression.shouldCompress(requestBody) { do { - headers[HeaderConstants.contentEncoding] = [compression.pool.name()] finalRequestBody = try compression.pool.compress(data: requestBody) + headers[HeaderConstants.contentEncoding] = [compression.pool.name()] } catch { finalRequestBody = requestBody } diff --git a/Libraries/Connect/Implementation/ProtocolClient.swift b/Libraries/Connect/Implementation/ProtocolClient.swift index af140ec8..6e4becd5 100644 --- a/Libraries/Connect/Implementation/ProtocolClient.swift +++ b/Libraries/Connect/Implementation/ProtocolClient.swift @@ -24,7 +24,7 @@ public final class ProtocolClient { /// Designated initializer. /// /// - parameter httpClient: The HTTP client to use for sending requests and starting streams. - /// - parameter config: The configuration to use for making requests. + /// - parameter config: The configuration to use for requests and streams. public init(httpClient: HTTPClientInterface, config: ProtocolClientConfig) { self.httpClient = httpClient self.config = config diff --git a/Libraries/Connect/Implementation/ProtocolClientConfig.swift b/Libraries/Connect/Implementation/ProtocolClientConfig.swift index c7ce92d4..832b06ad 100644 --- a/Libraries/Connect/Implementation/ProtocolClientConfig.swift +++ b/Libraries/Connect/Implementation/ProtocolClientConfig.swift @@ -14,11 +14,11 @@ import Foundation -/// Configuration used to set up `ProtocolClient` instances. +/// Configuration used to set up `ProtocolClientInterface` implementations. public struct ProtocolClientConfig { - /// The target host (e.g., `https://buf.build`). + /// Target host (e.g., `https://buf.build`). public let host: String - /// The protocol to use for requests and streams. + /// Protocol to use for requests and streams. public let networkProtocol: NetworkProtocol /// Codec to use for serializing requests and deserializing responses. public let codec: Codec @@ -27,7 +27,7 @@ public struct ProtocolClientConfig { /// Compression pools that can be used to decompress responses based on /// response headers like `content-encoding`. public let responseCompressionPools: [CompressionPool] - /// Set of interceptors that should be invoked with requests/responses. + /// List of interceptors that should be invoked with requests/responses. public let interceptors: [(ProtocolClientConfig) -> Interceptor] /// Configuration used to specify if/how requests should be compressed. From 8f614d038ccd9aa5ad8eda1a37e940b984db5e02 Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 27 Jan 2023 05:41:02 -0800 Subject: [PATCH 4/5] default value --- Libraries/Connect/Implementation/ProtocolClient.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Libraries/Connect/Implementation/ProtocolClient.swift b/Libraries/Connect/Implementation/ProtocolClient.swift index 6e4becd5..e3b5ffd4 100644 --- a/Libraries/Connect/Implementation/ProtocolClient.swift +++ b/Libraries/Connect/Implementation/ProtocolClient.swift @@ -25,7 +25,10 @@ public final class ProtocolClient { /// /// - parameter httpClient: The HTTP client to use for sending requests and starting streams. /// - parameter config: The configuration to use for requests and streams. - public init(httpClient: HTTPClientInterface, config: ProtocolClientConfig) { + public init( + httpClient: HTTPClientInterface = URLSessionHTTPClient(), + config: ProtocolClientConfig + ) { self.httpClient = httpClient self.config = config } From 283dbaecd879045859f3b721a3618e0a4999b74d Mon Sep 17 00:00:00 2001 From: Michael Rebello Date: Fri, 27 Jan 2023 05:43:12 -0800 Subject: [PATCH 5/5] docstring --- Examples/ElizaSharedSources/AppSources/MenuView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ElizaSharedSources/AppSources/MenuView.swift b/Examples/ElizaSharedSources/AppSources/MenuView.swift index 37f014b7..9e460073 100644 --- a/Examples/ElizaSharedSources/AppSources/MenuView.swift +++ b/Examples/ElizaSharedSources/AppSources/MenuView.swift @@ -39,7 +39,7 @@ struct MenuView: View { config: ProtocolClientConfig( host: "https://demo.connect.build", networkProtocol: networkProtocol, - codec: ProtoCodec() // Use Protobuf binary, not JSON + codec: ProtoCodec() // Protobuf binary, or JSONCodec() for JSON ) ) return Buf_Connect_Demo_Eliza_V1_ElizaServiceClient(client: protocolClient)