Skip to content

Commit 72f81e5

Browse files
authored
Use SwiftProtobuf's new CodeGenerator interface (#2043)
Motivation: SwiftProtobuf 1.27.0 added a new `CodeGenerator` interface in 1.27.0 and deprecated the old API. This didn't include (non-deprecated) access to the source proto which is required for reflection data, however, this was added in 1.28.0. Modification: - Rename `options.swift` to `Options.swift` - Rewrite `main` as `GenerateGRPC`, the functionality is unchanged but did require a bit of code shuffling. As part of this some global methods became private methods on the new `GenerateGRPC` `struct`. - Add support for protobuf editions. Result: - Fewer warnings - Can use protobuf editions
1 parent cc2c32d commit 72f81e5

File tree

3 files changed

+249
-234
lines changed

3 files changed

+249
-234
lines changed
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Copyright 2024, 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 Foundation
18+
import SwiftProtobuf
19+
import SwiftProtobufPluginLibrary
20+
21+
#if compiler(>=6.0)
22+
import GRPCCodeGen
23+
import GRPCProtobufCodeGen
24+
#endif
25+
26+
@main
27+
final class GenerateGRPC: CodeGenerator {
28+
var version: String? {
29+
Version.versionString
30+
}
31+
32+
var projectURL: String {
33+
"https://github.com/grpc/grpc-swift"
34+
}
35+
36+
var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] {
37+
[.proto3Optional, .supportsEditions]
38+
}
39+
40+
var supportedEditionRange: ClosedRange<Google_Protobuf_Edition> {
41+
Google_Protobuf_Edition.proto2 ... Google_Protobuf_Edition.edition2023
42+
}
43+
44+
// A count of generated files by desired name (actual name may differ to avoid collisions).
45+
private var generatedFileNames: [String: Int] = [:]
46+
47+
func generate(
48+
files fileDescriptors: [FileDescriptor],
49+
parameter: any CodeGeneratorParameter,
50+
protoCompilerContext: any ProtoCompilerContext,
51+
generatorOutputs outputs: any GeneratorOutputs
52+
) throws {
53+
let options = try GeneratorOptions(parameter: parameter)
54+
55+
for descriptor in fileDescriptors {
56+
if options.generateReflectionData {
57+
try self.generateReflectionData(
58+
descriptor,
59+
options: options,
60+
outputs: outputs
61+
)
62+
}
63+
64+
if descriptor.services.isEmpty {
65+
continue
66+
}
67+
68+
if options.generateClient || options.generateServer || options.generateTestClient {
69+
#if compiler(>=6.0)
70+
if options.v2 {
71+
try self.generateV2Stubs(descriptor, options: options, outputs: outputs)
72+
} else {
73+
try self.generateV1Stubs(descriptor, options: options, outputs: outputs)
74+
}
75+
#else
76+
try self.generateV1Stubs(descriptor, options: options, outputs: outputs)
77+
#endif
78+
}
79+
}
80+
}
81+
82+
private func generateReflectionData(
83+
_ descriptor: FileDescriptor,
84+
options: GeneratorOptions,
85+
outputs: any GeneratorOutputs
86+
) throws {
87+
let fileName = self.uniqueOutputFileName(
88+
fileDescriptor: descriptor,
89+
fileNamingOption: options.fileNaming,
90+
extension: "reflection"
91+
)
92+
93+
var options = ExtractProtoOptions()
94+
options.includeSourceCodeInfo = true
95+
let proto = descriptor.extractProto(options: options)
96+
let serializedProto = try proto.serializedData()
97+
let reflectionData = serializedProto.base64EncodedString()
98+
try outputs.add(fileName: fileName, contents: reflectionData)
99+
}
100+
101+
private func generateV1Stubs(
102+
_ descriptor: FileDescriptor,
103+
options: GeneratorOptions,
104+
outputs: any GeneratorOutputs
105+
) throws {
106+
let fileName = self.uniqueOutputFileName(
107+
fileDescriptor: descriptor,
108+
fileNamingOption: options.fileNaming
109+
)
110+
111+
let fileGenerator = Generator(descriptor, options: options)
112+
try outputs.add(fileName: fileName, contents: fileGenerator.code)
113+
}
114+
115+
#if compiler(>=6.0)
116+
private func generateV2Stubs(
117+
_ descriptor: FileDescriptor,
118+
options: GeneratorOptions,
119+
outputs: any GeneratorOutputs
120+
) throws {
121+
let fileName = self.uniqueOutputFileName(
122+
fileDescriptor: descriptor,
123+
fileNamingOption: options.fileNaming
124+
)
125+
126+
let config = SourceGenerator.Configuration(options: options)
127+
let fileGenerator = ProtobufCodeGenerator(configuration: config)
128+
let contents = try fileGenerator.generateCode(
129+
from: descriptor,
130+
protoFileModuleMappings: options.protoToModuleMappings,
131+
extraModuleImports: options.extraModuleImports
132+
)
133+
134+
try outputs.add(fileName: fileName, contents: contents)
135+
}
136+
#endif
137+
}
138+
139+
extension GenerateGRPC {
140+
private func uniqueOutputFileName(
141+
fileDescriptor: FileDescriptor,
142+
fileNamingOption: FileNaming,
143+
component: String = "grpc",
144+
extension: String = "swift"
145+
) -> String {
146+
let defaultName = outputFileName(
147+
component: component,
148+
fileDescriptor: fileDescriptor,
149+
fileNamingOption: fileNamingOption,
150+
extension: `extension`
151+
)
152+
if let count = self.generatedFileNames[defaultName] {
153+
self.generatedFileNames[defaultName] = count + 1
154+
return outputFileName(
155+
component: "\(count)." + component,
156+
fileDescriptor: fileDescriptor,
157+
fileNamingOption: fileNamingOption,
158+
extension: `extension`
159+
)
160+
} else {
161+
self.generatedFileNames[defaultName] = 1
162+
return defaultName
163+
}
164+
}
165+
166+
private func outputFileName(
167+
component: String,
168+
fileDescriptor: FileDescriptor,
169+
fileNamingOption: FileNaming,
170+
extension: String
171+
) -> String {
172+
let ext = "." + component + "." + `extension`
173+
let pathParts = splitPath(pathname: fileDescriptor.name)
174+
switch fileNamingOption {
175+
case .fullPath:
176+
return pathParts.dir + pathParts.base + ext
177+
case .pathToUnderscores:
178+
let dirWithUnderscores =
179+
pathParts.dir.replacingOccurrences(of: "/", with: "_")
180+
return dirWithUnderscores + pathParts.base + ext
181+
case .dropPath:
182+
return pathParts.base + ext
183+
}
184+
}
185+
}
186+
187+
// from apple/swift-protobuf/Sources/protoc-gen-swift/StringUtils.swift
188+
private func splitPath(pathname: String) -> (dir: String, base: String, suffix: String) {
189+
var dir = ""
190+
var base = ""
191+
var suffix = ""
192+
193+
for character in pathname {
194+
if character == "/" {
195+
dir += base + suffix + String(character)
196+
base = ""
197+
suffix = ""
198+
} else if character == "." {
199+
base += suffix
200+
suffix = String(character)
201+
} else {
202+
suffix += String(character)
203+
}
204+
}
205+
206+
let validSuffix = suffix.isEmpty || suffix.first == "."
207+
if !validSuffix {
208+
base += suffix
209+
suffix = ""
210+
}
211+
return (dir: dir, base: base, suffix: suffix)
212+
}
213+
214+
#if compiler(>=6.0)
215+
extension SourceGenerator.Configuration {
216+
init(options: GeneratorOptions) {
217+
let accessLevel: SourceGenerator.Configuration.AccessLevel
218+
switch options.visibility {
219+
case .internal:
220+
accessLevel = .internal
221+
case .package:
222+
accessLevel = .package
223+
case .public:
224+
accessLevel = .public
225+
}
226+
227+
self.init(
228+
accessLevel: accessLevel,
229+
accessLevelOnImports: options.useAccessLevelOnImports,
230+
client: options.generateClient,
231+
server: options.generateServer
232+
)
233+
}
234+
}
235+
#endif

Sources/protoc-gen-grpc-swift/options.swift renamed to Sources/protoc-gen-grpc-swift/Options.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,13 @@ enum GenerationError: Error {
3636
}
3737
}
3838

39-
final class GeneratorOptions {
39+
enum FileNaming: String {
40+
case fullPath = "FullPath"
41+
case pathToUnderscores = "PathToUnderscores"
42+
case dropPath = "DropPath"
43+
}
44+
45+
struct GeneratorOptions {
4046
enum Visibility: String {
4147
case `internal` = "Internal"
4248
case `public` = "Public"
@@ -63,7 +69,7 @@ final class GeneratorOptions {
6369

6470
private(set) var keepMethodCasing = false
6571
private(set) var protoToModuleMappings = ProtoFileToModuleMappings()
66-
private(set) var fileNaming = FileNaming.FullPath
72+
private(set) var fileNaming = FileNaming.fullPath
6773
private(set) var extraModuleImports: [String] = []
6874
private(set) var gRPCModuleName = "GRPC"
6975
private(set) var swiftProtobufModuleName = "SwiftProtobuf"
@@ -73,8 +79,12 @@ final class GeneratorOptions {
7379
#endif
7480
private(set) var useAccessLevelOnImports = true
7581

76-
init(parameter: String?) throws {
77-
for pair in GeneratorOptions.parseParameter(string: parameter) {
82+
init(parameter: any CodeGeneratorParameter) throws {
83+
try self.init(pairs: parameter.parsedPairs)
84+
}
85+
86+
init(pairs: [(key: String, value: String)]) throws {
87+
for pair in pairs {
7888
switch pair.key {
7989
case "Visibility":
8090
if let value = Visibility(rawValue: pair.value) {

0 commit comments

Comments
 (0)