Skip to content

Add dry run option to CLI #123

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

Merged
merged 9 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
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 @@ -48,10 +48,17 @@ struct _GenerateCommand: AsyncParsableCommand {
)
var isPluginInvocation: Bool = false

@Flag(
help:
"Simulate the command and print the operations, without actually affecting the file system."
)
var isDryRun: Bool = false

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
60 changes: 45 additions & 15 deletions Sources/swift-openapi-generator/runGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import ArgumentParser
import _OpenAPIGeneratorCore

extension _Tool {

/// Runs the generator with the specified configuration values.
/// - Parameters:
/// - doc: A path to the OpenAPI document.
Expand All @@ -25,12 +24,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 @@ -48,14 +50,15 @@ extension _Tool {
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,45 +70,72 @@ 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 result = 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 result.didCreate && isDryRun {
print("File \(outputFilePath.lastPathComponent) does not exist.\nCreating new file...")
}
if result.didChange {
print("File \(outputFilePath.lastPathComponent) will be overwritten.")
} else {
print("File \(outputFilePath.lastPathComponent) will remain unchanged.")
}
print("File \(outputFilePath.lastPathComponent): \(didChange ? "changed" : "unchanged")")
}

/// Evaluates a closure to generate file data and writes the data to disk
/// if the data is different than the current file contents.
/// if the data is different than the current file contents. Will write to disk
/// only if `isDryRun` is set as `false`.
/// - 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`.
/// - Returns: A tuple of two booleans didChange and didCreate.
/// The former will be `true` if the generated contents changed, otherwise `false` and
/// the latter will be `true` if the file did not exist before, 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 -> (didChange: Bool, didCreate: Bool) {
let data = try contents()
let didChange: Bool
var didCreate = false
if FileManager.default.fileExists(atPath: path.path) {
let existingData = try? Data(contentsOf: path)
didChange = existingData != data
} else {
didChange = true
didCreate = true
}
if didChange {
try data.write(to: path)
if !isDryRun {
try data.write(to: path)
}
}
return didChange
return (didChange, didCreate)
}
}