CommandConfig.swift 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. import Foundation
  17. import PackagePlugin
  18. struct CommandConfig {
  19. var common: GenerationConfig
  20. var verbose: Bool
  21. var dryRun: Bool
  22. static let defaults = Self(
  23. common: .init(
  24. accessLevel: .internal,
  25. servers: true,
  26. clients: true,
  27. messages: true,
  28. fileNaming: .fullPath,
  29. accessLevelOnImports: false,
  30. importPaths: [],
  31. outputPath: ""
  32. ),
  33. verbose: false,
  34. dryRun: false
  35. )
  36. static let parameterGroupSeparator = "--"
  37. }
  38. extension CommandConfig {
  39. static func parse(
  40. argumentExtractor argExtractor: inout ArgumentExtractor,
  41. pluginWorkDirectory: URL
  42. ) throws -> CommandConfig {
  43. var config = CommandConfig.defaults
  44. for flag in OptionsAndFlags.allCases {
  45. switch flag {
  46. case .accessLevel:
  47. if let value = argExtractor.extractSingleOption(named: flag.rawValue) {
  48. if let accessLevel = GenerationConfig.AccessLevel(rawValue: value) {
  49. config.common.accessLevel = accessLevel
  50. } else {
  51. throw CommandPluginError.unknownAccessLevel(value)
  52. }
  53. }
  54. case .noServers:
  55. // Handled by `.servers`
  56. continue
  57. case .servers:
  58. let servers = argExtractor.extractFlag(named: OptionsAndFlags.servers.rawValue)
  59. let noServers = argExtractor.extractFlag(named: OptionsAndFlags.noServers.rawValue)
  60. if servers > 0 && noServers > 0 {
  61. throw CommandPluginError.conflictingFlags(
  62. OptionsAndFlags.servers.rawValue,
  63. OptionsAndFlags.noServers.rawValue
  64. )
  65. } else if servers > 0 {
  66. config.common.servers = true
  67. } else if noServers > 0 {
  68. config.common.servers = false
  69. }
  70. case .noClients:
  71. // Handled by `.clients`
  72. continue
  73. case .clients:
  74. let clients = argExtractor.extractFlag(named: OptionsAndFlags.clients.rawValue)
  75. let noClients = argExtractor.extractFlag(named: OptionsAndFlags.noClients.rawValue)
  76. if clients > 0 && noClients > 0 {
  77. throw CommandPluginError.conflictingFlags(
  78. OptionsAndFlags.clients.rawValue,
  79. OptionsAndFlags.noClients.rawValue
  80. )
  81. } else if clients > 0 {
  82. config.common.clients = true
  83. } else if noClients > 0 {
  84. config.common.clients = false
  85. }
  86. case .noMessages:
  87. // Handled by `.messages`
  88. continue
  89. case .messages:
  90. let messages = argExtractor.extractFlag(named: OptionsAndFlags.messages.rawValue)
  91. let noMessages = argExtractor.extractFlag(named: OptionsAndFlags.noMessages.rawValue)
  92. if messages > 0 && noMessages > 0 {
  93. throw CommandPluginError.conflictingFlags(
  94. OptionsAndFlags.messages.rawValue,
  95. OptionsAndFlags.noMessages.rawValue
  96. )
  97. } else if messages > 0 {
  98. config.common.messages = true
  99. } else if noMessages > 0 {
  100. config.common.messages = false
  101. }
  102. case .fileNaming:
  103. if let value = argExtractor.extractSingleOption(named: flag.rawValue) {
  104. if let fileNaming = GenerationConfig.FileNaming(rawValue: value) {
  105. config.common.fileNaming = fileNaming
  106. } else {
  107. throw CommandPluginError.unknownFileNamingStrategy(value)
  108. }
  109. }
  110. case .accessLevelOnImports:
  111. if argExtractor.extractFlag(named: flag.rawValue) > 0 {
  112. config.common.accessLevelOnImports = true
  113. }
  114. case .importPath:
  115. config.common.importPaths = argExtractor.extractOption(named: flag.rawValue)
  116. case .protocPath:
  117. config.common.protocPath = argExtractor.extractSingleOption(named: flag.rawValue)
  118. case .outputPath:
  119. config.common.outputPath =
  120. argExtractor.extractSingleOption(named: flag.rawValue)
  121. ?? pluginWorkDirectory.absoluteStringNoScheme
  122. case .verbose:
  123. let verbose = argExtractor.extractFlag(named: flag.rawValue)
  124. config.verbose = verbose != 0
  125. case .dryRun:
  126. let dryRun = argExtractor.extractFlag(named: flag.rawValue)
  127. config.dryRun = dryRun != 0
  128. case .help:
  129. () // handled elsewhere
  130. }
  131. }
  132. if let argument = argExtractor.remainingArguments.first {
  133. throw CommandPluginError.unknownOption(argument)
  134. }
  135. return config
  136. }
  137. }
  138. extension ArgumentExtractor {
  139. mutating func extractSingleOption(named optionName: String) -> String? {
  140. let values = self.extractOption(named: optionName)
  141. if values.count > 1 {
  142. Diagnostics.warning(
  143. "'--\(optionName)' was unexpectedly repeated, the first value will be used."
  144. )
  145. }
  146. return values.first
  147. }
  148. }
  149. /// All valid input options/flags
  150. enum OptionsAndFlags: String, CaseIterable {
  151. case servers
  152. case noServers = "no-servers"
  153. case clients
  154. case noClients = "no-clients"
  155. case messages
  156. case noMessages = "no-messages"
  157. case fileNaming = "file-naming"
  158. case accessLevel = "access-level"
  159. case accessLevelOnImports = "access-level-on-imports"
  160. case importPath = "import-path"
  161. case protocPath = "protoc-path"
  162. case outputPath = "output-path"
  163. case verbose
  164. case dryRun = "dry-run"
  165. case help
  166. }
  167. extension OptionsAndFlags {
  168. func usageDescription() -> String {
  169. switch self {
  170. case .servers:
  171. return "Generate server code. Generated by default."
  172. case .noServers:
  173. return "Do not generate server code. Generated by default."
  174. case .clients:
  175. return "Generate client code. Generated by default."
  176. case .noClients:
  177. return "Do not generate client code. Generated by default."
  178. case .messages:
  179. return "Generate message code. Generated by default."
  180. case .noMessages:
  181. return "Do not generate message code. Generated by default."
  182. case .fileNaming:
  183. return
  184. "The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath."
  185. case .accessLevel:
  186. return
  187. "The access level of the generated source [internal/public/package]. Defaults to internal."
  188. case .accessLevelOnImports:
  189. return "Whether imports should have explicit access levels. Defaults to false."
  190. case .importPath:
  191. return
  192. "The directory in which to search for imports. May be specified multiple times. If none are specified the current working directory is used."
  193. case .protocPath:
  194. return "The path to the protoc binary."
  195. case .dryRun:
  196. return "Print but do not execute the protoc commands."
  197. case .outputPath:
  198. return "The directory into which the generated source files are created."
  199. case .verbose:
  200. return "Emit verbose output."
  201. case .help:
  202. return "Print this help."
  203. }
  204. }
  205. static func printHelp(requested: Bool) {
  206. let printMessage: (String) -> Void
  207. if requested {
  208. printMessage = { message in print(message) }
  209. } else {
  210. printMessage = Stderr.print
  211. }
  212. printMessage(
  213. "Usage: swift package generate-grpc-code-from-protos [flags] [\(CommandConfig.parameterGroupSeparator)] [input files]"
  214. )
  215. printMessage("")
  216. printMessage("Flags:")
  217. printMessage("")
  218. let spacing = 3
  219. let maxLength =
  220. (OptionsAndFlags.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0)
  221. + spacing
  222. for flag in OptionsAndFlags.allCases {
  223. printMessage(
  224. " --\(flag.rawValue.padding(toLength: maxLength, withPad: " ", startingAt: 0))\(flag.usageDescription())"
  225. )
  226. }
  227. }
  228. }