Skip to content

Commit

Permalink
Add support for Protobuf Editions (#284)
Browse files Browse the repository at this point in the history
This PR adds support for Protobuf Editions to Connect-Swift plugins.
There is no change to generated outputs in this PR, but it adds
`supportsEditions` and `supportedEditionRange` specifications in the
`Generator` class to ensure the Connect plugins can run with Protobuf
files that use Editions.

SwiftProtobuf also deprecated some usages of
`Google_Protobuf_Compiler_CodeGeneratorResponse`:

>
'Google_Protobuf_Compiler_CodeGeneratorResponse.init(files:supportedFeatures:)'
is deprecated: Please move your plugin to the CodeGenerator interface

As part of these changes, I migrated over to using `CodeGenerator` in
order to be able to specify the new Editions-related variables mentioned
above.

---------

Signed-off-by: Michael Rebello <[email protected]>
  • Loading branch information
rebello95 authored Jul 31, 2024
1 parent f763b73 commit 7dcc4b6
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 206 deletions.
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ let package = Package(
.target(
name: "ConnectPluginUtilities",
dependencies: [
.product(name: "SwiftProtobuf", package: "swift-protobuf"),
.product(name: "SwiftProtobufPluginLibrary", package: "swift-protobuf"),
],
path: "Plugins/ConnectPluginUtilities"
Expand Down
25 changes: 13 additions & 12 deletions Plugins/ConnectMocksPlugin/ConnectMockGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,35 @@ import ConnectPluginUtilities
import Foundation
import SwiftProtobufPluginLibrary

/// Responsible for generating services and RPCs that are compatible with the Connect library.
/// Responsible for generating mocks that are compatible with generated Connect services.
@main
final class ConnectMockGenerator: Generator {
private let propertyVisibility: String
private let typeVisibility: String
private var propertyVisibility = ""
private var typeVisibility = ""

required init(_ descriptor: FileDescriptor, options: GeneratorOptions) {
switch options.visibility {
override var outputFileExtension: String {
return ".mock.swift"
}

override func printContent(for descriptor: FileDescriptor) {
super.printContent(for: descriptor)

switch self.options.visibility {
case .internal:
self.propertyVisibility = "internal"
self.typeVisibility = "internal"
case .public:
self.propertyVisibility = "public"
self.typeVisibility = "open"
}
super.init(descriptor, options: options)
self.printContent()
}

private func printContent() {
self.printFilePreamble()

if self.options.generateCallbackMethods {
self.printModuleImports(adding: ["Combine", "ConnectMocks"])
} else {
self.printModuleImports(adding: ["ConnectMocks"])
}

for service in self.descriptor.services {
for service in self.services {
self.printLine()
self.printMockService(service)
}
Expand Down
19 changes: 0 additions & 19 deletions Plugins/ConnectMocksPlugin/main.swift

This file was deleted.

127 changes: 100 additions & 27 deletions Plugins/ConnectPluginUtilities/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,53 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import SwiftProtobuf
import SwiftProtobufPluginLibrary

/// Base generator class that can be used to output a file from a Protobuf file descriptor.
private struct GeneratorError: Swift.Error {
let message: String
}

/// Base generator class that can be used to generate Swift files from Protobuf file descriptors.
/// Not intended to be instantiated directly.
/// Subclasses must be annotated with `@main` to be properly invoked at runtime.
open class Generator {
private var printer = CodePrinter(indent: " ".unicodeScalars)
private var neededModules = [String]()
private var printer = SwiftProtobufPluginLibrary.CodePrinter()

public let descriptor: FileDescriptor
public let namer: SwiftProtobufNamer
public let options: GeneratorOptions
// MARK: - Overridable

public var output: String {
return self.printer.content
/// File extension to use for generated file names (e.g., ".connect.swift").
/// Must be overridden by subclasses.
open var outputFileExtension: String {
fatalError("\(#function) must be overridden by subclasses")
}

public required init(_ descriptor: FileDescriptor, options: GeneratorOptions) {
self.descriptor = descriptor
self.options = options
self.namer = SwiftProtobufNamer(
currentFile: descriptor,
protoFileToModuleMappings: options.protoToModuleMappings
)
/// Initializer required by `SwiftProtobufPluginLibrary.CodeGenerator`.
public required init() {}

/// Should be overridden by subclasses to write output for a given file descriptor.
/// Subclasses should call `super` before producing their own outputs.
/// May be called multiple times (once per file descriptor) over the lifetime of this class.
///
/// - parameter descriptor: The file descriptor for which to generate code.
open func printContent(for descriptor: SwiftProtobufPluginLibrary.FileDescriptor) {
self.printLine("// Code generated by protoc-gen-connect-swift. DO NOT EDIT.")
self.printLine("//")
self.printLine("// Source: \(descriptor.name)")
self.printLine("//")
self.printLine()
}

// MARK: - Output helpers

/// Used for producing type names when generating code.
public private(set) var namer = SwiftProtobufPluginLibrary.SwiftProtobufNamer()
/// Options to use when generating code.
public private(set) var options = GeneratorOptions.empty()
/// List of services specified in the current file.
public private(set) var services = [SwiftProtobufPluginLibrary.ServiceDescriptor]()

public func indent() {
self.printer.indent()
}
Expand All @@ -58,30 +80,81 @@ open class Generator {
self.printer.print("\n")
}

public func printCommentsIfNeeded(for entity: ProvidesSourceCodeLocation) {
public func printCommentsIfNeeded(
for entity: SwiftProtobufPluginLibrary.ProvidesSourceCodeLocation
) {
let comments = entity.protoSourceComments().trimmingCharacters(in: .whitespacesAndNewlines)
if !comments.isEmpty {
self.printLine(comments)
}
}

public func printFilePreamble() {
self.printLine("// Code generated by protoc-gen-connect-swift. DO NOT EDIT.")
self.printLine("//")
self.printLine("// Source: \(self.descriptor.name)")
self.printLine("//")
self.printLine()
}

public func printModuleImports(adding additional: [String] = []) {
let defaults = ["Connect", "Foundation", self.options.swiftProtobufModuleName]
let extraOptionImports = self.options.extraModuleImports
let mappings = self.options.protoToModuleMappings
.neededModules(forFile: self.descriptor) ?? []
let allImports = (defaults + mappings + extraOptionImports + additional).sorted()

let allImports = (defaults + self.neededModules + extraOptionImports + additional).sorted()
for module in allImports {
self.printLine("import \(module)")
}
}
}

extension Generator: SwiftProtobufPluginLibrary.CodeGenerator {
private func resetAndPrintFile(
for descriptor: SwiftProtobufPluginLibrary.FileDescriptor
) -> String {
self.namer = SwiftProtobufPluginLibrary.SwiftProtobufNamer(
currentFile: descriptor,
protoFileToModuleMappings: self.options.protoToModuleMappings
)
self.neededModules = self.options.protoToModuleMappings
.neededModules(forFile: descriptor) ?? []
self.services = descriptor.services
self.printer = SwiftProtobufPluginLibrary.CodePrinter(indent: " ".unicodeScalars)
self.printContent(for: descriptor)
return self.printer.content
}

public func generate(
files: [SwiftProtobufPluginLibrary.FileDescriptor],
parameter: any SwiftProtobufPluginLibrary.CodeGeneratorParameter,
protoCompilerContext _: any SwiftProtobufPluginLibrary.ProtoCompilerContext,
generatorOutputs: any SwiftProtobufPluginLibrary.GeneratorOutputs
) throws {
self.options = try GeneratorOptions(commandLineParameters: parameter)
guard self.options.generateAsyncMethods || self.options.generateCallbackMethods else {
throw GeneratorError(
message: "Either async methods or callback methods must be enabled"
)
}

for descriptor in files where !descriptor.services.isEmpty {
try generatorOutputs.add(
fileName: FilePathComponents(path: descriptor.name).outputFilePath(
withExtension: self.outputFileExtension,
using: self.options.fileNaming
),
contents: self.resetAndPrintFile(for: descriptor)
)
}
}

public var supportedEditionRange: ClosedRange<SwiftProtobuf.Google_Protobuf_Edition> {
let minEdition = max(
DescriptorSet.bundledEditionsSupport.lowerBound, Google_Protobuf_Edition.legacy
)
let maxEdition = min(
DescriptorSet.bundledEditionsSupport.upperBound, Google_Protobuf_Edition.edition2023
)
return minEdition...maxEdition
}

public var supportedFeatures: [
SwiftProtobufPluginLibrary.Google_Protobuf_Compiler_CodeGeneratorResponse.Feature
] {
return [
.proto3Optional,
.supportsEditions,
]
}
}
61 changes: 17 additions & 44 deletions Plugins/ConnectPluginUtilities/GeneratorOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,33 +42,6 @@ private enum CommandLineParameter: String {
}
}
}

static func parse(commandLineParameters: String) throws -> [(key: Self, value: String)] {
return try commandLineParameters
.components(separatedBy: ",")
.compactMap { parameter in
if parameter.isEmpty {
return nil
}

guard let index = parameter.firstIndex(of: "=") else {
throw Error.unknownParameter(string: parameter)
}

let rawKey = parameter[..<index].trimmingCharacters(in: .whitespacesAndNewlines)
guard let key = Self(rawValue: rawKey) else {
throw Error.unknownParameter(string: parameter)
}

let value = parameter[parameter.index(after: index)...]
.trimmingCharacters(in: .whitespacesAndNewlines)
if value.isEmpty {
return nil
}

return (key, value)
}
}
}

/// A set of options that are used to customize generator outputs.
Expand All @@ -94,18 +67,23 @@ public struct GeneratorOptions {
case `public` = "Public"
}

/// Initializes a set of generator options from the raw string representation of command line
static func empty() -> Self {
return .init()
}
}

extension GeneratorOptions {
/// Initializes a set of generator options from command line
/// parameters (e.g., "Visibility=Internal,KeepMethodCasing=true").
///
/// Handles trimming whitespace, and some parameters may be specified multiple times.
///
/// - parameter commandLineParameters: The raw CLI parameters.
public init(commandLineParameters: String) throws {
let parsedParameters = try CommandLineParameter.parse(
commandLineParameters: commandLineParameters
)
for (key, rawValue) in parsedParameters {
switch key {
/// - parameter commandLineParameters: The CLI parameters.
public init(commandLineParameters: SwiftProtobufPluginLibrary.CodeGeneratorParameter) throws {
for (key, rawValue) in commandLineParameters.parsedPairs {
guard let parsedParameter = CommandLineParameter(rawValue: key) else {
throw CommandLineParameter.Error.unknownParameter(string: key)
}

switch parsedParameter {
case .extraModuleImports:
self.extraModuleImports.append(rawValue)
continue
Expand Down Expand Up @@ -145,9 +123,7 @@ public struct GeneratorOptions {
self.protoToModuleMappings = try ProtoFileToModuleMappings(path: rawValue)
continue
} catch let error {
throw CommandLineParameter.Error.deserializationError(
key: key.rawValue, error: error
)
throw CommandLineParameter.Error.deserializationError(key: key, error: error)
}

case .swiftProtobufModuleName:
Expand All @@ -161,10 +137,7 @@ public struct GeneratorOptions {
}
}

throw CommandLineParameter.Error.invalidParameterValue(
key: key.rawValue,
value: rawValue
)
throw CommandLineParameter.Error.invalidParameterValue(key: key, value: rawValue)
}
}
}
Loading

0 comments on commit 7dcc4b6

Please sign in to comment.