Skip to content

Commit 8fbe023

Browse files
committed
New protocol and generator options for asynchronous mutating traversal of messages
## Motivation Our current `Visitor` protocol and generated `traverse` methods allow synchronous throwing traversal of the message fields. This works great for how our current serializers are working; however, often one wants to implement validators or transformers that mutate the fields of message. Sometimes this mutation is even asynchronous. ## Modification This PR adds a few things: - A new `AsyncVisitor` protocol that allows to asynchronously traverse and mutate the fields of a message - A code generator option to generate the new async traverse method - Tests that validate that mutating async traversal is working ## Result We can now implement async visitors that mutate message fields.
1 parent b3df4c3 commit 8fbe023

21 files changed

+1213
-53
lines changed

PluginExamples/Package.swift

+10
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,23 @@ let package = Package(
4747
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
4848
]
4949
),
50+
.target(
51+
name: "AsyncTraverse",
52+
dependencies: [
53+
.product(name: "SwiftProtobuf", package: "swift-protobuf")
54+
],
55+
plugins: [
56+
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
57+
]
58+
),
5059
.testTarget(
5160
name: "ExampleTests",
5261
dependencies: [
5362
.target(name: "Simple"),
5463
.target(name: "Nested"),
5564
.target(name: "Import"),
5665
.target(name: "AccessLevelOnImport"),
66+
.target(name: "AsyncTraverse"),
5767
]
5868
),
5969
],

PluginExamples/[email protected]

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ let package = Package(
1414
.target(name: "Simple"),
1515
.target(name: "Nested"),
1616
.target(name: "Import"),
17+
.target(name: "AsyncTraverse"),
1718
]
1819
),
1920
.target(
@@ -43,5 +44,14 @@ let package = Package(
4344
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
4445
]
4546
),
47+
.target(
48+
name: "AsyncTraverse",
49+
dependencies: [
50+
.product(name: "SwiftProtobuf", package: "swift-protobuf")
51+
],
52+
plugins: [
53+
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
54+
]
55+
),
4656
]
4757
)

PluginExamples/[email protected]

+10
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,23 @@ let package = Package(
4747
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
4848
]
4949
),
50+
.target(
51+
name: "AsyncTraverse",
52+
dependencies: [
53+
.product(name: "SwiftProtobuf", package: "swift-protobuf")
54+
],
55+
plugins: [
56+
.plugin(name: "SwiftProtobufPlugin", package: "swift-protobuf")
57+
]
58+
),
5059
.testTarget(
5160
name: "ExampleTests",
5261
dependencies: [
5362
.target(name: "Simple"),
5463
.target(name: "Nested"),
5564
.target(name: "Import"),
5665
.target(name: "AccessLevelOnImport"),
66+
.target(name: "AsyncTraverse"),
5767
]
5868
),
5969
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
syntax = "proto3";
2+
3+
message AsyncTraverseMessage {
4+
bytes data = 1;
5+
Nested nested = 2;
6+
repeated bytes repeated_data = 3;
7+
repeated Nested repeated_nested = 4;
8+
}
9+
10+
message Nested {
11+
bytes data = 1;
12+
NestedNested nestedNested = 2;
13+
}
14+
15+
message NestedNested {
16+
bytes data = 1;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/// DO NOT DELETE.
2+
///
3+
/// We need to keep this file otherwise the plugin is not running.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"invocations": [
3+
{
4+
"protoFiles": [
5+
"AsyncTraverse.proto",
6+
],
7+
"visibility": "public",
8+
"asyncTraverse": true,
9+
}
10+
]
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import AsyncTraverse
2+
import SwiftProtobuf
3+
import XCTest
4+
5+
final class AsyncTraverseTests: XCTestCase {
6+
func testAsyncTraverse() async throws {
7+
var message = AsyncTraverseMessage.with {
8+
$0.data = .init([UInt8(0)])
9+
$0.nested = .with {
10+
$0.data = .init([UInt8(1)])
11+
$0.nestedNested = .with {
12+
$0.data = .init([UInt8(2)])
13+
}
14+
}
15+
$0.repeatedData = [.init([UInt8(3)]), .init([UInt8(4)])]
16+
$0.repeatedNested = [
17+
.with {
18+
$0.data = .init([UInt8(5)])
19+
$0.nestedNested = .with {
20+
$0.data = .init([UInt8(6)])
21+
}
22+
}
23+
]
24+
}
25+
var visitor = MyVisitor()
26+
try await message.traverse(visitor: &visitor)
27+
XCTAssertEqual(message.data, .init([UInt8(1)]))
28+
XCTAssertEqual(message.nested.data, .init([UInt8(2)]))
29+
XCTAssertEqual(message.nested.nestedNested.data, .init([UInt8(3)]))
30+
XCTAssertEqual(message.repeatedData, [.init([UInt8(4)]), .init([UInt8(5)])])
31+
XCTAssertEqual(message.repeatedNested[0].data, .init([UInt8(6)]))
32+
XCTAssertEqual(message.repeatedNested[0].nestedNested.data, .init([UInt8(7)]))
33+
}
34+
}
35+
36+
struct MyVisitor: AsyncVisitor {
37+
mutating func visitSingularMessageField(value: inout some Message, fieldNumber: Int) async throws {
38+
try await value.traverse(visitor: &self)
39+
}
40+
41+
mutating func visitRepeatedMessageField<M>(value: inout [M], fieldNumber: Int) async throws
42+
where M: SwiftProtobuf.Message {
43+
for index in value.indices {
44+
try await value[index].traverse(visitor: &self)
45+
}
46+
}
47+
48+
mutating func visitSingularBytesField(value: inout Data, fieldNumber: Int) async throws {
49+
value = Data([value.first! + UInt8(1)])
50+
}
51+
mutating func visitRepeatedBytesField(value: inout [Data], fieldNumber: Int) async throws {
52+
value = value.map { Data([$0.first! + UInt8(1)]) }
53+
}
54+
}

Plugins/SwiftProtobufPlugin/plugin.swift

+6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ struct SwiftProtobufPlugin {
8787
var implementationOnlyImports: Bool?
8888
/// Whether import statements should be preceded with visibility.
8989
var useAccessLevelOnImports: Bool?
90+
/// Whether an async traverse method should be generated.
91+
var asyncTraverse: Bool?
9092
}
9193

9294
/// The path to the `protoc` binary.
@@ -197,6 +199,10 @@ struct SwiftProtobufPlugin {
197199
protocArgs.append("--swift_opt=UseAccessLevelOnImports=\(useAccessLevelOnImports)")
198200
}
199201

202+
if let asyncTraverse = invocation.asyncTraverse {
203+
protocArgs.append("--swift_opt=AsyncTraverse=\(asyncTraverse)")
204+
}
205+
200206
var inputFiles = [Path]()
201207
var outputFiles = [Path]()
202208

0 commit comments

Comments
 (0)