Jelajahi Sumber

Add a v2 Echo example (#1964)

Motivation:

We should migrate the echo example to v2.

Modification:

Add a v2 echo example
Result:

We can run echo for v2.
George Barnett 1 tahun lalu
induk
melakukan
43aa501c50

+ 21 - 1
Package@swift-6.swift

@@ -626,6 +626,23 @@ extension Target {
     )
   }
 
+  static var echo_v2: Target {
+    .executableTarget(
+      name: "echo-v2",
+      dependencies: [
+        .grpcCore,
+        .grpcProtobuf,
+        .grpcHTTP2Core,
+        .grpcHTTP2TransportNIOPosix,
+        .argumentParser,
+      ].appending(
+        .nioSSL, if: includeNIOSSL
+      ),
+      path: "Sources/Examples/v2/Echo",
+      swiftSettings: [.swiftLanguageVersion(.v6), .enableUpcomingFeature("ExistentialAny")]
+    )
+  }
+
   static var helloWorldModel: Target {
     .target(
       name: "HelloWorldModel",
@@ -935,7 +952,10 @@ let package = Package(
     .grpcHTTP2TransportTests,
     .grpcProtobufTests,
     .grpcProtobufCodeGenTests,
-    .inProcessInteroperabilityTests
+    .inProcessInteroperabilityTests,
+
+    // v2 examples
+    .echo_v2,
   ]
 )
 

+ 27 - 0
Sources/Examples/v2/Echo/Echo.swift

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+
+@main
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+struct Echo: AsyncParsableCommand {
+  static let configuration = CommandConfiguration(
+    commandName: "echo",
+    abstract: "A multi-tool to run an echo server and execute RPCs against it.",
+    subcommands: [Serve.self, Get.self, Collect.self, Expand.self, Update.self]
+  )
+}

+ 344 - 0
Sources/Examples/v2/Echo/Generated/echo.grpc.swift

@@ -0,0 +1,344 @@
+// Copyright (c) 2015, Google 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.
+
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
+// Source: echo.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/grpc/grpc-swift
+
+import GRPCCore
+import GRPCProtobuf
+
+internal enum Echo_Echo {
+    internal enum Method {
+        internal enum Get {
+            internal typealias Input = Echo_EchoRequest
+            internal typealias Output = Echo_EchoResponse
+            internal static let descriptor = MethodDescriptor(
+                service: "echo.Echo",
+                method: "Get"
+            )
+        }
+        internal enum Expand {
+            internal typealias Input = Echo_EchoRequest
+            internal typealias Output = Echo_EchoResponse
+            internal static let descriptor = MethodDescriptor(
+                service: "echo.Echo",
+                method: "Expand"
+            )
+        }
+        internal enum Collect {
+            internal typealias Input = Echo_EchoRequest
+            internal typealias Output = Echo_EchoResponse
+            internal static let descriptor = MethodDescriptor(
+                service: "echo.Echo",
+                method: "Collect"
+            )
+        }
+        internal enum Update {
+            internal typealias Input = Echo_EchoRequest
+            internal typealias Output = Echo_EchoResponse
+            internal static let descriptor = MethodDescriptor(
+                service: "echo.Echo",
+                method: "Update"
+            )
+        }
+        internal static let descriptors: [MethodDescriptor] = [
+            Get.descriptor,
+            Expand.descriptor,
+            Collect.descriptor,
+            Update.descriptor
+        ]
+    }
+    @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+    internal typealias StreamingServiceProtocol = Echo_EchoStreamingServiceProtocol
+    @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+    internal typealias ServiceProtocol = Echo_EchoServiceProtocol
+    @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+    internal typealias ClientProtocol = Echo_EchoClientProtocol
+    @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+    internal typealias Client = Echo_EchoClient
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+internal protocol Echo_EchoStreamingServiceProtocol: GRPCCore.RegistrableRPCService {
+    /// Immediately returns an echo of a request.
+    func get(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse>
+
+    /// Splits a request into words and returns each word in a stream of messages.
+    func expand(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse>
+
+    /// Collects a stream of messages and returns them concatenated when the caller closes.
+    func collect(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse>
+
+    /// Streams back messages as they are received in an input stream.
+    func update(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse>
+}
+
+/// Conformance to `GRPCCore.RegistrableRPCService`.
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension Echo_Echo.StreamingServiceProtocol {
+    @available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+    internal func registerMethods(with router: inout GRPCCore.RPCRouter) {
+        router.registerHandler(
+            forMethod: Echo_Echo.Method.Get.descriptor,
+            deserializer: ProtobufDeserializer<Echo_EchoRequest>(),
+            serializer: ProtobufSerializer<Echo_EchoResponse>(),
+            handler: { request in
+                try await self.get(request: request)
+            }
+        )
+        router.registerHandler(
+            forMethod: Echo_Echo.Method.Expand.descriptor,
+            deserializer: ProtobufDeserializer<Echo_EchoRequest>(),
+            serializer: ProtobufSerializer<Echo_EchoResponse>(),
+            handler: { request in
+                try await self.expand(request: request)
+            }
+        )
+        router.registerHandler(
+            forMethod: Echo_Echo.Method.Collect.descriptor,
+            deserializer: ProtobufDeserializer<Echo_EchoRequest>(),
+            serializer: ProtobufSerializer<Echo_EchoResponse>(),
+            handler: { request in
+                try await self.collect(request: request)
+            }
+        )
+        router.registerHandler(
+            forMethod: Echo_Echo.Method.Update.descriptor,
+            deserializer: ProtobufDeserializer<Echo_EchoRequest>(),
+            serializer: ProtobufSerializer<Echo_EchoResponse>(),
+            handler: { request in
+                try await self.update(request: request)
+            }
+        )
+    }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+internal protocol Echo_EchoServiceProtocol: Echo_Echo.StreamingServiceProtocol {
+    /// Immediately returns an echo of a request.
+    func get(request: ServerRequest.Single<Echo_EchoRequest>) async throws -> ServerResponse.Single<Echo_EchoResponse>
+
+    /// Splits a request into words and returns each word in a stream of messages.
+    func expand(request: ServerRequest.Single<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse>
+
+    /// Collects a stream of messages and returns them concatenated when the caller closes.
+    func collect(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Single<Echo_EchoResponse>
+
+    /// Streams back messages as they are received in an input stream.
+    func update(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse>
+}
+
+/// Partial conformance to `Echo_EchoStreamingServiceProtocol`.
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension Echo_Echo.ServiceProtocol {
+    internal func get(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse> {
+        let response = try await self.get(request: ServerRequest.Single(stream: request))
+        return ServerResponse.Stream(single: response)
+    }
+
+    internal func expand(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse> {
+        let response = try await self.expand(request: ServerRequest.Single(stream: request))
+        return response
+    }
+
+    internal func collect(request: ServerRequest.Stream<Echo_EchoRequest>) async throws -> ServerResponse.Stream<Echo_EchoResponse> {
+        let response = try await self.collect(request: request)
+        return ServerResponse.Stream(single: response)
+    }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+internal protocol Echo_EchoClientProtocol: Sendable {
+    /// Immediately returns an echo of a request.
+    func get<R>(
+        request: ClientRequest.Single<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions,
+        _ body: @Sendable @escaping (ClientResponse.Single<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable
+
+    /// Splits a request into words and returns each word in a stream of messages.
+    func expand<R>(
+        request: ClientRequest.Single<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions,
+        _ body: @Sendable @escaping (ClientResponse.Stream<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable
+
+    /// Collects a stream of messages and returns them concatenated when the caller closes.
+    func collect<R>(
+        request: ClientRequest.Stream<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions,
+        _ body: @Sendable @escaping (ClientResponse.Single<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable
+
+    /// Streams back messages as they are received in an input stream.
+    func update<R>(
+        request: ClientRequest.Stream<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions,
+        _ body: @Sendable @escaping (ClientResponse.Stream<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+extension Echo_Echo.ClientProtocol {
+    internal func get<R>(
+        request: ClientRequest.Single<Echo_EchoRequest>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Single<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.get(
+            request: request,
+            serializer: ProtobufSerializer<Echo_EchoRequest>(),
+            deserializer: ProtobufDeserializer<Echo_EchoResponse>(),
+            options: options,
+            body
+        )
+    }
+
+    internal func expand<R>(
+        request: ClientRequest.Single<Echo_EchoRequest>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Stream<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.expand(
+            request: request,
+            serializer: ProtobufSerializer<Echo_EchoRequest>(),
+            deserializer: ProtobufDeserializer<Echo_EchoResponse>(),
+            options: options,
+            body
+        )
+    }
+
+    internal func collect<R>(
+        request: ClientRequest.Stream<Echo_EchoRequest>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Single<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.collect(
+            request: request,
+            serializer: ProtobufSerializer<Echo_EchoRequest>(),
+            deserializer: ProtobufDeserializer<Echo_EchoResponse>(),
+            options: options,
+            body
+        )
+    }
+
+    internal func update<R>(
+        request: ClientRequest.Stream<Echo_EchoRequest>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Stream<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.update(
+            request: request,
+            serializer: ProtobufSerializer<Echo_EchoRequest>(),
+            deserializer: ProtobufDeserializer<Echo_EchoResponse>(),
+            options: options,
+            body
+        )
+    }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+internal struct Echo_EchoClient: Echo_Echo.ClientProtocol {
+    private let client: GRPCCore.GRPCClient
+
+    internal init(client: GRPCCore.GRPCClient) {
+        self.client = client
+    }
+
+    /// Immediately returns an echo of a request.
+    internal func get<R>(
+        request: ClientRequest.Single<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Single<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.client.unary(
+            request: request,
+            descriptor: Echo_Echo.Method.Get.descriptor,
+            serializer: serializer,
+            deserializer: deserializer,
+            options: options,
+            handler: body
+        )
+    }
+
+    /// Splits a request into words and returns each word in a stream of messages.
+    internal func expand<R>(
+        request: ClientRequest.Single<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Stream<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.client.serverStreaming(
+            request: request,
+            descriptor: Echo_Echo.Method.Expand.descriptor,
+            serializer: serializer,
+            deserializer: deserializer,
+            options: options,
+            handler: body
+        )
+    }
+
+    /// Collects a stream of messages and returns them concatenated when the caller closes.
+    internal func collect<R>(
+        request: ClientRequest.Stream<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Single<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.client.clientStreaming(
+            request: request,
+            descriptor: Echo_Echo.Method.Collect.descriptor,
+            serializer: serializer,
+            deserializer: deserializer,
+            options: options,
+            handler: body
+        )
+    }
+
+    /// Streams back messages as they are received in an input stream.
+    internal func update<R>(
+        request: ClientRequest.Stream<Echo_EchoRequest>,
+        serializer: some MessageSerializer<Echo_EchoRequest>,
+        deserializer: some MessageDeserializer<Echo_EchoResponse>,
+        options: CallOptions = .defaults,
+        _ body: @Sendable @escaping (ClientResponse.Stream<Echo_EchoResponse>) async throws -> R
+    ) async throws -> R where R: Sendable {
+        try await self.client.bidirectionalStreaming(
+            request: request,
+            descriptor: Echo_Echo.Method.Update.descriptor,
+            serializer: serializer,
+            deserializer: deserializer,
+            options: options,
+            handler: body
+        )
+    }
+}

+ 134 - 0
Sources/Examples/v2/Echo/Generated/echo.pb.swift

@@ -0,0 +1,134 @@
+// DO NOT EDIT.
+// swift-format-ignore-file
+//
+// Generated by the Swift generator plugin for the protocol buffer compiler.
+// Source: echo.proto
+//
+// For information on using the generated types, please see the documentation:
+//   https://github.com/apple/swift-protobuf/
+
+// Copyright (c) 2015, Google 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
+import SwiftProtobuf
+
+// If the compiler emits an error on this type, it is because this file
+// was generated by a version of the `protoc` Swift plug-in that is
+// incompatible with the version of SwiftProtobuf to which you are linking.
+// Please ensure that you are building against the same version of the API
+// that was used to generate this file.
+fileprivate struct _GeneratedWithProtocGenSwiftVersion: SwiftProtobuf.ProtobufAPIVersionCheck {
+  struct _2: SwiftProtobuf.ProtobufAPIVersion_2 {}
+  typealias Version = _2
+}
+
+struct Echo_EchoRequest {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// The text of a message to be echoed.
+  var text: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+struct Echo_EchoResponse {
+  // SwiftProtobuf.Message conformance is added in an extension below. See the
+  // `Message` and `Message+*Additions` files in the SwiftProtobuf library for
+  // methods supported on all messages.
+
+  /// The text of an echo response.
+  var text: String = String()
+
+  var unknownFields = SwiftProtobuf.UnknownStorage()
+
+  init() {}
+}
+
+#if swift(>=5.5) && canImport(_Concurrency)
+extension Echo_EchoRequest: @unchecked Sendable {}
+extension Echo_EchoResponse: @unchecked Sendable {}
+#endif  // swift(>=5.5) && canImport(_Concurrency)
+
+// MARK: - Code below here is support for the SwiftProtobuf runtime.
+
+fileprivate let _protobuf_package = "echo"
+
+extension Echo_EchoRequest: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".EchoRequest"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "text"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.text) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.text.isEmpty {
+      try visitor.visitSingularStringField(value: self.text, fieldNumber: 1)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Echo_EchoRequest, rhs: Echo_EchoRequest) -> Bool {
+    if lhs.text != rhs.text {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}
+
+extension Echo_EchoResponse: SwiftProtobuf.Message, SwiftProtobuf._MessageImplementationBase, SwiftProtobuf._ProtoNameProviding {
+  static let protoMessageName: String = _protobuf_package + ".EchoResponse"
+  static let _protobuf_nameMap: SwiftProtobuf._NameMap = [
+    1: .same(proto: "text"),
+  ]
+
+  mutating func decodeMessage<D: SwiftProtobuf.Decoder>(decoder: inout D) throws {
+    while let fieldNumber = try decoder.nextFieldNumber() {
+      // The use of inline closures is to circumvent an issue where the compiler
+      // allocates stack space for every case branch when no optimizations are
+      // enabled. https://github.com/apple/swift-protobuf/issues/1034
+      switch fieldNumber {
+      case 1: try { try decoder.decodeSingularStringField(value: &self.text) }()
+      default: break
+      }
+    }
+  }
+
+  func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
+    if !self.text.isEmpty {
+      try visitor.visitSingularStringField(value: self.text, fieldNumber: 1)
+    }
+    try unknownFields.traverse(visitor: &visitor)
+  }
+
+  static func ==(lhs: Echo_EchoResponse, rhs: Echo_EchoResponse) -> Bool {
+    if lhs.text != rhs.text {return false}
+    if lhs.unknownFields != rhs.unknownFields {return false}
+    return true
+  }
+}

+ 35 - 0
Sources/Examples/v2/Echo/Subcommands/ClientArguments.swift

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+import GRPCHTTP2Core
+
+struct ClientArguments: ParsableArguments {
+  @Option(help: "The server's listening port")
+  var port: Int = 1234
+
+  @Option(help: "The number of times to repeat the call")
+  var repetitions: Int = 1
+
+  @Option(help: "Message to send to the server")
+  var message: String
+}
+
+extension ClientArguments {
+  var target: any ResolvableTarget {
+    return .ipv4(host: "127.0.0.1", port: self.port)
+  }
+}

+ 59 - 0
Sources/Examples/v2/Echo/Subcommands/Collect.swift

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+import GRPCCore
+import GRPCHTTP2Core
+import GRPCHTTP2TransportNIOPosix
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+struct Collect: AsyncParsableCommand {
+  static let configuration = CommandConfiguration(
+    abstract: "Makes a client streaming RPC to the echo server."
+  )
+
+  @OptionGroup
+  var arguments: ClientArguments
+
+  func run() async throws {
+    let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target)
+    let client = GRPCClient(transport: transport)
+
+    try await withThrowingDiscardingTaskGroup { group in
+      group.addTask {
+        try await client.run()
+      }
+
+      let echo = Echo_EchoClient(client: client)
+
+      for _ in 0 ..< self.arguments.repetitions {
+        let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in
+          for part in self.arguments.message.split(separator: " ") {
+            print("collect → \(part)")
+            try await writer.write(.with { $0.text = String(part) })
+          }
+        }
+
+        try await echo.collect(request: request) { response in
+          let message = try response.message
+          print("collect ← \(message.text)")
+        }
+      }
+
+      client.close()
+    }
+  }
+}

+ 55 - 0
Sources/Examples/v2/Echo/Subcommands/Expand.swift

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+import GRPCCore
+import GRPCHTTP2Core
+import GRPCHTTP2TransportNIOPosix
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+struct Expand: AsyncParsableCommand {
+  static let configuration = CommandConfiguration(
+    abstract: "Makes a server streaming RPC to the echo server."
+  )
+
+  @OptionGroup
+  var arguments: ClientArguments
+
+  func run() async throws {
+    let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target)
+    let client = GRPCClient(transport: transport)
+
+    try await withThrowingDiscardingTaskGroup { group in
+      group.addTask {
+        try await client.run()
+      }
+
+      let echo = Echo_EchoClient(client: client)
+
+      for _ in 0 ..< self.arguments.repetitions {
+        let message = Echo_EchoRequest.with { $0.text = self.arguments.message }
+        print("expand → \(message.text)")
+        try await echo.expand(request: ClientRequest.Single(message: message)) { response in
+          for try await message in response.messages {
+            print("get ← \(message.text)")
+          }
+        }
+      }
+
+      client.close()
+    }
+  }
+}

+ 52 - 0
Sources/Examples/v2/Echo/Subcommands/Get.swift

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+import GRPCCore
+import GRPCHTTP2Core
+import GRPCHTTP2TransportNIOPosix
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+struct Get: AsyncParsableCommand {
+  static let configuration = CommandConfiguration(abstract: "Makes a unary RPC to the echo server.")
+
+  @OptionGroup
+  var arguments: ClientArguments
+
+  func run() async throws {
+    let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target)
+    let client = GRPCClient(transport: transport)
+
+    try await withThrowingDiscardingTaskGroup { group in
+      group.addTask {
+        try await client.run()
+      }
+
+      let echo = Echo_EchoClient(client: client)
+
+      for _ in 0 ..< self.arguments.repetitions {
+        let message = Echo_EchoRequest.with { $0.text = self.arguments.message }
+        print("get → \(message.text)")
+        try await echo.get(request: ClientRequest.Single(message: message)) { response in
+          let message = try response.message
+          print("get ← \(message.text)")
+        }
+      }
+
+      client.close()
+    }
+  }
+}

+ 77 - 0
Sources/Examples/v2/Echo/Subcommands/Serve.swift

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+import GRPCCore
+import GRPCHTTP2Core
+import GRPCHTTP2TransportNIOPosix
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+struct Serve: AsyncParsableCommand {
+  static let configuration = CommandConfiguration(abstract: "Starts an echo server.")
+
+  @Option(help: "The port to listen on")
+  var port: Int = 1234
+
+  func run() async throws {
+    let transport = HTTP2ServerTransport.Posix(address: .ipv4(host: "127.0.0.1", port: self.port))
+    let server = GRPCServer(transport: transport, services: [EchoService()])
+    try await withThrowingDiscardingTaskGroup { group in
+      group.addTask { try await server.run() }
+      let address = try await transport.listeningAddress
+      print("server listening on \(address)")
+    }
+  }
+}
+
+@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)
+struct EchoService: Echo_EchoServiceProtocol {
+  func get(
+    request: ServerRequest.Single<Echo_EchoRequest>
+  ) async throws -> ServerResponse.Single<Echo_EchoResponse> {
+    return ServerResponse.Single(message: .with { $0.text = request.message.text })
+  }
+
+  func collect(
+    request: ServerRequest.Stream<Echo_EchoRequest>
+  ) async throws -> ServerResponse.Single<Echo_EchoResponse> {
+    let messages = try await request.messages.reduce(into: []) { $0.append($1.text) }
+    let joined = messages.joined(separator: " ")
+    return ServerResponse.Single(message: .with { $0.text = joined })
+  }
+
+  func expand(
+    request: ServerRequest.Single<Echo_EchoRequest>
+  ) async throws -> ServerResponse.Stream<Echo_EchoResponse> {
+    return ServerResponse.Stream { writer in
+      let parts = request.message.text.split(separator: " ")
+      let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } }
+      try await writer.write(contentsOf: messages)
+      return [:]
+    }
+  }
+
+  func update(
+    request: ServerRequest.Stream<Echo_EchoRequest>
+  ) async throws -> ServerResponse.Stream<Echo_EchoResponse> {
+    return ServerResponse.Stream { writer in
+      for try await message in request.messages {
+        try await writer.write(.with { $0.text = message.text })
+      }
+      return [:]
+    }
+  }
+}

+ 60 - 0
Sources/Examples/v2/Echo/Subcommands/Update.swift

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2024, gRPC Authors All rights reserved.
+ *
+ * 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 ArgumentParser
+import GRPCCore
+import GRPCHTTP2Core
+import GRPCHTTP2TransportNIOPosix
+
+@available(macOS 14.0, iOS 17.0, watchOS 10.0, tvOS 17.0, *)
+struct Update: AsyncParsableCommand {
+  static let configuration = CommandConfiguration(
+    abstract: "Makes a bidirectional server streaming RPC to the echo server."
+  )
+
+  @OptionGroup
+  var arguments: ClientArguments
+
+  func run() async throws {
+    let transport = try HTTP2ClientTransport.Posix(target: self.arguments.target)
+    let client = GRPCClient(transport: transport)
+
+    try await withThrowingDiscardingTaskGroup { group in
+      group.addTask {
+        try await client.run()
+      }
+
+      let echo = Echo_EchoClient(client: client)
+
+      for _ in 0 ..< self.arguments.repetitions {
+        let request = ClientRequest.Stream(of: Echo_EchoRequest.self) { writer in
+          for part in self.arguments.message.split(separator: " ") {
+            print("update → \(part)")
+            try await writer.write(.with { $0.text = String(part) })
+          }
+        }
+
+        try await echo.update(request: request) { response in
+          for try await message in response.messages {
+            print("update ← \(message.text)")
+          }
+        }
+      }
+
+      client.close()
+    }
+  }
+}