Skip to content

Commit 287022f

Browse files
author
mustii
committed
Adds GRPCPayload protocol to allow other types of payloads to be used with GRPC-swift
1 parent 151b627 commit 287022f

30 files changed

+159
-52
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ project.xcworkspace
33
xcuserdata
44
DerivedData/
55
.build
6+
.swiftpm
67
build
78
/protoc-gen-swift
89
/protoc-gen-grpc-swift

Sources/Examples/Echo/Model/echo.grpc.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,6 @@ extension Echo_EchoProvider {
149149
}
150150
}
151151

152+
153+
extension Echo_EchoRequest: GRPCProtobufPayload {}
154+
extension Echo_EchoResponse: GRPCProtobufPayload {}

Sources/Examples/HelloWorld/Model/helloworld.grpc.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,6 @@ extension Helloworld_GreeterProvider {
8484
}
8585
}
8686

87+
88+
extension Helloworld_HelloRequest: GRPCProtobufPayload {}
89+
extension Helloworld_HelloReply: GRPCProtobufPayload {}

Sources/Examples/RouteGuide/Model/route_guide.grpc.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,9 @@ extension Routeguide_RouteGuideProvider {
149149
}
150150
}
151151

152+
153+
extension Routeguide_Point: GRPCProtobufPayload {}
154+
extension Routeguide_Feature: GRPCProtobufPayload {}
155+
extension Routeguide_Rectangle: GRPCProtobufPayload {}
156+
extension Routeguide_RouteSummary: GRPCProtobufPayload {}
157+
extension Routeguide_RouteNote: GRPCProtobufPayload {}

