Skip to content

Commit 965e157

Browse files
authored
Merge branch 'main' into metadata-description
2 parents c6424dd + f163392 commit 965e157

File tree

10 files changed

+528
-2
lines changed

10 files changed

+528
-2
lines changed

Sources/GRPCCore/Call/Client/ClientResponse.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ extension StreamingClientResponse {
363363

364364
/// Returns the messages received from the server.
365365
///
366-
/// For rejected RPCs the `RPCAsyncSequence` throws a `RPCError``.
366+
/// For rejected RPCs (in other words, where ``accepted`` is `failure`), the `RPCAsyncSequence` throws a ``RPCError``.
367367
public var messages: RPCAsyncSequence<Message, any Error> {
368368
switch self.accepted {
369369
case let .success(contents):
@@ -382,4 +382,19 @@ extension StreamingClientResponse {
382382
return RPCAsyncSequence.throwing(error)
383383
}
384384
}
385+
386+
/// Returns the body parts (i.e. `messages` and `trailingMetadata`) returned from the server.
387+
///
388+
/// For rejected RPCs (in other words, where ``accepted`` is `failure`), the `RPCAsyncSequence` throws a ``RPCError``.
389+
public var bodyParts: RPCAsyncSequence<Contents.BodyPart, any Error> {
390+
switch self.accepted {
391+
case let .success(contents):
392+
return contents.bodyParts
393+
394+
case let .failure(error):
395+
return RPCAsyncSequence.throwing(error)
396+
}
397+
}
385398
}
399+
400+
extension StreamingClientResponse.Contents.BodyPart: Equatable where Message: Equatable {}

Tests/GRPCCoreTests/Call/Client/ClientResponseTests.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ final class ClientResponseTests: XCTestCase {
5353
XCTAssertEqual(response.trailingMetadata, ["bar": "baz"])
5454
}
5555

56-
func testAcceptedStreamResponseConvenienceMethods() async throws {
56+
func testAcceptedStreamResponseConvenienceMethods_Messages() async throws {
5757
let response = StreamingClientResponse(
5858
of: String.self,
5959
metadata: ["foo": "bar"],
@@ -73,6 +73,29 @@ final class ClientResponseTests: XCTestCase {
7373
XCTAssertEqual(messages, ["foo", "bar", "baz"])
7474
}
7575

76+
func testAcceptedStreamResponseConvenienceMethods_BodyParts() async throws {
77+
let response = StreamingClientResponse(
78+
of: String.self,
79+
metadata: ["foo": "bar"],
80+
bodyParts: RPCAsyncSequence(
81+
wrapping: AsyncThrowingStream {
82+
$0.yield(.message("foo"))
83+
$0.yield(.message("bar"))
84+
$0.yield(.message("baz"))
85+
$0.yield(.trailingMetadata(["baz": "baz"]))
86+
$0.finish()
87+
}
88+
)
89+
)
90+
91+
XCTAssertEqual(response.metadata, ["foo": "bar"])
92+
let bodyParts = try await response.bodyParts.collect()
93+
XCTAssertEqual(
94+
bodyParts,
95+
[.message("foo"), .message("bar"), .message("baz"), .trailingMetadata(["baz": "baz"])]
96+
)
97+
}
98+
7699
func testRejectedStreamResponseConvenienceMethods() async throws {
77100
let error = RPCError(code: .aborted, message: "error message", metadata: ["bar": "baz"])
78101
let response = StreamingClientResponse(of: String.self, error: error)
@@ -83,6 +106,11 @@ final class ClientResponseTests: XCTestCase {
83106
} errorHandler: {
84107
XCTAssertEqual($0, error)
85108
}
109+
await XCTAssertThrowsRPCErrorAsync {
110+
try await response.bodyParts.collect()
111+
} errorHandler: {
112+
XCTAssertEqual($0, error)
113+
}
86114
}
87115

88116
func testStreamToSingleConversionForValidStream() async throws {

dev/format.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ if "$lint"; then
6161
"${repo}/Tests" \
6262
"${repo}/Examples" \
6363
"${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \
64+
"${repo}/dev" \
6465
&& SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$?
6566

6667
if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then
@@ -80,6 +81,7 @@ elif "$format"; then
8081
"${repo}/Tests" \
8182
"${repo}/Examples" \
8283
"${repo}/IntegrationTests/Benchmarks/Benchmarks/GRPCSwiftBenchmark" \
84+
"${repo}/dev" \
8385
&& SWIFT_FORMAT_RC=$? || SWIFT_FORMAT_RC=$?
8486

8587
if [[ "${SWIFT_FORMAT_RC}" -ne 0 ]]; then

dev/grpc-dev-tool/.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

dev/grpc-dev-tool/Package.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// swift-tools-version:6.0
2+
/*
3+
* Copyright 2025, 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: "grpc-dev-tool",
22+
platforms: [.macOS(.v15)],
23+
dependencies: [
24+
.package(path: "../.."),
25+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0"),
26+
],
27+
targets: [
28+
.executableTarget(
29+
name: "grpc-dev-tool",
30+
dependencies: [
31+
.product(name: "GRPCCodeGen", package: "grpc-swift"),
32+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
33+
]
34+
)
35+
]
36+
)
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
19+
@main
20+
struct GRPCDevTool: AsyncParsableCommand {
21+
static let configuration = CommandConfiguration(
22+
commandName: "grpc-dev-tool",
23+
subcommands: [GenerateJSON.self]
24+
)
25+
}
Lines changed: 75 additions & 0 deletions
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 GRPCCodeGen
18+
19+
/// Creates a `ServiceDescriptor` from a JSON `ServiceSchema`.
20+
extension ServiceDescriptor {
21+
init(_ service: ServiceSchema) {
22+
self.init(
23+
documentation: "",
24+
name: .init(
25+
identifyingName: service.name,
26+
typeName: service.name,
27+
propertyName: service.name
28+
),
29+
methods: service.methods.map {
30+
MethodDescriptor($0)
31+
}
32+
)
33+
}
34+
}
35+
36+
extension MethodDescriptor {
37+
/// Creates a `MethodDescriptor` from a JSON `ServiceSchema.Method`.
38+
init(_ method: ServiceSchema.Method) {
39+
self.init(
40+
documentation: "",
41+
name: .init(
42+
identifyingName: method.name,
43+
typeName: method.name,
44+
functionName: method.name
45+
),
46+
isInputStreaming: method.kind.streamsInput,
47+
isOutputStreaming: method.kind.streamsOutput,
48+
inputType: method.input,
49+
outputType: method.output
50+
)
51+
}
52+
}
53+
54+
extension CodeGenerator.Config.AccessLevel {
55+
init(_ level: GeneratorConfig.AccessLevel) {
56+
switch level {
57+
case .internal:
58+
self = .internal
59+
case .package:
60+
self = .package
61+
}
62+
}
63+
}
64+
65+
extension CodeGenerator.Config {
66+
init(_ config: GeneratorConfig) {
67+
self.init(
68+
accessLevel: CodeGenerator.Config.AccessLevel(config.accessLevel),
69+
accessLevelOnImports: config.accessLevelOnImports,
70+
client: config.generateClient,
71+
server: config.generateServer,
72+
indentation: 2
73+
)
74+
}
75+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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 Foundation
19+
20+
struct GenerateJSON: ParsableCommand {
21+
static let configuration = CommandConfiguration(
22+
commandName: "generate-json",
23+
subcommands: [Generate.self, DumpConfig.self],
24+
defaultSubcommand: Generate.self
25+
)
26+
}
27+
28+
extension GenerateJSON {
29+
struct Generate: ParsableCommand {
30+
@Argument(help: "The path to a JSON input file.")
31+
var input: String
32+
33+
func run() throws {
34+
// Decode the input file.
35+
let url = URL(filePath: self.input)
36+
let data = try Data(contentsOf: url)
37+
let json = JSONDecoder()
38+
let config = try json.decode(JSONCodeGeneratorRequest.self, from: data)
39+
40+
// Generate the output and dump it to stdout.
41+
let generator = JSONCodeGenerator()
42+
let sourceFile = try generator.generate(request: config)
43+
print(sourceFile.contents)
44+
}
45+
}
46+
}
47+
48+
extension GenerateJSON {
49+
struct DumpConfig: ParsableCommand {
50+
func run() throws {
51+
// Create a request for the code generator using all four RPC kinds.
52+
var request = JSONCodeGeneratorRequest(
53+
service: ServiceSchema(name: "Echo", methods: []),
54+
config: .defaults
55+
)
56+
57+
let methodNames = ["get", "collect", "expand", "update"]
58+
let methodKinds: [ServiceSchema.Method.Kind] = [
59+
.unary,
60+
.clientStreaming,
61+
.serverStreaming,
62+
.bidiStreaming,
63+
]
64+
65+
for (name, kind) in zip(methodNames, methodKinds) {
66+
let method = ServiceSchema.Method(
67+
name: name,
68+
input: "EchoRequest",
69+
output: "EchoResponse",
70+
kind: kind
71+
)
72+
request.service.methods.append(method)
73+
}
74+
75+
// Encoding the config to JSON and dump it to stdout.
76+
let encoder = JSONEncoder()
77+
encoder.outputFormatting = [.prettyPrinted]
78+
let data = try encoder.encode(request)
79+
let json = String(decoding: data, as: UTF8.self)
80+
print(json)
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)