feat: Adds generate commands that call to pandoc to generate pdf, latex, and html files from a project.
This commit is contained in:
@@ -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"
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
223
Sources/CliClient/CliClient+PandocCommands.swift
Normal file
223
Sources/CliClient/CliClient+PandocCommands.swift
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
15
Sources/TestSupport/WithTempDir.swift
Normal file
15
Sources/TestSupport/WithTempDir.swift
Normal file
@@ -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
|
||||
@@ -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
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
14
Sources/hpa/GenerateCommands/GenerateCommand.swift
Normal file
14
Sources/hpa/GenerateCommands/GenerateCommand.swift
Normal file
@@ -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
|
||||
]
|
||||
)
|
||||
}
|
||||
24
Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift
Normal file
24
Sources/hpa/GenerateCommands/GenerateHtmlCommand.swift
Normal file
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
23
Sources/hpa/GenerateCommands/GenerateLatexCommand.swift
Normal file
23
Sources/hpa/GenerateCommands/GenerateLatexCommand.swift
Normal file
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
87
Sources/hpa/GenerateCommands/GenerateOptions.swift
Normal file
87
Sources/hpa/GenerateCommands/GenerateOptions.swift
Normal file
@@ -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<T>(dynamicMember keyPath: WritableKeyPath<BasicGlobalOptions, T>) -> T {
|
||||
get { basic[keyPath: keyPath] }
|
||||
set { basic[keyPath: keyPath] = newValue }
|
||||
}
|
||||
|
||||
subscript<T>(dynamicMember keyPath: KeyPath<BasicGlobalOptions, T>) -> 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
|
||||
)
|
||||
}
|
||||
}
|
||||
32
Sources/hpa/GenerateCommands/GeneratePdfCommand.swift
Normal file
32
Sources/hpa/GenerateCommands/GeneratePdfCommand.swift
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -11,7 +11,7 @@ extension GlobalOptions {
|
||||
arguments: arguments,
|
||||
configuration: configuration,
|
||||
inventoryFilePath: inventoryPath,
|
||||
playbookDirectory: playbookDir,
|
||||
playbookDirectory: playbookDirectory,
|
||||
quiet: quietOnlyPlaybook ? true : basic.quiet,
|
||||
shell: basic.shell
|
||||
)
|
||||
|
||||
2
Sources/hpa/Version.swift
Normal file
2
Sources/hpa/Version.swift
Normal file
@@ -0,0 +1,2 @@
|
||||
// Do not set this variable, it is set during the build process.
|
||||
let VERSION: String? = nil
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user