Sources/GRPC/CallHandlers/BidirectionalStreamingCallHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import Logging
2626
/// they can fail the observer block future.
2727
/// - To close the call and send the status, complete `context.statusPromise`.
2828
public class BidirectionalStreamingCallHandler<
29-
RequestMessage: Message,
30-
ResponseMessage: Message
29+
RequestMessage: GRPCPayload,
30+
ResponseMessage: GRPCPayload
3131
>: _BaseCallHandler<RequestMessage, ResponseMessage> {
3232
public typealias Context = StreamingResponseCallContext<ResponseMessage>
3333
public typealias EventObserver = (StreamEvent<RequestMessage>) -> Void

Sources/GRPC/CallHandlers/ClientStreamingCallHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ enum ClientStreamingHandlerObserverState<Factory, Observer> {
3434
/// they can fail the observer block future.
3535
/// - To close the call and send the response, complete `context.responsePromise`.
3636
public final class ClientStreamingCallHandler<
37-
RequestMessage: Message,
38-
ResponseMessage: Message
37+
RequestMessage: GRPCPayload,
38+
ResponseMessage: GRPCPayload
3939
>: _BaseCallHandler<RequestMessage, ResponseMessage> {
4040
public typealias Context = UnaryResponseCallContext<ResponseMessage>
4141
public typealias EventObserver = (StreamEvent<RequestMessage>) -> Void

Sources/GRPC/CallHandlers/ServerStreamingCallHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import Logging
2424
/// - The observer block is implemented by the framework user and calls `context.sendResponse` as needed.
2525
/// - To close the call and send the status, complete the status future returned by the observer block.
2626
public final class ServerStreamingCallHandler<
27-
RequestMessage: Message,
28-
ResponseMessage: Message
27+
RequestMessage: GRPCPayload,
28+
ResponseMessage: GRPCPayload
2929
>: _BaseCallHandler<RequestMessage, ResponseMessage> {
3030
public typealias EventObserver = (RequestMessage) -> EventLoopFuture<GRPCStatus>
3131

Sources/GRPC/CallHandlers/UnaryCallHandler.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import Logging
2525
/// - To return a response to the client, the framework user should complete that future
2626
/// (similar to e.g. serving regular HTTP requests in frameworks such as Vapor).
2727
public final class UnaryCallHandler<
28-
RequestMessage: Message,
29-
ResponseMessage: Message
28+
RequestMessage: GRPCPayload,
29+
ResponseMessage: GRPCPayload
3030
>: _BaseCallHandler<RequestMessage, ResponseMessage> {
3131
public typealias EventObserver = (RequestMessage) -> EventLoopFuture<ResponseMessage>
3232
private var eventObserver: EventObserver?

Sources/GRPC/CallHandlers/_BaseCallHandler.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import Logging
2323
///
2424
/// Calls through to `processMessage` for individual messages it receives, which needs to be implemented by subclasses.
2525
/// - Important: This is **NOT** part of the public API.
26-
public class _BaseCallHandler<RequestMessage: Message, ResponseMessage: Message>: GRPCCallHandler {
26+
public class _BaseCallHandler<RequestMessage: GRPCPayload, ResponseMessage: GRPCPayload>: GRPCCallHandler {
2727
public func makeGRPCServerCodec() -> ChannelHandler {
2828
return GRPCServerCodec<RequestMessage, ResponseMessage>()
2929
}

Sources/GRPC/ClientCalls/BaseClientCall.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import Logging
4848
///
4949
/// This class also provides much of the framework user facing functionality via conformance to
5050
/// `ClientCall`.
51-
public class BaseClientCall<Request: Message, Response: Message>: ClientCall {
51+
public class BaseClientCall<Request: GRPCPayload, Response: GRPCPayload>: ClientCall {
5252
public typealias RequestMessage = Request
5353
public typealias ResponseMessage = Response
5454

Sources/GRPC/ClientCalls/BidirectionalStreamingCall.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Logging
2727
/// - `initialMetadata`: the initial metadata returned from the server,
2828
/// - `status`: the status of the gRPC call after it has ended,
2929
/// - `trailingMetadata`: any metadata returned from the server alongside the `status`.
30-
public final class BidirectionalStreamingCall<RequestMessage: Message, ResponseMessage: Message>
30+
public final class BidirectionalStreamingCall<RequestMessage: GRPCPayload, ResponseMessage: GRPCPayload>
3131
: BaseClientCall<RequestMessage, ResponseMessage>,
3232
StreamingRequestClientCall {
3333
private var messageQueue: EventLoopFuture<Void>

Sources/GRPC/ClientCalls/ClientCall.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ import SwiftProtobuf
2323
/// Base protocol for a client call to a gRPC service.
2424
public protocol ClientCall {
2525
/// The type of the request message for the call.
26-
associatedtype RequestMessage: Message
26+
associatedtype RequestMessage: GRPCPayload
2727
/// The type of the response message for the call.
28-
associatedtype ResponseMessage: Message
28+
associatedtype ResponseMessage: GRPCPayload
2929

3030
/// HTTP/2 stream that requests and responses are sent and received on.
3131
var subchannel: EventLoopFuture<Channel> { get }

Sources/GRPC/ClientCalls/ClientStreamingCall.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Logging
2828
/// - `response`: the response from the call,
2929
/// - `status`: the status of the gRPC call after it has ended,
3030
/// - `trailingMetadata`: any metadata returned from the server alongside the `status`.
31-
public final class ClientStreamingCall<RequestMessage: Message, ResponseMessage: Message>
31+
public final class ClientStreamingCall<RequestMessage: GRPCPayload, ResponseMessage: GRPCPayload>
3232
: BaseClientCall<RequestMessage, ResponseMessage>,
3333
StreamingRequestClientCall,
3434
UnaryResponseClientCall {

Sources/GRPC/ClientCalls/ServerStreamingCall.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Logging
2424
/// - `initialMetadata`: the initial metadata returned from the server,
2525
/// - `status`: the status of the gRPC call after it has ended,
2626
/// - `trailingMetadata`: any metadata returned from the server alongside the `status`.
27-
public final class ServerStreamingCall<RequestMessage: Message, ResponseMessage: Message>: BaseClientCall<RequestMessage, ResponseMessage> {
27+
public final class ServerStreamingCall<RequestMessage: GRPCPayload, ResponseMessage: GRPCPayload>: BaseClientCall<RequestMessage, ResponseMessage> {
2828
public init(
2929
connection: ClientConnection,
3030
path: String,

Sources/GRPC/ClientCalls/UnaryCall.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import Logging
2727
/// - `response`: the response from the unary call,
2828
/// - `status`: the status of the gRPC call after it has ended,
2929
/// - `trailingMetadata`: any metadata returned from the server alongside the `status`.
30-
public final class UnaryCall<RequestMessage: Message, ResponseMessage: Message>
30+
public final class UnaryCall<RequestMessage: GRPCPayload, ResponseMessage: GRPCPayload>
3131
: BaseClientCall<RequestMessage, ResponseMessage>,
3232
UnaryResponseClientCall {
3333
public let response: EventLoopFuture<ResponseMessage>

Sources/GRPC/GRPCClient.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public protocol GRPCClient {
2626
}
2727

2828
extension GRPCClient {
29-
public func makeUnaryCall<Request: Message, Response: Message>(
29+
public func makeUnaryCall<Request: GRPCPayload, Response: GRPCPayload>(
3030
path: String,
3131
request: Request,
3232
callOptions: CallOptions? = nil,
@@ -40,7 +40,7 @@ extension GRPCClient {
4040
errorDelegate: self.connection.configuration.errorDelegate)
4141
}
4242

43-
public func makeServerStreamingCall<Request: Message, Response: Message>(
43+
public func makeServerStreamingCall<Request: GRPCPayload, Response: GRPCPayload>(
4444
path: String,
4545
request: Request,
4646
callOptions: CallOptions? = nil,
@@ -56,7 +56,7 @@ extension GRPCClient {
5656
handler: handler)
5757
}
5858

59-
public func makeClientStreamingCall<Request: Message, Response: Message>(
59+
public func makeClientStreamingCall<Request: GRPCPayload, Response: GRPCPayload>(
6060
path: String,
6161
callOptions: CallOptions? = nil,
6262
requestType: Request.Type = Request.self,
@@ -69,7 +69,7 @@ extension GRPCClient {
6969
errorDelegate: self.connection.configuration.errorDelegate)
7070
}
7171

72-
public func makeBidirectionalStreamingCall<Request: Message, Response: Message>(
72+
public func makeBidirectionalStreamingCall<Request: GRPCPayload, Response: GRPCPayload>(
7373
path: String,
7474
callOptions: CallOptions? = nil,
7575
requestType: Request.Type = Request.self,

Sources/GRPC/GRPCClientResponseChannelHandler.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Logging
2424
/// This includes holding promises for the initial metadata and status of the gRPC call. This handler
2525
/// is also responsible for error handling, via an error delegate and by appropriately failing the
2626
/// aforementioned promises.
27-
internal class GRPCClientResponseChannelHandler<ResponseMessage: Message>: ChannelInboundHandler {
27+
internal class GRPCClientResponseChannelHandler<ResponseMessage: GRPCPayload>: ChannelInboundHandler {
2828
public typealias InboundIn = _GRPCClientResponsePart<ResponseMessage>
2929
internal let logger: Logger
3030
internal var stopwatch: Stopwatch?
@@ -186,7 +186,7 @@ internal class GRPCClientResponseChannelHandler<ResponseMessage: Message>: Chann
186186
}
187187

188188
/// A channel handler for client calls which receive a single response.
189-
final class GRPCClientUnaryResponseChannelHandler<ResponseMessage: Message>: GRPCClientResponseChannelHandler<ResponseMessage> {
189+
final class GRPCClientUnaryResponseChannelHandler<ResponseMessage: GRPCPayload>: GRPCClientResponseChannelHandler<ResponseMessage> {
190190
let responsePromise: EventLoopPromise<ResponseMessage>
191191

192192
internal init(
@@ -236,7 +236,7 @@ final class GRPCClientUnaryResponseChannelHandler<ResponseMessage: Message>: GRP
236236
}
237237

238238
/// A channel handler for client calls which receive a stream of responses.
239-
final class GRPCClientStreamingResponseChannelHandler<ResponseMessage: Message>: GRPCClientResponseChannelHandler<ResponseMessage> {
239+
final class GRPCClientStreamingResponseChannelHandler<ResponseMessage: GRPCPayload>: GRPCClientResponseChannelHandler<ResponseMessage> {
240240
typealias ResponseHandler = (ResponseMessage) -> Void
241241

242242
let responseHandler: ResponseHandler

Sources/GRPC/GRPCClientStateMachine.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ enum SendEndOfRequestStreamError: Error {
6666
/// A state machine for a single gRPC call from the perspective of a client.
6767
///
6868
/// See: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
69-
struct GRPCClientStateMachine<Request: Message, Response: Message> {
69+
struct GRPCClientStateMachine<Request: GRPCPayload, Response: GRPCPayload> {
7070
/// The combined state of the request (client) and response (server) streams for an RPC call.
7171
///
7272
/// The following states are not possible:

Sources/GRPC/GRPCPayload.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2020, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import NIO
17+
18+
public protocol GRPCPayload {
19+
init(serializedByteBuffer: inout NIO.ByteBuffer) throws
20+
func serialize(into buffer: inout NIO.ByteBuffer) throws
21+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2020, gRPC Authors All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
import NIO
17+
import SwiftProtobuf
18+
19+
public protocol GRPCProtobufPayload: GRPCPayload, Message {}
20+
21+
public extension GRPCProtobufPayload {
22+
init(serializedByteBuffer: inout NIO.ByteBuffer) throws {
23+
try self.init(serializedData: serializedByteBuffer.readData(length: serializedByteBuffer.readableBytes)!,
24+
extensions: nil,
25+
partial: false,
26+
options: BinaryDecodingOptions())
27+
}
28+
func serialize(into buffer: inout NIO.ByteBuffer) throws {
29+
let data = try self.serializedData()
30+
buffer.writeBytes(data)
31+
}
32+
}

Sources/GRPC/GRPCServerCodec.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import NIOHTTP1
2222
/// Incoming gRPC package with a fixed message type.
2323
///
2424
/// - Important: This is **NOT** part of the public API.
25-
public enum _GRPCServerRequestPart<RequestMessage: Message> {
25+
public enum _GRPCServerRequestPart<RequestMessage: GRPCPayload> {
2626
case head(HTTPRequestHead)
2727
case message(RequestMessage)
2828
case end
@@ -31,14 +31,14 @@ public enum _GRPCServerRequestPart<RequestMessage: Message> {
3131
/// Outgoing gRPC package with a fixed message type.
3232
///
3333
/// - Important: This is **NOT** part of the public API.
34-
public enum _GRPCServerResponsePart<ResponseMessage: Message> {
34+
public enum _GRPCServerResponsePart<ResponseMessage: GRPCPayload> {
3535
case headers(HTTPHeaders)
3636
case message(ResponseMessage)
3737
case statusAndTrailers(GRPCStatus, HTTPHeaders)
3838
}
3939

4040
/// A simple channel handler that translates raw gRPC packets into decoded protobuf messages, and vice versa.
41-
internal final class GRPCServerCodec<RequestMessage: Message, ResponseMessage: Message> {}
41+
internal final class GRPCServerCodec<RequestMessage: GRPCPayload, ResponseMessage: GRPCPayload> {}
4242

4343
extension GRPCServerCodec: ChannelInboundHandler {
4444
typealias InboundIn = _RawGRPCServerRequestPart
@@ -50,9 +50,8 @@ extension GRPCServerCodec: ChannelInboundHandler {
5050
context.fireChannelRead(self.wrapInboundOut(.head(requestHead)))
5151

5252
case .message(var message):
53-
let messageAsData = message.readData(length: message.readableBytes)!
5453
do {
55-
context.fireChannelRead(self.wrapInboundOut(.message(try RequestMessage(serializedData: messageAsData))))
54+
context.fireChannelRead(self.wrapInboundOut(.message(try RequestMessage(serializedByteBuffer: &message))))
5655
} catch {
5756
context.fireErrorCaught(GRPCError.DeserializationFailure().captureContext())
5857
}
@@ -75,8 +74,9 @@ extension GRPCServerCodec: ChannelOutboundHandler {
7574

7675
case .message(let message):
7776
do {
78-
let messageData = try message.serializedData()
79-
context.write(self.wrapOutboundOut(.message(messageData)), promise: promise)
77+
var buffer = ByteBufferAllocator().buffer(capacity: 0)
78+
try message.serialize(into: &buffer)
79+
context.write(self.wrapOutboundOut(.message(buffer.readData(length: buffer.readableBytes)!)), promise: promise)
8080
} catch {
8181
let error = GRPCError.SerializationFailure().captureContext()
8282
promise?.fail(error)

Sources/GRPC/ReadWriteStates.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ enum WriteState {
5454
/// - Parameter allocator: An allocator to provide a `ByteBuffer` into which the message will be
5555
/// written.
5656
mutating func write(
57-
_ message: Message,
57+
_ message: GRPCPayload,
5858
disableCompression: Bool,
5959
allocator: ByteBufferAllocator
6060
) -> Result<ByteBuffer, MessageWriteError> {
@@ -63,13 +63,19 @@ enum WriteState {
6363
return .failure(.cardinalityViolation)
6464

6565
case let .writing(writeArity, contentType, writer):
66-
guard let data = try? message.serializedData() else {
66+
// Zero is fine: the writer will allocate the correct amount of space.
67+
var buffer = allocator.buffer(capacity: 0)
68+
69+
do {
70+
try message.serialize(into: &buffer)
71+
} catch {
6772
self = .notWriting
6873
return .failure(.serializationFailed)
6974
}
70-
75+
7176
// Zero is fine: the writer will allocate the correct amount of space.
72-
var buffer = allocator.buffer(capacity: 0)
77+
let data = buffer.readData(length: buffer.readableBytes)!
78+
buffer.clear()
7379
do {
7480
try writer.write(data, into: &buffer, disableCompression: disableCompression)
7581
} catch {
@@ -115,7 +121,7 @@ enum ReadState {
115121
/// a message has been produced then subsequent calls will result in an error.
116122
///
117123
/// - Parameter buffer: The buffer to read from.
118-
mutating func readMessages<MessageType: Message>(
124+
mutating func readMessages<MessageType: GRPCPayload>(
119125
_ buffer: inout ByteBuffer,
120126
as: MessageType.Type = MessageType.self
121127
) -> Result<[MessageType], MessageReadError> {
@@ -130,8 +136,7 @@ enum ReadState {
130136
do {
131137
while var serializedBytes = try? reader.nextMessage() {
132138
// Force unwrapping is okay here: we will always be able to read `readableBytes`.
133-
let serializedData = serializedBytes.readData(length: serializedBytes.readableBytes)!
134-
messages.append(try MessageType(serializedData: serializedData))
139+
messages.append(try MessageType(serializedByteBuffer: &serializedBytes))
135140
}
136141
} catch {
137142
self = .notReading

Sources/GRPC/ServerCallContexts/StreamingResponseCallContext.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Logging
2525
/// - If `statusPromise` is failed and the error is of type `GRPCStatusTransformable`,
2626
/// the result of `error.asGRPCStatus()` will be returned to the client.
2727
/// - If `error.asGRPCStatus()` is not available, `GRPCStatus.processingError` is returned to the client.
28-
open class StreamingResponseCallContext<ResponseMessage: Message>: ServerCallContextBase {
28+
open class StreamingResponseCallContext<ResponseMessage: GRPCPayload>: ServerCallContextBase {
2929
typealias WrappedResponse = _GRPCServerResponsePart<ResponseMessage>
3030

3131
public let statusPromise: EventLoopPromise<GRPCStatus>
@@ -41,7 +41,7 @@ open class StreamingResponseCallContext<ResponseMessage: Message>: ServerCallCon
4141
}
4242

4343
/// Concrete implementation of `StreamingResponseCallContext` used by our generated code.
44-
open class StreamingResponseCallContextImpl<ResponseMessage: Message>: StreamingResponseCallContext<ResponseMessage> {
44+
open class StreamingResponseCallContextImpl<ResponseMessage: GRPCPayload>: StreamingResponseCallContext<ResponseMessage> {
4545
public let channel: Channel
4646

4747
/// - Parameters:
@@ -80,7 +80,7 @@ open class StreamingResponseCallContextImpl<ResponseMessage: Message>: Streaming
8080
/// Concrete implementation of `StreamingResponseCallContext` used for testing.
8181
///
8282
/// Simply records all sent messages.
83-
open class StreamingResponseCallContextTestStub<ResponseMessage: Message>: StreamingResponseCallContext<ResponseMessage> {
83+
open class StreamingResponseCallContextTestStub<ResponseMessage: GRPCPayload>: StreamingResponseCallContext<ResponseMessage> {
8484
open var recordedResponses: [ResponseMessage] = []
8585

8686
open override func sendResponse(_ message: ResponseMessage) -> EventLoopFuture<Void> {

0 commit comments

Comments
 (0)