Skip to content

Commit 2b0f7fc

Browse files
committed
Step 1 of better plugin support.
Add the concept of a `CodeGenerator` and some building blocks to go with it so some of the boilerplate around writing plugins is provided. This is the start of the building blocks to make supporting Editions easier for any plugin (grpc) when that support lands as it will make a lot of the setup details hidden rather than having to be implemented by each plugin.
1 parent 599d3d3 commit 2b0f7fc

File tree

5 files changed

+272
-0
lines changed

5 files changed

+272
-0
lines changed
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Sources/SwiftProtobufPluginLibrary/CodeGenerator.swift
2+
//
3+
// Copyright (c) 2014 - 2023 Apple Inc. and the project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See LICENSE.txt for license information:
7+
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8+
//
9+
// -----------------------------------------------------------------------------
10+
///
11+
/// This provides the basic interface for writing a CodeGenerator.
12+
///
13+
// -----------------------------------------------------------------------------
14+
15+
import Foundation
16+
17+
/// A protocol that generator should conform to then get easy support for
18+
/// being a protocol buffer compiler pluign.
19+
public protocol CodeGenerator {
20+
/// Generates code for the given proto files.
21+
///
22+
/// - Parameters:
23+
/// - parameter: The parameter (or paramenters) passed for the generator.
24+
/// This is for parameters specific to this generator,
25+
/// `parse(parameter:)` (below) can be used to split back out
26+
/// multiple parameters into the combined for the protocol buffer
27+
/// compiler uses.
28+
/// - protoCompilerContext: Context information about the protocol buffer
29+
/// compiler being used.
30+
/// - generatorOutputs: A object that can be used to send back the
31+
/// generated outputs.
32+
///
33+
/// - Throws: Can throw any `Error` to fail generate. `String(describing:)`
34+
/// will be called on the error to provide the error string reported
35+
/// to the user attempting to generate sources.
36+
func generate(
37+
files: [FileDescriptor],
38+
parameter: CodeGeneratorParameter,
39+
protoCompilerContext: ProtoCompilerContext,
40+
generatorOutputs: GeneratorOutputs) throws
41+
42+
/// The list of features this CodeGenerator support to be reported back to
43+
/// the protocol buffer compiler.
44+
var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { get }
45+
}
46+
47+
/// Uses the given `Google_Protobuf_Compiler_CodeGeneratorRequest` and
48+
/// `CodeGenerator` to get code generated and create the
49+
/// `Google_Protobuf_Compiler_CodeGeneratorResponse`. If there is a failure,
50+
/// the failure will be used in the response to be returned to the protocol
51+
/// buffer compiler to then be reported.
52+
///
53+
/// - Parameters:
54+
/// - request: The request proto as generated by the protocol buffer compiler.
55+
/// - geneator: The `CodeGenerator` to use for generation.
56+
///
57+
/// - Returns a filled out response with the success or failure of the
58+
/// generation.
59+
public func generateCode(
60+
request: Google_Protobuf_Compiler_CodeGeneratorRequest,
61+
generator: CodeGenerator
62+
) -> Google_Protobuf_Compiler_CodeGeneratorResponse {
63+
// TODO: This will need update to support editions and language specific features.
64+
65+
let descriptorSet = DescriptorSet(protos: request.protoFile)
66+
67+
var files = [FileDescriptor]()
68+
for name in request.fileToGenerate {
69+
guard let fileDescriptor = descriptorSet.fileDescriptor(named: name) else {
70+
return Google_Protobuf_Compiler_CodeGeneratorResponse(
71+
error:
72+
"protoc asked plugin to generate a file but did not provide a descriptor for the file: \(name)"
73+
)
74+
}
75+
files.append(fileDescriptor)
76+
}
77+
78+
let context = InternalProtoCompilerContext(request: request)
79+
let outputs = InternalGeneratorOutputs()
80+
let parameter = InternalCodeGeneratorParameter(request.parameter)
81+
82+
do {
83+
try generator.generate(
84+
files: files, parameter: parameter, protoCompilerContext: context,
85+
generatorOutputs: outputs)
86+
} catch let e {
87+
return Google_Protobuf_Compiler_CodeGeneratorResponse(error: String(describing: e))
88+
}
89+
90+
return Google_Protobuf_Compiler_CodeGeneratorResponse(
91+
files: outputs.files, supportedFeatures: generator.supportedFeatures)
92+
}
93+
94+
// MARK: Internal supporting types
95+
96+
/// Internal implementation of `CodeGeneratorParameter` for
97+
/// `generateCode(request:generator:)`
98+
struct InternalCodeGeneratorParameter: CodeGeneratorParameter {
99+
let parameter: String
100+
101+
init(_ parameter: String) {
102+
self.parameter = parameter
103+
}
104+
105+
var parsedPairs: [(key: String, value: String)] {
106+
guard !parameter.isEmpty else {
107+
return []
108+
}
109+
let parts = parameter.components(separatedBy: ",")
110+
let asPairs = parts.map { partition(string: $0, atFirstOccurrenceOf: "=") }
111+
let result = asPairs.map { (key: trimWhitespace($0), value: trimWhitespace($1)) }
112+
return result
113+
}
114+
}
115+
116+
/// Internal implementation of `ProtoCompilerContext` for
117+
/// `generateCode(request:generator:)`
118+
private struct InternalProtoCompilerContext: ProtoCompilerContext {
119+
let version: Google_Protobuf_Compiler_Version?
120+
121+
init(request: Google_Protobuf_Compiler_CodeGeneratorRequest) {
122+
self.version = request.hasCompilerVersion ? request.compilerVersion : nil
123+
}
124+
}
125+
126+
/// Internal implementation of `GeneratorOutputs` for
127+
/// `generateCode(request:generator:)`
128+
private final class InternalGeneratorOutputs: GeneratorOutputs {
129+
130+
enum OutputError: Error, CustomStringConvertible {
131+
/// Attempt to add two files with the same name.
132+
case duplicateName(String)
133+
134+
var description: String {
135+
switch self {
136+
case .duplicateName(let name):
137+
return "Generator tried to generate two files named \(name)."
138+
}
139+
}
140+
}
141+
142+
var files: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = []
143+
144+
func add(fileName: String, contents: String) throws {
145+
guard !files.contains(where: { $0.name == fileName }) else {
146+
throw OutputError.duplicateName(fileName)
147+
}
148+
files.append(
149+
Google_Protobuf_Compiler_CodeGeneratorResponse.File(
150+
name: fileName,
151+
content: contents))
152+
}
153+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Sources/SwiftProtobufPluginLibrary/CodeGeneratorParameter.swift
2+
//
3+
// Copyright (c) 2014 - 2023 Apple Inc. and the project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See LICENSE.txt for license information:
7+
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8+
//
9+
// -----------------------------------------------------------------------------
10+
///
11+
/// This provides the basic interface for a CodeGeneratorParameter. This is
12+
/// passed to the `CodeGenerator` to get any command line options.
13+
///
14+
// -----------------------------------------------------------------------------
15+
16+
import Foundation
17+
18+
/// The the generator specific parameter that was passed to the protocol
19+
/// compiler invocation. The protocol buffer compiler supports providing
20+
/// parameters via the `--[LANG]_out` or `--[LANG]_opt` command line flags.
21+
/// The compiler will relay those through as a _parameter_ string.
22+
public protocol CodeGeneratorParameter {
23+
/// The raw value from the compiler as a single string, if multiple values
24+
/// were passed, they are joined into a single string. See `parsedPairs` as
25+
/// that is likely a better option for consuming the parameters.
26+
var parameter: String { get }
27+
28+
/// The protocol buffer compiler will combine multiple `--[LANG]_opt`
29+
/// directives into a "single" parameter by joining them with commas. This
30+
/// vends the parameter split back back out into the individual arguments:
31+
/// i.e.,
32+
/// "foo=bar,baz,mumble=blah"
33+
/// becomes:
34+
/// [
35+
/// (key: "foo", value: "bar"),
36+
/// (key: "baz", value: ""),
37+
/// (key: "mumble", value: "blah")
38+
/// ]
39+
var parsedPairs: [(key: String, value: String)] { get }
40+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Sources/SwiftProtobufPluginLibrary/GeneratorOutputs.swift
2+
//
3+
// Copyright (c) 2014 - 2023 Apple Inc. and the project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See LICENSE.txt for license information:
7+
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8+
//
9+
// -----------------------------------------------------------------------------
10+
///
11+
/// This provides the basic interface for providing the generation outputs.
12+
///
13+
// -----------------------------------------------------------------------------
14+
15+
import Foundation
16+
17+
/// Abstract interface for receiving generation outputs.
18+
public protocol GeneratorOutputs {
19+
/// Add the a file with the given `name` and `contents` to the outputs.
20+
///
21+
/// - Parameters:
22+
/// - fileName: The name of the file.
23+
/// - contents: The body of the file.
24+
///
25+
/// - Throws May throw errors for duplicate file names or any other problem.
26+
/// Generally `CodeGenerator`s do *not* need to catch these, and instead
27+
/// they are ripple all the way out to the code calling the
28+
/// `CodeGenerator`.
29+
func add(fileName: String, contents: String) throws
30+
31+
// TODO: Consider adding apis to stream things like C++ protobuf does?
32+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Sources/SwiftProtobufPluginLibrary/ProtoCompilerContext.swift
2+
//
3+
// Copyright (c) 2014 - 2023 Apple Inc. and the project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See LICENSE.txt for license information:
7+
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8+
//
9+
// -----------------------------------------------------------------------------
10+
///
11+
/// This provides some basic interface about the protocol buffer compiler
12+
/// being used to generate.
13+
///
14+
// -----------------------------------------------------------------------------
15+
16+
import Foundation
17+
18+
/// Abstact interface to get information about the protocol buffer compiler
19+
/// being used for generation.
20+
public protocol ProtoCompilerContext {
21+
/// The version of the protocol buffer compiler (if it was provided in the
22+
/// generation request).
23+
var version: Google_Protobuf_Compiler_Version? { get }
24+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Sources/SwiftProtobufPluginLibrary/StringUtils.swift - String processing utilities
2+
//
3+
// Copyright (c) 2014 - 2016 Apple Inc. and the project authors
4+
// Licensed under Apache License v2.0 with Runtime Library Exception
5+
//
6+
// See LICENSE.txt for license information:
7+
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
8+
//
9+
// -----------------------------------------------------------------------------
10+
11+
import Foundation
12+
13+
func partition(string: String, atFirstOccurrenceOf substring: String) -> (String, String) {
14+
guard let index = string.range(of: substring)?.lowerBound else {
15+
return (string, "")
16+
}
17+
return (String(string[..<index]),
18+
String(string[string.index(after: index)...]))
19+
}
20+
21+
func trimWhitespace(_ s: String) -> String {
22+
return s.trimmingCharacters(in: .whitespacesAndNewlines)
23+
}

0 commit comments

Comments
 (0)