Skip to content

Commit f20f916

Browse files
gjcairoglbrntt
andauthored
Add echo-metadata example (#2182)
This PR adds an example showcasing how to set/read request and response metadata. --------- Co-authored-by: George Barnett <gbarnett@apple.com>
1 parent 0e94c29 commit f20f916

14 files changed

+539
-1
lines changed

Examples/echo-metadata/.gitignore

Lines changed: 8 additions & 0 deletions
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

Lines changed: 43 additions & 0 deletions
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

Lines changed: 58 additions & 0 deletions
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 SwiftNIO
6+
based HTTP/2 transport.
7+
8+
## Overview
9+
10+
An `echo-metadata` command line tool that uses generated stubs for an 'echo-metadata' service
11+
which allows you to start a server and to make requests against it.
12+
13+
You can use any of the client's subcommands (`get`, `collect`, `expand` and `update`) to send the
14+
provided `message` as both the request's message, and as the value for the `echo-message` key in
15+
the request's metadata.
16+
17+
The server will then echo back the message and the metadata's `echo-message` key-value pair sent
18+
by the client. The request's metadata will be echoed both in the initial and the trailing metadata.
19+
20+
The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) HTTP/2 transport.
21+
22+
## Prerequisites
23+
24+
You must have the Protocol Buffers compiler (`protoc`) installed. You can find
25+
the instructions for doing this in the [gRPC Swift Protobuf documentation][0].
26+
The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`,
27+
this is to let the build system know where `protoc` is located so that it can
28+
generate stubs for you. You can read more about it in the [gRPC Swift Protobuf
29+
documentation][1].
30+
31+
## Usage
32+
33+
Build and run the server using the CLI:
34+
35+
```console
36+
$ PROTOC_PATH=$(which protoc) swift run echo-metadata serve
37+
Echo-Metadata listening on [ipv4]127.0.0.1:1234
38+
```
39+
40+
Use the CLI to run the client and make a `get` (unary) request:
41+
42+
```console
43+
$ PROTOC_PATH=$(which protoc) swift run echo-metadata get --message "hello"
44+
get → metadata: [("echo-message", "hello")]
45+
get → message: hello
46+
get ← initial metadata: [("echo-message", "hello")]
47+
get ← message: hello
48+
get ← trailing metadata: [("echo-message", "hello")]
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
Lines changed: 35 additions & 0 deletions
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+
"Message to send to the server. It will also be sent in the request's metadata as the value for `echo-message`."
27+
)
28+
var message: String
29+
}
30+
31+
extension ClientArguments {
32+
var target: any ResolvableTarget {
33+
return .ipv4(host: "127.0.0.1", port: self.port)
34+
}
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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, Get.self, Collect.self, Update.self, Expand.self]
26+
)
27+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
25+
return ServerResponse(
26+
message: .with { $0.text = request.message.text },
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 = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
37+
let messages = try await request.messages.reduce(into: []) { $0.append($1.text) }
38+
let joined = messages.joined(separator: " ")
39+
40+
return ServerResponse(
41+
message: .with { $0.text = joined },
42+
metadata: responseMetadata,
43+
trailingMetadata: responseMetadata
44+
)
45+
}
46+
47+
func expand(
48+
request: ServerRequest<Echo_EchoRequest>,
49+
context: ServerContext
50+
) async throws -> StreamingServerResponse<Echo_EchoResponse> {
51+
let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
52+
let parts = request.message.text.split(separator: " ")
53+
let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } }
54+
55+
return StreamingServerResponse(metadata: responseMetadata) { writer in
56+
try await writer.write(contentsOf: messages)
57+
return responseMetadata
58+
}
59+
}
60+
61+
func update(
62+
request: StreamingServerRequest<Echo_EchoRequest>,
63+
context: ServerContext
64+
) async throws -> StreamingServerResponse<Echo_EchoResponse> {
65+
let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
66+
return StreamingServerResponse(metadata: responseMetadata) { writer in
67+
for try await message in request.messages {
68+
try await writer.write(.with { $0.text = message.text })
69+
}
70+
return responseMetadata
71+
}
72+
}
73+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../../../dev/protos/examples/echo/
Lines changed: 7 additions & 0 deletions
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+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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 Collect: AsyncParsableCommand {
22+
static let configuration = CommandConfiguration(
23+
abstract: "Makes a client streaming RPC to the echo-metadata server."
24+
)
25+
26+
@OptionGroup
27+
var arguments: ClientArguments
28+
29+
func run() async throws {
30+
try await withGRPCClient(
31+
transport: .http2NIOPosix(
32+
target: self.arguments.target,
33+
transportSecurity: .plaintext
34+
)
35+
) { client in
36+
let echo = Echo_Echo.Client(wrapping: client)
37+
let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"]
38+
39+
print("collect → metadata: \(requestMetadata)")
40+
try await echo.collect(metadata: requestMetadata) { writer in
41+
for part in self.arguments.message.split(separator: " ") {
42+
print("collect → \(part)")
43+
try await writer.write(.with { $0.text = String(part) })
44+
}
45+
} onResponse: { response in
46+
let initialMetadata = Metadata(response.metadata.filter({ $0.key.starts(with: "echo-") }))
47+
print("collect ← initial metadata: \(initialMetadata)")
48+
49+
print("collect ← message: \(try response.message.text)")
50+
51+
let trailingMetadata = Metadata(
52+
response.trailingMetadata.filter({ $0.key.starts(with: "echo-") })
53+
)
54+
print("collect ← trailing metadata: \(trailingMetadata)")
55+
}
56+
}
57+
}
58+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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 Expand: AsyncParsableCommand {
22+
static let configuration = CommandConfiguration(
23+
abstract: "Makes a server streaming RPC to the echo-metadata server."
24+
)
25+
26+
@OptionGroup
27+
var arguments: ClientArguments
28+
29+
func run() async throws {
30+
try await withGRPCClient(
31+
transport: .http2NIOPosix(
32+
target: self.arguments.target,
33+
transportSecurity: .plaintext
34+
)
35+
) { client in
36+
let echo = Echo_Echo.Client(wrapping: client)
37+
let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"]
38+
let message = Echo_EchoRequest.with { $0.text = self.arguments.message }
39+
40+
print("expand → metadata: \(requestMetadata)")
41+
print("expand → message: \(message.text)")
42+
43+
try await echo.expand(message, metadata: requestMetadata) { response in
44+
let responseContents = try response.accepted.get()
45+
46+
let initialMetadata = Metadata(
47+
responseContents.metadata.filter({ $0.key.starts(with: "echo-") })
48+
)
49+
print("expand ← initial metadata: \(initialMetadata)")
50+
for try await part in responseContents.bodyParts {
51+
switch part {
52+
case .message(let message):
53+
print("expand ← message: \(message.text)")
54+
55+
case .trailingMetadata(let trailingMetadata):
56+
let trailingMetadata = Metadata(
57+
trailingMetadata.filter({ $0.key.starts(with: "echo-") })
58+
)
59+
print("expand ← trailing metadata: \(trailingMetadata)")
60+
}
61+
}
62+
}
63+
}
64+
}
65+
}

0 commit comments

Comments
 (0)