Skip to content

Commit

Permalink
Add echo-metadata example (#2182)
Browse files Browse the repository at this point in the history
This PR adds an example showcasing how to set/read request and response
metadata.

---------

Co-authored-by: George Barnett <gbarnett@apple.com>
  • Loading branch information
gjcairo and glbrntt authored Jan 31, 2025
1 parent 0e94c29 commit f20f916
Show file tree
Hide file tree
Showing 14 changed files with 539 additions and 1 deletion.
8 changes: 8 additions & 0 deletions Examples/echo-metadata/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
43 changes: 43 additions & 0 deletions Examples/echo-metadata/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// swift-tools-version:6.0
/*
* 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 PackageDescription

let package = Package(
name: "echo-metadata",
platforms: [.macOS("15.0")],
dependencies: [
.package(url: "https://github.com/grpc/grpc-swift.git", exact: "2.0.0-rc.1"),
.package(url: "https://github.com/grpc/grpc-swift-protobuf.git", exact: "1.0.0-rc.1"),
.package(url: "https://github.com/grpc/grpc-swift-nio-transport.git", exact: "1.0.0-rc.1"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
],
targets: [
.executableTarget(
name: "echo-metadata",
dependencies: [
.product(name: "GRPCCore", package: "grpc-swift"),
.product(name: "GRPCNIOTransportHTTP2", package: "grpc-swift-nio-transport"),
.product(name: "GRPCProtobuf", package: "grpc-swift-protobuf"),
.product(name: "ArgumentParser", package: "swift-argument-parser"),
],
plugins: [
.plugin(name: "GRPCProtobufGenerator", package: "grpc-swift-protobuf")
]
)
]
)
58 changes: 58 additions & 0 deletions Examples/echo-metadata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Echo-Metadata

This example demonstrates how to interact with `Metadata` on RPCs: how to set and read it on unary
and streaming requests, as well as how to set and read both initial and trailing metadata on unary
and streaming responses. This is done using a simple 'echo' server and client and the SwiftNIO
based HTTP/2 transport.

## Overview

An `echo-metadata` command line tool that uses generated stubs for an 'echo-metadata' service
which allows you to start a server and to make requests against it.

You can use any of the client's subcommands (`get`, `collect`, `expand` and `update`) to send the
provided `message` as both the request's message, and as the value for the `echo-message` key in
the request's metadata.

The server will then echo back the message and the metadata's `echo-message` key-value pair sent
by the client. The request's metadata will be echoed both in the initial and the trailing metadata.

The tool uses the [SwiftNIO](https://github.com/grpc/grpc-swift-nio-transport) HTTP/2 transport.

## Prerequisites

You must have the Protocol Buffers compiler (`protoc`) installed. You can find
the instructions for doing this in the [gRPC Swift Protobuf documentation][0].
The `swift` commands below are all prefixed with `PROTOC_PATH=$(which protoc)`,
this is to let the build system know where `protoc` is located so that it can
generate stubs for you. You can read more about it in the [gRPC Swift Protobuf
documentation][1].

## Usage

Build and run the server using the CLI:

```console
$ PROTOC_PATH=$(which protoc) swift run echo-metadata serve
Echo-Metadata listening on [ipv4]127.0.0.1:1234
```

Use the CLI to run the client and make a `get` (unary) request:

```console
$ PROTOC_PATH=$(which protoc) swift run echo-metadata get --message "hello"
get → metadata: [("echo-message", "hello")]
get → message: hello
get ← initial metadata: [("echo-message", "hello")]
get ← message: hello
get ← trailing metadata: [("echo-message", "hello")]
```

Get help with the CLI by running:

```console
$ PROTOC_PATH=$(which protoc) swift run echo-metadata --help
```

[0]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/installing-protoc
[1]: https://swiftpackageindex.com/grpc/grpc-swift-protobuf/documentation/grpcprotobuf/generating-stubs
35 changes: 35 additions & 0 deletions Examples/echo-metadata/Sources/ClientArguments.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2025, 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 GRPCNIOTransportHTTP2

struct ClientArguments: ParsableArguments {
@Option(help: "The server's listening port")
var port: Int = 1234

@Option(
help:
"Message to send to the server. It will also be sent in the request's metadata as the value for `echo-message`."
)
var message: String
}

extension ClientArguments {
var target: any ResolvableTarget {
return .ipv4(host: "127.0.0.1", port: self.port)
}
}
27 changes: 27 additions & 0 deletions Examples/echo-metadata/Sources/EchoMetadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2025, 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

@main
struct EchoMetadata: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "echo-metadata",
abstract: "A multi-tool to run an echo-metadata server and execute RPCs against it.",
subcommands: [Serve.self, Get.self, Collect.self, Update.self, Expand.self]
)
}
73 changes: 73 additions & 0 deletions Examples/echo-metadata/Sources/EchoService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright 2025, 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 GRPCCore

struct EchoService: Echo_Echo.ServiceProtocol {
func get(
request: ServerRequest<Echo_EchoRequest>,
context: ServerContext
) async throws -> ServerResponse<Echo_EchoResponse> {
let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
return ServerResponse(
message: .with { $0.text = request.message.text },
metadata: responseMetadata,
trailingMetadata: responseMetadata
)
}

func collect(
request: StreamingServerRequest<Echo_EchoRequest>,
context: ServerContext
) async throws -> ServerResponse<Echo_EchoResponse> {
let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
let messages = try await request.messages.reduce(into: []) { $0.append($1.text) }
let joined = messages.joined(separator: " ")

return ServerResponse(
message: .with { $0.text = joined },
metadata: responseMetadata,
trailingMetadata: responseMetadata
)
}

func expand(
request: ServerRequest<Echo_EchoRequest>,
context: ServerContext
) async throws -> StreamingServerResponse<Echo_EchoResponse> {
let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
let parts = request.message.text.split(separator: " ")
let messages = parts.map { part in Echo_EchoResponse.with { $0.text = String(part) } }

return StreamingServerResponse(metadata: responseMetadata) { writer in
try await writer.write(contentsOf: messages)
return responseMetadata
}
}

func update(
request: StreamingServerRequest<Echo_EchoRequest>,
context: ServerContext
) async throws -> StreamingServerResponse<Echo_EchoResponse> {
let responseMetadata = Metadata(request.metadata.filter({ $0.key.starts(with: "echo-") }))
return StreamingServerResponse(metadata: responseMetadata) { writer in
for try await message in request.messages {
try await writer.write(.with { $0.text = message.text })
}
return responseMetadata
}
}
}
1 change: 1 addition & 0 deletions Examples/echo-metadata/Sources/Protos/echo
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"generate": {
"clients": true,
"servers": true,
"messages": true
}
}
58 changes: 58 additions & 0 deletions Examples/echo-metadata/Sources/Subcommands/Collect.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2025, 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 GRPCNIOTransportHTTP2

struct Collect: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Makes a client streaming RPC to the echo-metadata server."
)

@OptionGroup
var arguments: ClientArguments

func run() async throws {
try await withGRPCClient(
transport: .http2NIOPosix(
target: self.arguments.target,
transportSecurity: .plaintext
)
) { client in
let echo = Echo_Echo.Client(wrapping: client)
let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"]

print("collect → metadata: \(requestMetadata)")
try await echo.collect(metadata: requestMetadata) { writer in
for part in self.arguments.message.split(separator: " ") {
print("collect → \(part)")
try await writer.write(.with { $0.text = String(part) })
}
} onResponse: { response in
let initialMetadata = Metadata(response.metadata.filter({ $0.key.starts(with: "echo-") }))
print("collect ← initial metadata: \(initialMetadata)")

print("collect ← message: \(try response.message.text)")

let trailingMetadata = Metadata(
response.trailingMetadata.filter({ $0.key.starts(with: "echo-") })
)
print("collect ← trailing metadata: \(trailingMetadata)")
}
}
}
}
65 changes: 65 additions & 0 deletions Examples/echo-metadata/Sources/Subcommands/Expand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2025, 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 GRPCNIOTransportHTTP2

struct Expand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Makes a server streaming RPC to the echo-metadata server."
)

@OptionGroup
var arguments: ClientArguments

func run() async throws {
try await withGRPCClient(
transport: .http2NIOPosix(
target: self.arguments.target,
transportSecurity: .plaintext
)
) { client in
let echo = Echo_Echo.Client(wrapping: client)
let requestMetadata: Metadata = ["echo-message": "\(arguments.message)"]
let message = Echo_EchoRequest.with { $0.text = self.arguments.message }

print("expand → metadata: \(requestMetadata)")
print("expand → message: \(message.text)")

try await echo.expand(message, metadata: requestMetadata) { response in
let responseContents = try response.accepted.get()

let initialMetadata = Metadata(
responseContents.metadata.filter({ $0.key.starts(with: "echo-") })
)
print("expand ← initial metadata: \(initialMetadata)")
for try await part in responseContents.bodyParts {
switch part {
case .message(let message):
print("expand ← message: \(message.text)")

case .trailingMetadata(let trailingMetadata):
let trailingMetadata = Metadata(
trailingMetadata.filter({ $0.key.starts(with: "echo-") })
)
print("expand ← trailing metadata: \(trailingMetadata)")
}
}
}
}
}
}
Loading

0 comments on commit f20f916

Please sign in to comment.