Skip to content

Commit f5d026c

Browse files
committed
Allow commnad plugin to take a dir as input
Motivation: The command plugin requires proto files to be passed as separate inputs. While expanding these with a wildcard is straightforward, doing so usually requires the user to also set an import path. Modifications: - Allow directories to be used as input to the command plugin. Doing so also uses the directory as an import path. - Improve the help text for the command plugin and provide example calls Result: Easier to use the command plugin. In many cases users will only need to specify the directory containing their protos and the output directory.
1 parent 2f5ef06 commit f5d026c

File tree

3 files changed

+108
-66
lines changed

3 files changed

+108
-66
lines changed

Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift

Lines changed: 57 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -185,67 +185,68 @@ enum OptionsAndFlags: String, CaseIterable {
185185
}
186186

187187
extension OptionsAndFlags {
188-
func usageDescription() -> String {
189-
switch self {
190-
case .servers:
191-
return "Generate server code. Generated by default."
192-
case .noServers:
193-
return "Do not generate server code. Generated by default."
194-
case .clients:
195-
return "Generate client code. Generated by default."
196-
case .noClients:
197-
return "Do not generate client code. Generated by default."
198-
case .messages:
199-
return "Generate message code. Generated by default."
200-
case .noMessages:
201-
return "Do not generate message code. Generated by default."
202-
case .fileNaming:
203-
return
204-
"The naming scheme for output files [fullPath/pathToUnderscores/dropPath]. Defaults to fullPath."
205-
case .accessLevel:
206-
return
207-
"The access level of the generated source [internal/public/package]. Defaults to internal."
208-
case .accessLevelOnImports:
209-
return "Whether imports should have explicit access levels. Defaults to false."
210-
case .importPath:
211-
return
212-
"The directory in which to search for imports. May be specified multiple times. If none are specified the current working directory is used."
213-
case .protocPath:
214-
return "The path to the protoc binary."
215-
case .dryRun:
216-
return "Print but do not execute the protoc commands."
217-
case .outputPath:
218-
return "The directory into which the generated source files are created."
219-
case .verbose:
220-
return "Emit verbose output."
221-
case .help:
222-
return "Print this help."
223-
}
188+
static var helpText: String {
189+
"""
190+
USAGE: swift package generate-grpc-code-from-protos [[<options> [--]] <inputs> ...
191+
192+
ARGUMENTS:
193+
<inputs> The '.proto' files or directories containing them.
194+
195+
OPTIONS:
196+
--servers/--no-servers Generate server code (default: --servers)
197+
--clients/--no-clients Generate client code (default: --clients)
198+
--messages/--no-messages Generate message code (default: --messages)
199+
--access-level <access> Access level of generated code (internal/public/package)
200+
(default: internal)
201+
--access-level-on-imports Whether imports have explicit access levels
202+
--protoc-path <path> Path to the protoc binary
203+
--import-path <path> Directory to search for imports, may be specified
204+
multiple times. If none are specified the current
205+
working directory is used.
206+
--file-naming <naming> The naming scheme for generated files
207+
(fullPath/pathToUnderscores/dropPath)
208+
(default: fullPath).
209+
--output-path <path> Directory to generate files into
210+
--verbose Emit verbose output
211+
--dry-run Print but don't execute the protoc commands
212+
--help Print this help
213+
214+
EXAMPLES:
215+
216+
swift package generate-grpc-code-from-protos service.proto
217+
Generates servers, clients, and messages from 'service.proto' into
218+
the current working directory.
219+
220+
swift package generate-grpc-code-from-protos --no-clients --no-messages -- service1.proto service2.proto
221+
Generate only servers from service1.proto and service2.proto into the
222+
current working directory.
223+
224+
swift package generate-grpc-code-from-protos --output-path Generated --access-level public -- Protos
225+
Generate server, clients, and messages from all .proto files contained
226+
within the 'Protos' directory into the 'Generated' directory at the
227+
public access level.
228+
229+
swift package --allow-writing-to-package-directory generate-grpc-code-from-protos --output-path Sources/Generated -- service.proto
230+
Generates code from service.proto into the Sources/Generated directory
231+
within a Swift Package without asking for permission to do so.
232+
233+
PERMISSIONS:
234+
Swift Package Manager command plugins require permission to create files.
235+
You'll be prompted to give generate-grpc-code-from-protos permission
236+
when running it.
237+
238+
You can grant permissions by specifying --allow-writing-to-package-directory
239+
or --allow-writing-to-directory to the swift package command.
240+
241+
See swift package plugin --help for more info.
242+
"""
224243
}
225244

226245
static func printHelp(requested: Bool) {
227-
let printMessage: (String) -> Void
228246
if requested {
229-
printMessage = { message in print(message) }
247+
print(Self.helpText)
230248
} else {
231-
printMessage = Stderr.print
232-
}
233-
234-
printMessage(
235-
"Usage: swift package generate-grpc-code-from-protos [flags] [\(CommandConfig.parameterGroupSeparator)] [input files]"
236-
)
237-
printMessage("")
238-
printMessage("Flags:")
239-
printMessage("")
240-
241-
let spacing = 3
242-
let maxLength =
243-
(OptionsAndFlags.allCases.map(\.rawValue).max(by: { $0.count < $1.count })?.count ?? 0)
244-
+ spacing
245-
for flag in OptionsAndFlags.allCases {
246-
printMessage(
247-
" --\(flag.rawValue.padding(toLength: maxLength, withPad: " ", startingAt: 0))\(flag.usageDescription())"
248-
)
249+
Stderr.print(Self.helpText)
249250
}
250251
}
251252
}

Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,15 @@ extension CommandPluginError: CustomStringConvertible {
4747

4848
case .invalidInputFiles(let files):
4949
var lines: [String] = []
50-
lines.append("Invalid input file(s)")
50+
lines.append("Found \(files.count) invalid input file(s)")
5151
lines.append("")
52-
lines.append("Found \(files.count) input(s) not ending in '.proto':")
52+
lines.append("The following don't exist or aren't '.proto' files or directories:")
5353
for file in files {
5454
lines.append("- \(file)")
5555
}
5656
lines.append("")
5757
lines.append("All options must be before '--', and all input files must be")
58-
lines.append("after '--'. Input files must end in '.proto'.")
58+
lines.append("after '--'. Inputs be '.proto' files or directories.")
5959
lines.append("")
6060
lines.append("See --help for more information.")
6161
return lines.joined(separator: "\n")

Plugins/GRPCProtobufGeneratorCommand/Plugin.swift

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,10 @@ struct GRPCProtobufGeneratorCommandPlugin {
5757
}
5858

5959
// Split the input into options and input files.
60-
let (flagsAndOptions, inputFiles) = self.splitArgs(arguments)
60+
let (flagsAndOptions, inputs) = self.splitArgs(arguments)
6161

6262
// Check the input files all look like protos
63-
let nonProtoInputs = inputFiles.filter { !$0.hasSuffix(".proto") }
64-
if !nonProtoInputs.isEmpty {
65-
throw CommandPluginError.invalidInputFiles(nonProtoInputs)
66-
}
63+
let (inputFiles, extraImportPaths) = try self.checkAndExpandInputs(inputs)
6764

6865
if inputFiles.isEmpty {
6966
throw CommandPluginError.missingInputFile
@@ -101,7 +98,7 @@ struct GRPCProtobufGeneratorCommandPlugin {
10198
config: config,
10299
fileNaming: config.fileNaming,
103100
inputFiles: inputFileURLs,
104-
protoDirectoryPaths: config.importPaths,
101+
protoDirectoryPaths: config.importPaths + extraImportPaths,
105102
protocGenGRPCSwiftPath: protocGenGRPCSwiftPath,
106103
outputDirectory: outputDirectory
107104
)
@@ -124,7 +121,7 @@ struct GRPCProtobufGeneratorCommandPlugin {
124121
config: config,
125122
fileNaming: config.fileNaming,
126123
inputFiles: inputFileURLs,
127-
protoDirectoryPaths: config.importPaths,
124+
protoDirectoryPaths: config.importPaths + extraImportPaths,
128125
protocGenSwiftPath: protocGenSwiftPath,
129126
outputDirectory: outputDirectory
130127
)
@@ -159,6 +156,50 @@ struct GRPCProtobufGeneratorCommandPlugin {
159156

160157
return (options, inputs)
161158
}
159+
160+
private func checkAndExpandInputs(
161+
_ files: [String]
162+
) throws -> (protos: [String], directories: [String]) {
163+
let fileManager = FileManager.default
164+
var invalidInputFiles = [String]()
165+
var protos = [String]()
166+
var dirs = [String]()
167+
168+
for file in files {
169+
// Check that each file:
170+
// a) exists, and
171+
// b) is either a '.proto' file or a directory.
172+
//
173+
// If the file is a directory then all '.proto' files within the directory
174+
// are added as input files.
175+
var isDirectory = ObjCBool(false)
176+
let exists = fileManager.fileExists(atPath: file, isDirectory: &isDirectory)
177+
178+
if !exists {
179+
invalidInputFiles.append(file)
180+
} else if isDirectory.boolValue {
181+
dirs.append(file)
182+
// Do a deep traversal of the directory.
183+
if var enumerator = fileManager.enumerator(atPath: file) {
184+
while let path = enumerator.nextObject() as? String {
185+
if path.hasSuffix(".proto") {
186+
protos.append(path)
187+
}
188+
}
189+
}
190+
} else if file.hasSuffix(".proto") {
191+
protos.append(file)
192+
} else {
193+
invalidInputFiles.append(file)
194+
}
195+
}
196+
197+
if !invalidInputFiles.isEmpty {
198+
throw CommandPluginError.invalidInputFiles(invalidInputFiles)
199+
}
200+
201+
return (protos, dirs)
202+
}
162203
}
163204

164205
/// Execute a single invocation of `protoc`, printing output and if in verbose mode the invocation

0 commit comments

Comments
 (0)