ProtobufCodeGenParser.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. internal import SwiftProtobuf
  17. package import SwiftProtobufPluginLibrary
  18. package import struct GRPCCodeGen.CodeGenerationRequest
  19. package import struct GRPCCodeGen.CodeGenerator
  20. package import struct GRPCCodeGen.Dependency
  21. package import struct GRPCCodeGen.MethodDescriptor
  22. package import struct GRPCCodeGen.MethodName
  23. package import struct GRPCCodeGen.ServiceDescriptor
  24. package import struct GRPCCodeGen.ServiceName
  25. #if canImport(FoundationEssentials)
  26. internal import struct FoundationEssentials.IndexPath
  27. #else
  28. internal import struct Foundation.IndexPath
  29. #endif
  30. /// Parses a ``FileDescriptor`` object into a ``CodeGenerationRequest`` object.
  31. package struct ProtobufCodeGenParser {
  32. let extraModuleImports: [String]
  33. let protoToModuleMappings: ProtoFileToModuleMappings
  34. let accessLevel: CodeGenerator.Config.AccessLevel
  35. package init(
  36. protoFileModuleMappings: ProtoFileToModuleMappings,
  37. extraModuleImports: [String],
  38. accessLevel: CodeGenerator.Config.AccessLevel
  39. ) {
  40. self.extraModuleImports = extraModuleImports
  41. self.protoToModuleMappings = protoFileModuleMappings
  42. self.accessLevel = accessLevel
  43. }
  44. package func parse(descriptor: FileDescriptor) throws -> CodeGenerationRequest {
  45. let namer = SwiftProtobufNamer(
  46. currentFile: descriptor,
  47. protoFileToModuleMappings: self.protoToModuleMappings
  48. )
  49. var header = descriptor.header
  50. // Ensuring there is a blank line after the header.
  51. if !header.isEmpty && !header.hasSuffix("\n\n") {
  52. header.append("\n")
  53. }
  54. let leadingTrivia = """
  55. // DO NOT EDIT.
  56. // swift-format-ignore-file
  57. //
  58. // Generated by the gRPC Swift generator plugin for the protocol buffer compiler.
  59. // Source: \(descriptor.name)
  60. //
  61. // For information on using the generated types, please see the documentation:
  62. // https://github.com/grpc/grpc-swift
  63. """
  64. let services = descriptor.services.map {
  65. GRPCCodeGen.ServiceDescriptor(
  66. descriptor: $0,
  67. package: descriptor.package,
  68. protobufNamer: namer,
  69. file: descriptor
  70. )
  71. }
  72. return CodeGenerationRequest(
  73. fileName: descriptor.name,
  74. leadingTrivia: header + leadingTrivia,
  75. dependencies: self.codeDependencies(file: descriptor),
  76. services: services,
  77. makeSerializerCodeSnippet: { messageType in
  78. "GRPCProtobuf.ProtobufSerializer<\(messageType)>()"
  79. },
  80. makeDeserializerCodeSnippet: { messageType in
  81. "GRPCProtobuf.ProtobufDeserializer<\(messageType)>()"
  82. }
  83. )
  84. }
  85. }
  86. extension ProtobufCodeGenParser {
  87. fileprivate func codeDependencies(file: FileDescriptor) -> [Dependency] {
  88. guard file.services.count > 0 else {
  89. return []
  90. }
  91. var codeDependencies: [Dependency] = [
  92. Dependency(module: "GRPCProtobuf", accessLevel: .internal)
  93. ]
  94. // If there's a dependency on a bundled proto then add the SwiftProtobuf import.
  95. //
  96. // Importing SwiftProtobuf unconditionally results in warnings in the generated
  97. // code if access-levels are used on imports and no bundled protos are used.
  98. let dependsOnBundledProto = file.dependencies.contains { descriptor in
  99. SwiftProtobufInfo.isBundledProto(file: descriptor)
  100. }
  101. if dependsOnBundledProto {
  102. codeDependencies.append(Dependency(module: "SwiftProtobuf", accessLevel: self.accessLevel))
  103. }
  104. // Adding as dependencies the modules containing generated code or types for
  105. // '.proto' files imported in the '.proto' file we are parsing.
  106. codeDependencies.append(
  107. contentsOf: (self.protoToModuleMappings.neededModules(forFile: file) ?? []).map {
  108. Dependency(module: $0, accessLevel: self.accessLevel)
  109. }
  110. )
  111. // Adding extra imports passed in as an option to the plugin.
  112. codeDependencies.append(
  113. contentsOf: self.extraModuleImports.sorted().map {
  114. Dependency(module: $0, accessLevel: self.accessLevel)
  115. }
  116. )
  117. return codeDependencies
  118. }
  119. }
  120. extension GRPCCodeGen.ServiceDescriptor {
  121. fileprivate init(
  122. descriptor: SwiftProtobufPluginLibrary.ServiceDescriptor,
  123. package: String,
  124. protobufNamer: SwiftProtobufNamer,
  125. file: FileDescriptor
  126. ) {
  127. let methods = descriptor.methods.map {
  128. GRPCCodeGen.MethodDescriptor(
  129. descriptor: $0,
  130. protobufNamer: protobufNamer
  131. )
  132. }
  133. let typePrefix = protobufNamer.typePrefix(forFile: file)
  134. let name = ServiceName(
  135. identifyingName: descriptor.fullName,
  136. // The service name from the '.proto' file is expected to be in upper camel case
  137. typeName: typePrefix + descriptor.name,
  138. propertyName: protobufNamer.typePrefixProperty(file: file) + descriptor.name
  139. )
  140. let documentation = descriptor.protoSourceComments()
  141. self.init(documentation: documentation, name: name, methods: methods)
  142. }
  143. }
  144. extension GRPCCodeGen.MethodDescriptor {
  145. fileprivate init(
  146. descriptor: SwiftProtobufPluginLibrary.MethodDescriptor,
  147. protobufNamer: SwiftProtobufNamer
  148. ) {
  149. let name = MethodName(
  150. identifyingName: descriptor.name,
  151. // The method name from the '.proto' file is expected to be in upper camel case
  152. typeName: descriptor.name,
  153. functionName: CamelCaser.toLowerCamelCase(descriptor.name)
  154. )
  155. let documentation = descriptor.protoSourceComments()
  156. self.init(
  157. documentation: documentation,
  158. name: name,
  159. isInputStreaming: descriptor.clientStreaming,
  160. isOutputStreaming: descriptor.serverStreaming,
  161. inputType: protobufNamer.fullName(message: descriptor.inputType),
  162. outputType: protobufNamer.fullName(message: descriptor.outputType)
  163. )
  164. }
  165. }
  166. extension FileDescriptor {
  167. fileprivate var header: String {
  168. var header = String()
  169. // Field number used to collect the syntax field which is usually the first
  170. // declaration in a.proto file.
  171. // See more here:
  172. // https://github.com/apple/swift-protobuf/blob/main/Protos/SwiftProtobuf/google/protobuf/descriptor.proto
  173. let syntaxPath = IndexPath(index: 12)
  174. if let syntaxLocation = self.sourceCodeInfoLocation(path: syntaxPath) {
  175. header = syntaxLocation.asSourceComment(
  176. commentPrefix: "///",
  177. leadingDetachedPrefix: "//"
  178. )
  179. }
  180. return header
  181. }
  182. }
  183. extension SwiftProtobufNamer {
  184. internal func typePrefixProperty(file: FileDescriptor) -> String {
  185. let typePrefix = self.typePrefix(forFile: file)
  186. let lowercased = typePrefix.split(separator: "_").map { component in
  187. NamingUtils.toLowerCamelCase(String(component))
  188. }
  189. let joined = lowercased.joined(separator: "_")
  190. if typePrefix.hasSuffix("_"), !joined.hasSuffix("_") {
  191. // Add the trailing "_" if it was dropped.
  192. return joined + "_"
  193. } else {
  194. return joined
  195. }
  196. }
  197. }
  198. extension String {
  199. internal func trimTrailingUnderscores() -> String {
  200. if let index = self.lastIndex(where: { $0 != "_" }) {
  201. return String(self[...index])
  202. } else {
  203. return ""
  204. }
  205. }
  206. }