CodeGenerator.swift 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  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. import Foundation
  15. /// A protocol that generator should conform to then get easy support for
  16. /// being a protocol buffer compiler pluign.
  17. public protocol CodeGenerator {
  18. init()
  19. /// Generates code for the given proto files.
  20. ///
  21. /// - Parameters:
  22. /// - parameter: The parameter (or paramenters) passed for the generator.
  23. /// This is for parameters specific to this generator,
  24. /// `parse(parameter:)` (below) can be used to split back out
  25. /// multiple parameters into the combined for the protocol buffer
  26. /// compiler uses.
  27. /// - protoCompilerContext: Context information about the protocol buffer
  28. /// compiler being used.
  29. /// - generatorOutputs: A object that can be used to send back the
  30. /// generated outputs.
  31. ///
  32. /// - Throws: Can throw any `Error` to fail generate. `String(describing:)`
  33. /// will be called on the error to provide the error string reported
  34. /// to the user attempting to generate sources.
  35. func generate(
  36. files: [FileDescriptor],
  37. parameter: CodeGeneratorParameter,
  38. protoCompilerContext: ProtoCompilerContext,
  39. generatorOutputs: GeneratorOutputs) throws
  40. /// The list of features this CodeGenerator support to be reported back to
  41. /// the protocol buffer compiler.
  42. var supportedFeatures: [Google_Protobuf_Compiler_CodeGeneratorResponse.Feature] { get }
  43. /// If provided, the argument parsing will support `--version` and report
  44. /// this value.
  45. var version: String? { get }
  46. /// If provided and `printHelp` isn't provide, this value will be including in
  47. /// default output for the `--help` output.
  48. var projectURL: String? { get }
  49. /// If provided and `printHelp` isn't provide, this value will be including in
  50. /// default output for the `--help` output.
  51. var copyrightLine: String? { get }
  52. /// Will be called for `-h` or `--help`, should `print()` out whatever is
  53. /// desired; there is a default implementation that uses the above info
  54. /// when provided.
  55. func printHelp()
  56. }
  57. extension CodeGenerator {
  58. var programName: String {
  59. guard let name = CommandLine.arguments.first?.split(separator: "/").last else {
  60. return "<plugin>"
  61. }
  62. return String(name)
  63. }
  64. /// Runs as a protocol buffer compiler plugin based on the given arguments
  65. /// or falls back to `CommandLine.arguments`.
  66. public func main(_ args: [String]?) {
  67. let args = args ?? Array(CommandLine.arguments.dropFirst())
  68. for arg in args {
  69. if arg == "--version", let version = version {
  70. print("\(programName) \(version)")
  71. return
  72. }
  73. if arg == "-h" || arg == "--help" {
  74. printHelp()
  75. return
  76. }
  77. // Could look at bringing back the support for recorded requests, but
  78. // haven't needed it in a long time.
  79. var stderr = StandardErrorOutputStream()
  80. print("Unknown argument: \(arg)", to: &stderr)
  81. return
  82. }
  83. let response: Google_Protobuf_Compiler_CodeGeneratorResponse
  84. do {
  85. let request = try Google_Protobuf_Compiler_CodeGeneratorRequest(
  86. serializedData: FileHandle.standardInput.readDataToEndOfFile())
  87. response = generateCode(request: request, generator: self)
  88. } catch let e {
  89. response = Google_Protobuf_Compiler_CodeGeneratorResponse(
  90. error: "Received an unparsable request from the compiler: \(e)")
  91. }
  92. let serializedResponse: Data
  93. do {
  94. serializedResponse = try response.serializedData()
  95. } catch let e {
  96. var stderr = StandardErrorOutputStream()
  97. print("\(programName): Failure while serializing response: \(e)", to: &stderr)
  98. return
  99. }
  100. FileHandle.standardOutput.write(serializedResponse)
  101. }
  102. /// Runs as a protocol buffer compiler plugin; reading the generation request
  103. /// off stdin and sending the response on stdout.
  104. ///
  105. /// Instead of calling this, just add `@main` to your `CodeGenerator`.
  106. public static func main() {
  107. let generator = Self()
  108. generator.main(nil)
  109. }
  110. }
  111. // Provide default implementation for things so `CodeGenerator`s only have to
  112. // provide them if they wish too.
  113. extension CodeGenerator {
  114. public var version: String? { return nil }
  115. public var projectURL: String? { return nil }
  116. public var copyrightLine: String? { return nil }
  117. public func printHelp() {
  118. print("\(programName): A plugin for protoc and should not normally be run directly.")
  119. if let copyright = copyrightLine {
  120. print("\(copyright)")
  121. }
  122. if let projectURL = projectURL {
  123. print(
  124. """
  125. For more information on the usage of this plugin, please see:
  126. \(projectURL)
  127. """)
  128. }
  129. }
  130. }
  131. /// Uses the given `Google_Protobuf_Compiler_CodeGeneratorRequest` and
  132. /// `CodeGenerator` to get code generated and create the
  133. /// `Google_Protobuf_Compiler_CodeGeneratorResponse`. If there is a failure,
  134. /// the failure will be used in the response to be returned to the protocol
  135. /// buffer compiler to then be reported.
  136. ///
  137. /// - Parameters:
  138. /// - request: The request proto as generated by the protocol buffer compiler.
  139. /// - geneator: The `CodeGenerator` to use for generation.
  140. ///
  141. /// - Returns a filled out response with the success or failure of the
  142. /// generation.
  143. public func generateCode(
  144. request: Google_Protobuf_Compiler_CodeGeneratorRequest,
  145. generator: CodeGenerator
  146. ) -> Google_Protobuf_Compiler_CodeGeneratorResponse {
  147. // TODO: This will need update to support editions and language specific features.
  148. let descriptorSet = DescriptorSet(protos: request.protoFile)
  149. var files = [FileDescriptor]()
  150. for name in request.fileToGenerate {
  151. guard let fileDescriptor = descriptorSet.fileDescriptor(named: name) else {
  152. return Google_Protobuf_Compiler_CodeGeneratorResponse(
  153. error:
  154. "protoc asked plugin to generate a file but did not provide a descriptor for the file: \(name)"
  155. )
  156. }
  157. files.append(fileDescriptor)
  158. }
  159. let context = InternalProtoCompilerContext(request: request)
  160. let outputs = InternalGeneratorOutputs()
  161. let parameter = InternalCodeGeneratorParameter(request.parameter)
  162. do {
  163. try generator.generate(
  164. files: files, parameter: parameter, protoCompilerContext: context,
  165. generatorOutputs: outputs)
  166. } catch let e {
  167. return Google_Protobuf_Compiler_CodeGeneratorResponse(error: String(describing: e))
  168. }
  169. return Google_Protobuf_Compiler_CodeGeneratorResponse(
  170. files: outputs.files, supportedFeatures: generator.supportedFeatures)
  171. }
  172. // MARK: Internal supporting types
  173. /// Internal implementation of `CodeGeneratorParameter` for
  174. /// `generateCode(request:generator:)`
  175. struct InternalCodeGeneratorParameter: CodeGeneratorParameter {
  176. let parameter: String
  177. init(_ parameter: String) {
  178. self.parameter = parameter
  179. }
  180. var parsedPairs: [(key: String, value: String)] {
  181. guard !parameter.isEmpty else {
  182. return []
  183. }
  184. let parts = parameter.components(separatedBy: ",")
  185. return parts.map { s -> (key: String, value: String) in
  186. guard let index = s.range(of: "=")?.lowerBound else {
  187. // Key only, no value ("baz" in example).
  188. return (trimWhitespace(s), "")
  189. }
  190. return (
  191. key: trimWhitespace(s[..<index]),
  192. value: trimWhitespace(s[s.index(after: index)...])
  193. )
  194. }
  195. }
  196. }
  197. /// Internal implementation of `ProtoCompilerContext` for
  198. /// `generateCode(request:generator:)`
  199. private struct InternalProtoCompilerContext: ProtoCompilerContext {
  200. let version: Google_Protobuf_Compiler_Version?
  201. init(request: Google_Protobuf_Compiler_CodeGeneratorRequest) {
  202. self.version = request.hasCompilerVersion ? request.compilerVersion : nil
  203. }
  204. }
  205. /// Internal implementation of `GeneratorOutputs` for
  206. /// `generateCode(request:generator:)`
  207. private final class InternalGeneratorOutputs: GeneratorOutputs {
  208. enum OutputError: Error, CustomStringConvertible {
  209. /// Attempt to add two files with the same name.
  210. case duplicateName(String)
  211. var description: String {
  212. switch self {
  213. case .duplicateName(let name):
  214. return "Generator tried to generate two files named \(name)."
  215. }
  216. }
  217. }
  218. var files: [Google_Protobuf_Compiler_CodeGeneratorResponse.File] = []
  219. private var fileNames: Set<String> = []
  220. func add(fileName: String, contents: String) throws {
  221. guard !fileNames.contains(fileName) else {
  222. throw OutputError.duplicateName(fileName)
  223. }
  224. fileNames.insert(fileName)
  225. files.append(
  226. Google_Protobuf_Compiler_CodeGeneratorResponse.File(
  227. name: fileName,
  228. content: contents))
  229. }
  230. }