Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add dry run option to CLI #123

Merged
merged 9 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion Sources/swift-openapi-generator/GenerateCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,18 @@ struct _GenerateCommand: AsyncParsableCommand {
"Whether this invocation is from the SwiftPM plugin. We always need to produce all files when invoked from the plugin. Non-requested modes produce empty files."
)
var isPluginInvocation: Bool = false

@Flag(
help:
"Simulate the command and print the operations, without actually affecting the file system."
)
var isDryRun: Bool = false
denil-ct marked this conversation as resolved.
Show resolved Hide resolved

func run() async throws {
try generate.runGenerator(
outputDirectory: outputDirectory,
isPluginInvocation: isPluginInvocation
isPluginInvocation: isPluginInvocation,
isDryRun: isDryRun
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,13 @@ extension _GenerateOptions {
/// Server.swift) regardless of which generator mode was requested, with
/// the caveat that the not requested files are empty. This is due to
/// a limitation of the build system used by SwiftPM under the hood.
/// - isDryRun: A Boolean value that indicates whether this invocation should
/// be run in a testing mode to preview all the operations being carried out without
/// making any actual changes.
func runGenerator(
outputDirectory: URL,
isPluginInvocation: Bool
isPluginInvocation: Bool,
isDryRun: Bool
) throws {
let config = try loadedConfig()
let sortedModes = try resolvedModes(config)
Expand Down Expand Up @@ -63,6 +67,7 @@ extension _GenerateOptions {
- Diagnostics output path: \(diagnosticsOutputPath?.path ?? "<none - logs to stderr>")
- Current directory: \(FileManager.default.currentDirectoryPath)
- Is plugin invocation: \(isPluginInvocation)
- Is dry run: \(isDryRun)
- Additional imports: \(resolvedAdditionalImports.isEmpty ? "<none>" : resolvedAdditionalImports.joined(separator: ", "))
"""
)
Expand All @@ -72,6 +77,7 @@ extension _GenerateOptions {
configs: configs,
isPluginInvocation: isPluginInvocation,
outputDirectory: outputDirectory,
isDryRun: isDryRun,
diagnostics: diagnostics
)
} catch let error as Diagnostic {
Expand Down
79 changes: 68 additions & 11 deletions Sources/swift-openapi-generator/runGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,30 @@ import ArgumentParser
import _OpenAPIGeneratorCore

extension _Tool {

/// A structure that defines ANSI escape codes for different colors in the terminal output.
///
/// You can use these codes to change the color of the text printed by the `print` function or other methods that write to the standard output. To reset the text color to the default, use the `reset` code.
///
/// For example, to print "Hello" in red and "World" in green, you can use:
/// ```swift
/// print(
/// CommandLineColors.red +
/// "Hello" +
/// CommandLineColors.green +
/// "World" +
/// CommandLineColors.reset
/// )
/// ```
struct CommandLineColors {
denil-ct marked this conversation as resolved.
Show resolved Hide resolved
/// The ANSI escape code for resetting the text color and style to the default.
static let reset = "\u{001B}[0;0m"
/// The ANSI escape code for changing the text color to red.
static let red = "\u{001B}[0;31m"
/// The ANSI escape code for changing the text color to green.
static let green = "\u{001B}[0;32m"
}


/// Runs the generator with the specified configuration values.
/// - Parameters:
Expand All @@ -25,12 +49,15 @@ extension _Tool {
/// generator invocation is coming from a SwiftPM plugin.
/// - outputDirectory: The directory to which the generator writes
/// the generated Swift files.
/// - isDryRun: A Boolean value that indicates whether this invocation should
/// be a dry run.
/// - diagnostics: A collector for diagnostics emitted by the generator.
static func runGenerator(
doc: URL,
configs: [Config],
isPluginInvocation: Bool,
outputDirectory: URL,
isDryRun: Bool,
diagnostics: any DiagnosticCollector
) throws {
let docData: Data
Expand All @@ -42,20 +69,25 @@ extension _Tool {
let filePathForMode: (GeneratorMode) -> URL = { mode in
outputDirectory.appendingPathComponent(mode.outputFileName)
}
if isDryRun {
print("--------------------------------")
print("Dry run mode: No files will be created or modified")
}
denil-ct marked this conversation as resolved.
Show resolved Hide resolved
for config in configs {
try runGenerator(
doc: doc,
docData: docData,
config: config,
outputFilePath: filePathForMode(config.mode),
isDryRun: isDryRun,
diagnostics: diagnostics
)
}
if isPluginInvocation {
let nonGeneratedModes = Set(GeneratorMode.allCases).subtracting(configs.map(\.mode))
for mode in nonGeneratedModes.sorted() {
let path = filePathForMode(mode)
try replaceFileContents(at: path, with: { Data() })
try replaceFileContents(at: path, with: { Data() }, isDryRun: isDryRun)
}
}
}
Expand All @@ -67,44 +99,69 @@ extension _Tool {
/// - config: A set of configuration values for the generator.
/// - outputFilePath: The directory to which the generator writes
/// the generated Swift files.
/// - isDryRun: A Boolean value that indicates whether this invocation should
/// be a dry run.
/// - diagnostics: A collector for diagnostics emitted by the generator.
static func runGenerator(
doc: URL,
docData: Data,
config: Config,
outputFilePath: URL,
isDryRun: Bool,
diagnostics: any DiagnosticCollector
) throws {
let didChange = try replaceFileContents(at: outputFilePath) {
let output = try _OpenAPIGeneratorCore.runGenerator(
input: .init(absolutePath: doc, contents: docData),
config: config,
diagnostics: diagnostics
)
return output.contents
let didChange = try replaceFileContents(
at: outputFilePath,
with: {
let output = try _OpenAPIGeneratorCore.runGenerator(
input: .init(absolutePath: doc, contents: docData),
config: config,
diagnostics: diagnostics
)
return output.contents
},
isDryRun: isDryRun
)
if !isDryRun {
print("File \(outputFilePath.lastPathComponent): \(didChange ? "changed" : "unchanged")")
}
print("File \(outputFilePath.lastPathComponent): \(didChange ? "changed" : "unchanged")")
denil-ct marked this conversation as resolved.
Show resolved Hide resolved
}

/// Evaluates a closure to generate file data and writes the data to disk
/// if the data is different than the current file contents.
/// - Parameters:
/// - path: A path to the file.
/// - contents: A closure evaluated to produce the file contents data.
/// - isDryRun: A Boolean value that indicates whether this invocation should
/// be a dry run. File system changes will not be written to disk in this mode.
/// - Throws: When writing to disk fails.
/// - Returns: `true` if the generated contents changed, otherwise `false`.
@discardableResult
static func replaceFileContents(at path: URL, with contents: () throws -> Data) throws -> Bool {
static func replaceFileContents(
at path: URL,
with contents: () throws -> Data,
isDryRun: Bool
) throws -> Bool {
let data = try contents()
let didChange: Bool
if FileManager.default.fileExists(atPath: path.path) {
let existingData = try? Data(contentsOf: path)
didChange = existingData != data
if didChange {
print(CommandLineColors.red + "File \(path.lastPathComponent) will be overwritten." + CommandLineColors.reset)
} else {
print(CommandLineColors.green + "File \(path.lastPathComponent) will remain unchanged." + CommandLineColors.reset)
}
} else {
print(CommandLineColors.green + "File \(path.lastPathComponent) does not exist.\nCreating new file..." + CommandLineColors.reset)
didChange = true
}
if didChange {
try data.write(to: path)
if isDryRun {
print(CommandLineColors.green + "Writing data to \(path.lastPathComponent)..." + CommandLineColors.reset)
} else {
try data.write(to: path)
}
denil-ct marked this conversation as resolved.
Show resolved Hide resolved
}
return didChange
}
denil-ct marked this conversation as resolved.
Show resolved Hide resolved
Expand Down