Skip to content

Commit 14fcae9

Browse files
committed
Add echo-metadata example
1 parent 968f4dc commit 14fcae9

File tree

10 files changed

+385
-0
lines changed

10 files changed

+385
-0
lines changed

Examples/echo-metadata/.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc

Examples/echo-metadata/Package.swift

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// swift-tools-version:6.0
2+
/*
3+
* Copyright 2024, gRPC Authors All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import PackageDescription
19+
20+
let package = Package(
21+
name: "echo-metadata",
22+
platforms: [.macOS("15.0")],
23+
dependencies: [
24+
.package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"),
25+
.package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"),
26+
.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"),
27+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
28+
],
29+
targets: [
30+
.executableTarget(
31+
name: "echo-metadata",
32+
dependencies: [
33+
.product(name: "GRPCCore", package: "grpc-swift"),
34+
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
35+
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
36+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
37+
],
38+
plugins: [
39+
.plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf")
40+
]
41+
)
42+
]
43+
)

Examples/echo-metadata/README.md

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Echo-Metadata
2+
3+
This example demonstrates how to interact with `Metadata` on RPCs: how to set and read it on unary
4+
and streaming requests, as well as how to set and read both initial and trailing metadata on unary
5+
and streaming responses. This is done using a simple 'echo' server and client and the Swift NIO
6+
based HTTP/2 transport.
7+
8+
## Overview
9+
10+
An `echo-metadata` command line tool that uses generated stubs for an 'echo' service
11+
which allows you to start a server and to make requests against it. The client will automatically
12+
run a unary request followed by a bidirectional streaming request. In both cases, no message will
13+
be sent as part of the request: only the metadata provided as arguments to the executable will be
14+
included.
15+
The server will then echo back all metadata key-value pairs that begin with "echo-". No message
16+
will be included in the responses, and the echoed values will be included in both the initial and
17+
the trailing metadata.
18+
19+
The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) HTTP/2 transport.
20+
21+
## Prerequisites
22+
23+
You must have the Protocol Buffers compiler (`protoc`) installed. You can find
24+
the instructions for doing this in the [gRPC Swift Protobuf documentation][0].
25+
The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`,
26+
this is to let the build system know where `protoc` is located so that it can
27+
generate stubs for you. You can read more about it in the [gRPC Swift Protobuf
28+
documentation][1].
29+
30+
## Usage
31+
32+
Build and run the server using the CLI:
33+
34+
```console
35+
$ PROTOC_PATH=$(which protoc) swift run echo-metadata serve
36+
Echo-Metadata listening on [ipv4]127.0.0.1:1234
37+
```
38+
39+
Use the CLI to run the client and make a unary request followed by a bidirectional streaming one:
40+
41+
```console
42+
$ PROTOC_PATH=$(which protoc) swift run echo-metadata echo --metadata "echo-key=value" --metadata "another-key=value"
43+
unary → [("echo-key", value)]
44+
unary ← Initial metadata: [("echo-key", value)]
45+
unary ← Trailing metadata: [("echo-key", value)]
46+
bidirectional → [("echo-key", value)]
47+
bidirectional ← Initial metadata: [("echo-key", value)]
48+
bidirectional ← Trailing metadata: [("echo-key", value)]
49+
```
50+
51+
Get help with the CLI by running:
52+
53+
```console
54+
$ PROTOC_PATH=$(which protoc) swift run echo-metadata --help
55+
```
56+
57+
[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc
58+
[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2025, 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+
17+
import ArgumentParser
18+
import GRPCCore
19+
20+
@main
21+
struct EchoMetadata: AsyncParsableCommand {
22+
static let configuration = CommandConfiguration(
23+
commandName: "echo-metadata",
24+
abstract: "A multi-tool to run an echo-metadata server and execute RPCs against it.",
25+
subcommands: [Serve.self, Echo.self]
26+
)
27+
}
28+
29+
extension Metadata {
30+
var echoPairs: Self {
31+
Metadata(self.filter({ $0.key.starts(with: "echo-") }))
32+
}
33+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../dev/protos/examples/echo/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"generate": {
3+
"clients": true,
4+
"servers": true,
5+
"messages": true
6+
}
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2025, 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+
17+
import ArgumentParser
18+
import GRPCNIOTransportHTTP2
19+
20+
struct ClientArguments: ParsableArguments {
21+
@Option(help: "The server's listening port")
22+
var port: Int = 1234
23+
24+
@Option(
25+
help:
26+
"Metadata 'key=value' pair to send to the server. Key must begin with 'echo-' to be echoed back."
27+
)
28+
var metadata: [String]
29+
}
30+
31+
extension ClientArguments {
32+
var target: any ResolvableTarget {
33+
return .ipv4(host: "127.0.0.1", port: self.port)
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2025, 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+
17+
import ArgumentParser
18+
import GRPCCore
19+
import GRPCNIOTransportHTTP2
20+
21+
struct Echo: AsyncParsableCommand {
22+
static let configuration = CommandConfiguration(
23+
abstract:
24+
"Makes a unary RPC to the echo-metadata server, followed by a bidirectional-streaming request."
25+
)
26+
27+
@OptionGroup
28+
var arguments: ClientArguments
29+
30+
func run() async throws {
31+
try await withGRPCClient(
32+
transport: .http2NIOPosix(
33+
target: self.arguments.target,
34+
transportSecurity: .plaintext
35+
)
36+
) { client in
37+
let echo = Echo_Echo.Client(wrapping: client)
38+
39+
var requestMetadata: Metadata = [:]
40+
for metadataPair in arguments.metadata {
41+
guard metadataPair.starts(with: "echo-") else {
42+
continue
43+
}
44+
45+
let pair = metadataPair.split(separator: "=")
46+
if pair.count == 2, let key = pair.first.map(String.init),
47+
let value = pair.last.map(String.init)
48+
{
49+
requestMetadata.addString(value, forKey: key)
50+
}
51+
}
52+
53+
print("unary → \(requestMetadata)")
54+
try await echo.get(
55+
Echo_EchoRequest(),
56+
metadata: requestMetadata
57+
) { response in
58+
print("unary ← Initial metadata: \(response.metadata.echoPairs)")
59+
print("unary ← Trailing metadata: \(response.trailingMetadata)")
60+
}
61+
62+
print("bidirectional → \(requestMetadata)")
63+
try await echo.update(
64+
request: .init(
65+
metadata: requestMetadata,
66+
producer: { _ in }
67+
)
68+
) { response in
69+
print("bidirectional ← Initial metadata: \(response.metadata.echoPairs)")
70+
for try await part in try response.accepted.get().bodyParts {
71+
switch part {
72+
case .trailingMetadata(let trailingMetadata):
73+
print("bidirectional ← Trailing metadata: \(trailingMetadata.echoPairs)")
74+
75+
case .message:
76+
()
77+
}
78+
}
79+
}
80+
}
81+
}
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2025, 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+
17+
import GRPCCore
18+
19+
struct EchoService: Echo_Echo.ServiceProtocol {
20+
func get(
21+
request: ServerRequest<Echo_EchoRequest>,
22+
context: ServerContext
23+
) async throws -> ServerResponse<Echo_EchoResponse> {
24+
let responseMetadata = request.metadata.echoPairs
25+
return ServerResponse(
26+
message: .init(),
27+
metadata: responseMetadata,
28+
trailingMetadata: responseMetadata
29+
)
30+
}
31+
32+
func collect(
33+
request: StreamingServerRequest<Echo_EchoRequest>,
34+
context: ServerContext
35+
) async throws -> ServerResponse<Echo_EchoResponse> {
36+
let responseMetadata = request.metadata.echoPairs
37+
return ServerResponse(
38+
message: .init(),
39+
metadata: responseMetadata,
40+
trailingMetadata: responseMetadata
41+
)
42+
}
43+
44+
func expand(
45+
request: ServerRequest<Echo_EchoRequest>,
46+
context: ServerContext
47+
) async throws -> StreamingServerResponse<Echo_EchoResponse> {
48+
let responseMetadata = request.metadata.echoPairs
49+
return StreamingServerResponse(
50+
single: ServerResponse(
51+
message: .init(),
52+
metadata: responseMetadata,
53+
trailingMetadata: responseMetadata
54+
)
55+
)
56+
}
57+
58+
func update(
59+
request: StreamingServerRequest<Echo_EchoRequest>,
60+
context: ServerContext
61+
) async throws -> StreamingServerResponse<Echo_EchoResponse> {
62+
for try await _ in request.messages {
63+
// Wait for request to be done
64+
}
65+
66+
let responseMetadata = request.metadata.echoPairs
67+
return StreamingServerResponse(
68+
single: ServerResponse(
69+
message: .init(),
70+
metadata: responseMetadata,
71+
trailingMetadata: responseMetadata
72+
)
73+
)
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2025, 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+
17+
import ArgumentParser
18+
import GRPCCore
19+
import GRPCNIOTransportHTTP2
20+
21+
struct Serve: AsyncParsableCommand {
22+
static let configuration = CommandConfiguration(abstract: "Starts an echo-metadata server.")
23+
24+
@Option(help: "The port to listen on")
25+
var port: Int = 1234
26+
27+
func run() async throws {
28+
let server = GRPCServer(
29+
transport: .http2NIOPosix(
30+
address: .ipv4(host: "127.0.0.1", port: self.port),
31+
transportSecurity: .plaintext
32+
),
33+
services: [EchoService()]
34+
)
35+
36+
try await withThrowingDiscardingTaskGroup { group in
37+
group.addTask { try await server.serve() }
38+
if let address = try await server.listeningAddress {
39+
print("Echo-Metadata listening on \(address)")
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)