From 3f56dda5689a4fa01ae56f987418393f809f8c4e Mon Sep 17 00:00:00 2001 From: Michael Housh Date: Fri, 13 Dec 2024 15:33:20 -0500 Subject: [PATCH] feat: Adds generate commands that call to pandoc to generate pdf, latex, and html files from a project. --- Package.swift | 8 +- .../CliClient/CliClient+PandocCommands.swift | 223 ++++++++++++++++++ Sources/CliClient/CliClientError.swift | 5 + Sources/CliClient/Interface.swift | 46 ++++ .../ConfigurationClient/Configuration.swift | 39 +++ .../ConfigurationClient/Resources/hpa.toml | 35 ++- Sources/PlaybookClient/PlaybookClient.swift | 14 +- Sources/TestSupport/TestSupport.swift | 4 - Sources/TestSupport/WithTempDir.swift | 15 ++ Sources/hpa/Application.swift | 8 +- Sources/hpa/BuildCommand.swift | 34 ++- .../GenerateCommands/GenerateCommand.swift | 14 ++ .../GenerateHtmlCommand.swift | 24 ++ .../GenerateLatexCommand.swift | 23 ++ .../GenerateCommands/GenerateOptions.swift | 87 +++++++ .../GenerateCommands/GeneratePdfCommand.swift | 32 +++ Sources/hpa/Internal/GlobalOptions.swift | 4 +- .../Internal/PlaybookOptions+Globals.swift | 2 +- Sources/hpa/Version.swift | 2 + .../ConfigurationClientTests.swift | 17 -- .../PlaybookClientTests.swift | 20 +- justfile | 7 + 22 files changed, 606 insertions(+), 57 deletions(-) create mode 100644 Sources/CliClient/CliClient+PandocCommands.swift create mode 100644 Sources/TestSupport/WithTempDir.swift create mode 100644 Sources/hpa/GenerateCommands/GenerateCommand.swift create mode 100644 Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift create mode 100644 Sources/hpa/GenerateCommands/GenerateLatexCommand.swift create mode 100644 Sources/hpa/GenerateCommands/GenerateOptions.swift create mode 100644 Sources/hpa/GenerateCommands/GeneratePdfCommand.swift create mode 100644 Sources/hpa/Version.swift diff --git a/Package.swift b/Package.swift index 3f77e2e..747acd2 100644 --- a/Package.swift +++ b/Package.swift @@ -32,9 +32,6 @@ let package = Package( .product(name: "CliDoc", package: "swift-cli-doc"), .product(name: "Dependencies", package: "swift-dependencies"), .product(name: "ShellClient", package: "swift-shell-client") - ], - plugins: [ - .plugin(name: "BuildWithVersionPlugin", package: "swift-cli-version") ] ), .target( @@ -109,7 +106,10 @@ let package = Package( ), .testTarget( name: "PlaybookClientTests", - dependencies: ["PlaybookClient"] + dependencies: [ + "PlaybookClient", + "TestSupport" + ] ) ] ) diff --git a/Sources/CliClient/CliClient+PandocCommands.swift b/Sources/CliClient/CliClient+PandocCommands.swift new file mode 100644 index 0000000..1d2a249 --- /dev/null +++ b/Sources/CliClient/CliClient+PandocCommands.swift @@ -0,0 +1,223 @@ +import ConfigurationClient +import Dependencies +import Foundation +import ShellClient + +// TODO: Need to parse / ensure that header includes and files are in the build directory, not sure +// exactly how to handle if they're not, but it seems reasonable to potentially allow files that are +// outside of the build directory to be included. + +public extension CliClient { + + @discardableResult + func runPandocCommand( + _ options: PandocOptions, + logging loggingOptions: LoggingOptions + ) async throws -> String { + try await withLogger(loggingOptions) { + @Dependency(\.configurationClient) var configurationClient + @Dependency(\.logger) var logger + + let configuration = try await configurationClient.findAndLoad() + logger.trace("Configuration: \(configuration)") + + let ensuredOptions = try await ensurePandocOptions( + configuration: configuration, + options: options + ) + + let projectDirectory = options.projectDirectory ?? ProcessInfo.processInfo.environment["PWD"] + guard let projectDirectory else { + throw CliClientError.generate(.projectDirectoryNotSpecified) + } + + let outputDirectory = options.outputDirectory ?? projectDirectory + let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)" + + var arguments = [ + "pandoc" + ] + arguments += ensuredOptions.includeInHeader.map { + "--include-in-header=\(projectDirectory)/\(ensuredOptions.buildDirectory)/\($0)" + } + + if let pdfEngine = ensuredOptions.pdfEngine { + arguments.append("--pdf-engine=\(pdfEngine)") + } + + arguments.append("--output=\(outputPath)") + arguments += ensuredOptions.files.map { + "\(projectDirectory)/\(ensuredOptions.buildDirectory)/\($0)" + } + + if options.shouldBuild { + logger.trace("Building project...") + try await runPlaybookCommand( + .init( + arguments: [ + "--tags", "build-project", + "--extra-vars", "project_dir=\(projectDirectory)" + ], + configuration: configuration, + quiet: options.quiet, + shell: options.shell + ), + logging: loggingOptions + ) + } + + logger.trace("Running pandoc with arguments: \(arguments)") + + try await runCommand( + quiet: options.quiet, + shell: options.shell.orDefault, + arguments + ) + + return outputPath + } + } +} + +@_spi(Internal) +public struct EnsuredPandocOptions: Equatable, Sendable { + public let buildDirectory: String + public let files: [String] + public let includeInHeader: [String] + public let outputFileName: String + public let outputFileType: CliClient.PandocOptions.FileType + public let pdfEngine: String? + + public var ensuredExtensionFileName: String { + let extensionString: String + + switch outputFileType { + case .html: + extensionString = ".html" + case .latex: + extensionString = ".tex" + case .pdf: + extensionString = ".pdf" + } + + if !outputFileName.hasSuffix(extensionString) { + return outputFileName + extensionString + } + return outputFileName + } +} + +@_spi(Internal) +public func ensurePandocOptions( + configuration: Configuration, + options: CliClient.PandocOptions +) async throws -> EnsuredPandocOptions { + let defaults = Configuration.Generate.default + let pdfEngine = parsePdfEngine(configuration.generate, defaults, options) + + return .init( + buildDirectory: parseBuildDirectory(configuration.generate, defaults, options), + files: parseFiles(configuration.generate, defaults, options), + includeInHeader: parseIncludeInHeader(configuration.generate, defaults, options), + outputFileName: parseOutputFileName(configuration.generate, defaults, options), + outputFileType: options.outputFileType, + pdfEngine: pdfEngine + ) +} + +private func parsePdfEngine( + _ configuration: Configuration.Generate?, + _ defaults: Configuration.Generate, + _ options: CliClient.PandocOptions +) -> String? { + switch options.outputFileType { + case .html, .latex: + return nil + case let .pdf(engine: engine): + if let engine { + return engine + } else if let engine = configuration?.pdfEngine { + return engine + } else if let engine = defaults.pdfEngine { + return engine + } else { + return "xelatex" + } + } +} + +private func parseFiles( + _ configuration: Configuration.Generate?, + _ defaults: Configuration.Generate, + _ options: CliClient.PandocOptions +) -> [String] { + @Dependency(\.logger) var logger + + if let files = options.files { + return files + } else if let files = configuration?.files { + return files + } else if let files = defaults.files { + return files + } else { + logger.warning("Files not specified, this could lead to errors.") + return [] + } +} + +private func parseIncludeInHeader( + _ configuration: Configuration.Generate?, + _ defaults: Configuration.Generate, + _ options: CliClient.PandocOptions +) -> [String] { + @Dependency(\.logger) var logger + + if let files = options.includeInHeader { + return files + } else if let files = configuration?.includeInHeader { + return files + } else if let files = defaults.includeInHeader { + return files + } else { + logger.warning("Include in header files not specified, this could lead to errors.") + return [] + } +} + +private func parseOutputFileName( + _ configuration: Configuration.Generate?, + _ defaults: Configuration.Generate, + _ options: CliClient.PandocOptions +) -> String { + @Dependency(\.logger) var logger + + if let output = options.outputFileName { + return output + } else if let output = configuration?.outputFileName { + return output + } else if let output = defaults.outputFileName { + return output + } else { + logger.warning("Output file name not specified, this could lead to errors.") + return "Report" + } +} + +private func parseBuildDirectory( + _ configuration: Configuration.Generate?, + _ defaults: Configuration.Generate, + _ options: CliClient.PandocOptions +) -> String { + @Dependency(\.logger) var logger + + if let output = options.buildDirectory { + return output + } else if let output = configuration?.buildDirectory { + return output + } else if let output = defaults.buildDirectory { + return output + } else { + logger.warning("Output file name not specified, this could lead to errors.") + return ".build" + } +} diff --git a/Sources/CliClient/CliClientError.swift b/Sources/CliClient/CliClientError.swift index 68b606d..d61759f 100644 --- a/Sources/CliClient/CliClientError.swift +++ b/Sources/CliClient/CliClientError.swift @@ -4,7 +4,12 @@ public enum CliClientError: Error { case brewfileNotFound case encodingError case playbookDirectoryNotFound + case generate(GenerateError) case templateDirectoryNotFound case templateDirectoryOrRepoNotSpecified case vaultFileNotFound + + public enum GenerateError: Sendable { + case projectDirectoryNotSpecified + } } diff --git a/Sources/CliClient/Interface.swift b/Sources/CliClient/Interface.swift index ec3f228..49b54ba 100644 --- a/Sources/CliClient/Interface.swift +++ b/Sources/CliClient/Interface.swift @@ -27,6 +27,52 @@ public struct CliClient: Sendable { public extension CliClient { + struct PandocOptions: Equatable, Sendable { + + let buildDirectory: String? + let files: [String]? + let includeInHeader: [String]? + let outputDirectory: String? + let outputFileName: String? + let outputFileType: FileType + let projectDirectory: String? + let quiet: Bool + let shell: String? + let shouldBuild: Bool + + public init( + buildDirectory: String? = nil, + files: [String]? = nil, + includeInHeader: [String]? = nil, + outputDirectory: String? = nil, + outputFileName: String? = nil, + outputFileType: FileType, + projectDirectory: String?, + quiet: Bool, + shell: String? = nil, + shouldBuild: Bool + ) { + self.buildDirectory = buildDirectory + self.files = files + self.includeInHeader = includeInHeader + self.outputDirectory = outputDirectory + self.outputFileName = outputFileName + self.outputFileType = outputFileType + self.projectDirectory = projectDirectory + self.quiet = quiet + self.shell = shell + self.shouldBuild = shouldBuild + } + + // swiftlint:disable nesting + public enum FileType: Equatable, Sendable { + case html + case latex + case pdf(engine: String?) + } + // swiftlint:enable nesting + } + struct GenerateJsonOptions: Equatable, Sendable { let templateDirectory: String? let templateRepo: String? diff --git a/Sources/ConfigurationClient/Configuration.swift b/Sources/ConfigurationClient/Configuration.swift index 2ad9508..687a9c9 100644 --- a/Sources/ConfigurationClient/Configuration.swift +++ b/Sources/ConfigurationClient/Configuration.swift @@ -1,9 +1,12 @@ import Foundation +// NOTE: When adding items, then the 'hpa.toml' resource file needs to be updated. + /// Represents configurable settings for the command line tool. public struct Configuration: Codable, Equatable, Sendable { public let args: [String]? public let useVaultArgs: Bool + public let generate: Generate? public let playbook: Playbook? public let template: Template public let vault: Vault @@ -11,12 +14,14 @@ public struct Configuration: Codable, Equatable, Sendable { public init( args: [String]? = nil, useVaultArgs: Bool = true, + generate: Generate? = nil, playbook: Playbook? = nil, template: Template = .init(), vault: Vault = .init() ) { self.args = args self.useVaultArgs = useVaultArgs + self.generate = generate self.playbook = playbook self.template = template self.vault = vault @@ -32,6 +37,40 @@ public struct Configuration: Codable, Equatable, Sendable { ) } + public struct Generate: Codable, Equatable, Sendable { + public let buildDirectory: String? + public let files: [String]? + public let includeInHeader: [String]? + public let outputFileName: String? + public let pdfEngine: String? + + public init( + buildDirectory: String? = nil, + files: [String]? = nil, + includeInHeader: [String]? = nil, + outputFileName: String? = nil, + pdfEngine: String? = nil + ) { + self.buildDirectory = buildDirectory + self.files = files + self.includeInHeader = includeInHeader + self.outputFileName = outputFileName + self.pdfEngine = pdfEngine + } + + public static let `default` = Self.mock + + public static var mock: Self { + .init( + buildDirectory: ".build", + files: ["Report.md", "Definitions.md"], + includeInHeader: ["head.tex", "footer.tex"], + outputFileName: "Report", + pdfEngine: "xelatex" + ) + } + } + public struct Playbook: Codable, Equatable, Sendable { public let directory: String? diff --git a/Sources/ConfigurationClient/Resources/hpa.toml b/Sources/ConfigurationClient/Resources/hpa.toml index b8cae62..93bd994 100644 --- a/Sources/ConfigurationClient/Resources/hpa.toml +++ b/Sources/ConfigurationClient/Resources/hpa.toml @@ -1,13 +1,42 @@ +# NOTE: # Configuration settings for the `hpa` command line tool. -# -# Delete settings that are not applicable to your use case. +# You can delete settings that are not applicable to your use case. # Default arguments / options that get passed into `ansible-playbook` commands. +# WARNING: Do not put arguments / options that contain spaces in the same string, +# they should be separate strings, for example do not do something like +# ['--tags debug'], instead use ['--tags', 'debug']. +# args = [] # Set to true if you want to pass the vault args to `ansible-playbook` commands. useVaultArgs = true +# NOTE: +# Configuration for running the generate command(s). This allows custimizations +# to the files that get used to generate the final output (generally a pdf). +# See `pandoc --help`. Below are the defaults that get used, which only need +# adjusted if your template does not follow the default template design or if +# you add extra files to your template that need to be included in the final +# output. Also be aware that any of the files specified in the `files` or +# `includeInHeader` options, need to be inside the `buildDirectory` when generating +# the final output file. + +# [generate] +# this relative to the project directory. +# buildDirectory = '.build' +# pdfEngine = 'xelatex' +# includeInHeader = [ +# 'head.tex', +# 'footer.tex' +# ] +# files = [ +# 'Report.md', +# 'Definitions.md' +# ] +# outputFileName = 'Report' + +# NOTE: # These are more for local development of the ansible playbook and should not be needed # in most cases. Uncomment the lines if you want to customize the playbook and use it # instead of the provided / default playbook. @@ -17,6 +46,7 @@ useVaultArgs = true #inventory = '/path/to/local/inventory.ini' #version = 'main' +# NOTE: # These are to declare where your template files are either on your local system or # a remote git repository. [template] @@ -31,6 +61,7 @@ url = 'https://git.example.com/consult-template.git' # branch. version = '1.0.0' +# NOTE: # Holds settings for `ansible-vault` commands. [vault] # Arguments to pass to commands that use `ansible-vault`, such as encrypting or decrypting diff --git a/Sources/PlaybookClient/PlaybookClient.swift b/Sources/PlaybookClient/PlaybookClient.swift index 340aa3c..d609b7e 100644 --- a/Sources/PlaybookClient/PlaybookClient.swift +++ b/Sources/PlaybookClient/PlaybookClient.swift @@ -39,14 +39,13 @@ private func install(config: Configuration.Playbook?) async throws { @Dependency(\.logger) var logger @Dependency(\.asyncShellClient) var shell - let path = config?.directory ?? PlaybookClient.Constants.defaultInstallationPath - let version = config?.version ?? PlaybookClient.Constants.playbookRepoVersion + let (path, version) = parsePlaybookPathAndVerion(config) let parentDirectory = URL(filePath: path) .deletingLastPathComponent() - let exists = try await fileClient.isDirectory(parentDirectory) - if !exists { + let parentExists = try await fileClient.isDirectory(parentDirectory) + if !parentExists { try await fileClient.createDirectory(parentDirectory) } @@ -70,3 +69,10 @@ private func install(config: Configuration.Playbook?) async throws { )) } } + +private func parsePlaybookPathAndVerion(_ configuration: Configuration.Playbook?) -> (path: String, version: String) { + return ( + path: configuration?.directory ?? PlaybookClient.Constants.defaultInstallationPath, + version: configuration?.version ?? PlaybookClient.Constants.playbookRepoVersion + ) +} diff --git a/Sources/TestSupport/TestSupport.swift b/Sources/TestSupport/TestSupport.swift index 6ef21e1..6888e7d 100644 --- a/Sources/TestSupport/TestSupport.swift +++ b/Sources/TestSupport/TestSupport.swift @@ -6,10 +6,6 @@ import Logging public protocol TestCase {} public extension TestCase { -// static var logLevel: Logger.Level = { -// let levelString = ProcessInfo.processInfo.environment["LOG_LEVEL"] ?? "debug" -// return Logger.Level(rawValue: levelString) ?? .debug -// }() func withTestLogger( key: String, diff --git a/Sources/TestSupport/WithTempDir.swift b/Sources/TestSupport/WithTempDir.swift new file mode 100644 index 0000000..4d8e619 --- /dev/null +++ b/Sources/TestSupport/WithTempDir.swift @@ -0,0 +1,15 @@ +import Foundation + +// swiftlint:disable force_try +public func withTemporaryDirectory( + _ operation: @Sendable (URL) async throws -> Void +) async rethrows { + let temporaryDirectory = FileManager.default + .temporaryDirectory + .appending(path: UUID().uuidString) + try! FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: false) + try await operation(temporaryDirectory) + try! FileManager.default.removeItem(at: temporaryDirectory) +} + +// swiftlint:enable force_try diff --git a/Sources/hpa/Application.swift b/Sources/hpa/Application.swift index b587f8a..652b296 100644 --- a/Sources/hpa/Application.swift +++ b/Sources/hpa/Application.swift @@ -9,9 +9,13 @@ struct Application: AsyncParsableCommand { static let configuration = CommandConfiguration( commandName: Constants.appName, abstract: createAbstract("A utility for working with ansible hpa playbook."), - version: VERSION, + version: VERSION ?? "0.0.0", subcommands: [ - BuildCommand.self, CreateCommand.self, VaultCommand.self, UtilsCommand.self + BuildCommand.self, + CreateCommand.self, + GenerateCommand.self, + VaultCommand.self, + UtilsCommand.self ] ) diff --git a/Sources/hpa/BuildCommand.swift b/Sources/hpa/BuildCommand.swift index e8e315a..30d22b8 100644 --- a/Sources/hpa/BuildCommand.swift +++ b/Sources/hpa/BuildCommand.swift @@ -1,6 +1,7 @@ import ArgumentParser import CliClient import Dependencies +import Foundation struct BuildCommand: AsyncParsableCommand { @@ -9,16 +10,23 @@ struct BuildCommand: AsyncParsableCommand { static let configuration = CommandConfiguration.playbook( commandName: commandName, abstract: "Build a home performance assesment project.", - examples: (label: "Build Project", example: "\(commandName) /path/to/project") + examples: ( + label: "Build project when in the project directory.", + example: "\(commandName)" + ), + ( + label: "Build project from outside the project directory.", + example: "\(commandName) --project-directory /path/to/project" + ) ) @OptionGroup var globals: GlobalOptions - @Argument( + @Option( help: "Path to the project directory.", completion: .directory ) - var projectDir: String + var projectDirectory: String? @Argument( help: "Extra arguments / options passed to the playbook." @@ -27,19 +35,21 @@ struct BuildCommand: AsyncParsableCommand { mutating func run() async throws { try await _run() - -// try await runPlaybook( -// commandName: Self.commandName, -// globals: globals, -// extraArgs: extraArgs, -// "--tags", "build-project", -// "--extra-vars", "project_dir=\(projectDir)" -// ) } private func _run() async throws { @Dependency(\.cliClient) var cliClient + let projectDir: String + if projectDirectory == nil { + guard let pwd = ProcessInfo.processInfo.environment["PWD"] else { + throw ProjectDirectoryNotSupplied() + } + projectDir = pwd + } else { + projectDir = projectDirectory! + } + try await cliClient.runPlaybookCommand( globals.playbookOptions( arguments: [ @@ -52,3 +62,5 @@ struct BuildCommand: AsyncParsableCommand { ) } } + +struct ProjectDirectoryNotSupplied: Error {} diff --git a/Sources/hpa/GenerateCommands/GenerateCommand.swift b/Sources/hpa/GenerateCommands/GenerateCommand.swift new file mode 100644 index 0000000..0628bf1 --- /dev/null +++ b/Sources/hpa/GenerateCommands/GenerateCommand.swift @@ -0,0 +1,14 @@ +import ArgumentParser + +struct GenerateCommand: AsyncParsableCommand { + static let commandName = "generate" + + static let configuration = CommandConfiguration( + commandName: commandName, + subcommands: [ + GeneratePdfCommand.self, + GenerateLatexCommand.self, + GenerateHtmlCommand.self + ] + ) +} diff --git a/Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift b/Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift new file mode 100644 index 0000000..8533c36 --- /dev/null +++ b/Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift @@ -0,0 +1,24 @@ +import ArgumentParser +import CliClient +import Dependencies + +// TODO: Need to add a step to build prior to generating file. +struct GenerateHtmlCommand: AsyncParsableCommand { + static let commandName = "html" + + static let configuration = CommandConfiguration( + commandName: commandName + ) + + @OptionGroup var globals: GenerateOptions + + mutating func run() async throws { + @Dependency(\.cliClient) var cliClient + + try await cliClient.runPandocCommand( + globals.pandocOptions(.html), + logging: globals.loggingOptions(commandName: Self.commandName) + ) + } + +} diff --git a/Sources/hpa/GenerateCommands/GenerateLatexCommand.swift b/Sources/hpa/GenerateCommands/GenerateLatexCommand.swift new file mode 100644 index 0000000..2270286 --- /dev/null +++ b/Sources/hpa/GenerateCommands/GenerateLatexCommand.swift @@ -0,0 +1,23 @@ +import ArgumentParser +import CliClient +import Dependencies + +// TODO: Need to add a step to build prior to generating file. +struct GenerateLatexCommand: AsyncParsableCommand { + static let commandName = "latex" + + static let configuration = CommandConfiguration( + commandName: commandName + ) + + @OptionGroup var globals: GenerateOptions + + mutating func run() async throws { + @Dependency(\.cliClient) var cliClient + + try await cliClient.runPandocCommand( + globals.pandocOptions(.latex), + logging: globals.loggingOptions(commandName: Self.commandName) + ) + } +} diff --git a/Sources/hpa/GenerateCommands/GenerateOptions.swift b/Sources/hpa/GenerateCommands/GenerateOptions.swift new file mode 100644 index 0000000..d514ce4 --- /dev/null +++ b/Sources/hpa/GenerateCommands/GenerateOptions.swift @@ -0,0 +1,87 @@ +import ArgumentParser +import CliClient + +@dynamicMemberLookup +struct GenerateOptions: ParsableArguments { + + @OptionGroup var basic: BasicGlobalOptions + + @Option( + name: [.short, .customLong("file")], + help: "Files used to generate the output, can be specified multiple times.", + completion: .file() + ) + var files: [String] = [] + + @Option( + name: [.customShort("H"), .long], + help: "Files to include in the header, can be specified multiple times.", + completion: .file() + ) + var includeInHeader: [String] = [] + + @Flag( + help: "Do not build the project prior to generating the output." + ) + var noBuild: Bool = false + + @Option( + name: .shortAndLong, + help: "The project directory.", + completion: .directory + ) + var projectDirectory: String? + + @Option( + name: .shortAndLong, + help: "The output directory", + completion: .directory + ) + var outputDirectory: String? + + @Option( + name: [.customShort("n"), .customLong("name")], + help: "Name of the output file." + ) + var outputFileName: String? + + // NOTE: This must be last, both here and in the commands, so if the commands have options of their + // own, they must be declared ahead of using the global options. + + @Argument( + help: "Extra arguments / options to pass to the underlying pandoc command." + ) + var extraOptions: [String] = [] + + subscript(dynamicMember keyPath: WritableKeyPath) -> T { + get { basic[keyPath: keyPath] } + set { basic[keyPath: keyPath] = newValue } + } + + subscript(dynamicMember keyPath: KeyPath) -> T { + basic[keyPath: keyPath] + } +} + +extension GenerateOptions { + + func loggingOptions(commandName: String) -> CliClient.LoggingOptions { + basic.loggingOptions(commandName: commandName) + } + + func pandocOptions( + _ fileType: CliClient.PandocOptions.FileType + ) -> CliClient.PandocOptions { + .init( + files: files.count > 0 ? files : nil, + includeInHeader: includeInHeader.count > 0 ? includeInHeader : nil, + outputDirectory: outputDirectory, + outputFileName: outputFileName, + outputFileType: fileType, + projectDirectory: projectDirectory, + quiet: basic.quiet, + shell: basic.shell, + shouldBuild: !noBuild + ) + } +} diff --git a/Sources/hpa/GenerateCommands/GeneratePdfCommand.swift b/Sources/hpa/GenerateCommands/GeneratePdfCommand.swift new file mode 100644 index 0000000..4a86fb0 --- /dev/null +++ b/Sources/hpa/GenerateCommands/GeneratePdfCommand.swift @@ -0,0 +1,32 @@ +import ArgumentParser +import CliClient +import Dependencies + +// TODO: Need to add a step to build prior to generating file. + +struct GeneratePdfCommand: AsyncParsableCommand { + static let commandName = "pdf" + + static let configuration = CommandConfiguration( + commandName: commandName + ) + + @Option( + name: [.customShort("e"), .customLong("engine")], + help: "The pdf engine to use." + ) + var pdfEngine: String? + + @OptionGroup var globals: GenerateOptions + + mutating func run() async throws { + @Dependency(\.cliClient) var cliClient + + let output = try await cliClient.runPandocCommand( + globals.pandocOptions(.pdf(engine: pdfEngine)), + logging: globals.loggingOptions(commandName: Self.commandName) + ) + + print(output) + } +} diff --git a/Sources/hpa/Internal/GlobalOptions.swift b/Sources/hpa/Internal/GlobalOptions.swift index 77fa51e..25684ca 100644 --- a/Sources/hpa/Internal/GlobalOptions.swift +++ b/Sources/hpa/Internal/GlobalOptions.swift @@ -27,10 +27,10 @@ struct BasicGlobalOptions: ParsableArguments { struct GlobalOptions: ParsableArguments { @Option( - name: .shortAndLong, + name: .long, help: "Optional path to the ansible hpa playbook directory." ) - var playbookDir: String? + var playbookDirectory: String? @Option( name: .shortAndLong, diff --git a/Sources/hpa/Internal/PlaybookOptions+Globals.swift b/Sources/hpa/Internal/PlaybookOptions+Globals.swift index 7969884..f805f3c 100644 --- a/Sources/hpa/Internal/PlaybookOptions+Globals.swift +++ b/Sources/hpa/Internal/PlaybookOptions+Globals.swift @@ -11,7 +11,7 @@ extension GlobalOptions { arguments: arguments, configuration: configuration, inventoryFilePath: inventoryPath, - playbookDirectory: playbookDir, + playbookDirectory: playbookDirectory, quiet: quietOnlyPlaybook ? true : basic.quiet, shell: basic.shell ) diff --git a/Sources/hpa/Version.swift b/Sources/hpa/Version.swift new file mode 100644 index 0000000..c5554ab --- /dev/null +++ b/Sources/hpa/Version.swift @@ -0,0 +1,2 @@ +// Do not set this variable, it is set during the build process. +let VERSION: String? = nil diff --git a/Tests/ConfigurationClientTests/ConfigurationClientTests.swift b/Tests/ConfigurationClientTests/ConfigurationClientTests.swift index 9495fe4..968a20f 100644 --- a/Tests/ConfigurationClientTests/ConfigurationClientTests.swift +++ b/Tests/ConfigurationClientTests/ConfigurationClientTests.swift @@ -143,23 +143,6 @@ func generateFindEnvironments(file: File) -> [[String: String]] { ] } -// swiftlint:disable force_try -func withTemporaryDirectory( - _ operation: @Sendable (URL) async throws -> Void -) async rethrows { - let dir = FileManager.default.temporaryDirectory - let tempDir = dir.appending(path: UUID().uuidString) - - try! FileManager.default.createDirectory( - atPath: tempDir.cleanFilePath, - withIntermediateDirectories: false - ) - try await operation(tempDir) - try! FileManager.default.removeItem(at: tempDir) -} - -// swiftlint:enable force_try - func withGeneratedConfigFile( named fileName: String, client: ConfigurationClient, diff --git a/Tests/PlaybookClientTests/PlaybookClientTests.swift b/Tests/PlaybookClientTests/PlaybookClientTests.swift index 7e792d4..5690eeb 100644 --- a/Tests/PlaybookClientTests/PlaybookClientTests.swift +++ b/Tests/PlaybookClientTests/PlaybookClientTests.swift @@ -5,6 +5,7 @@ import Foundation @_spi(Internal) import PlaybookClient import ShellClient import Testing +import TestSupport @Suite("PlaybookClientTests") struct PlaybookClientTests { @@ -15,18 +16,17 @@ struct PlaybookClientTests { $0.fileClient = .liveValue $0.asyncShellClient = .liveValue } operation: { - let tempDirectory = FileManager.default.temporaryDirectory - let pathUrl = tempDirectory.appending(path: "playbook") - let playbookClient = PlaybookClient.liveValue + try await withTemporaryDirectory { tempDirectory in + let pathUrl = tempDirectory.appending(path: "playbook") + let playbookClient = PlaybookClient.liveValue - let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath)) + let configuration = Configuration(playbook: .init(directory: pathUrl.cleanFilePath)) - try? FileManager.default.removeItem(at: pathUrl) - try await playbookClient.installPlaybook(configuration) - let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath) - #expect(exists) - - try FileManager.default.removeItem(at: pathUrl) + try? FileManager.default.removeItem(at: pathUrl) + try await playbookClient.installPlaybook(configuration) + let exists = FileManager.default.fileExists(atPath: pathUrl.cleanFilePath) + #expect(exists) + } } } diff --git a/justfile b/justfile index 47db532..3cccc13 100644 --- a/justfile +++ b/justfile @@ -16,3 +16,10 @@ alias r := run clean: rm -rf .build + +update-version: + @swift package \ + --disable-sandbox \ + --allow-writing-to-package-directory \ + update-version \ + hpa