Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Protobuf Editions #284

Merged
merged 5 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading