feat: Merges dev
All checks were successful
CI / Run Tests (push) Successful in 2m43s

This commit is contained in:
2024-12-17 15:55:36 -05:00
parent 857177032c
commit faa28749bc
88 changed files with 4513 additions and 2301 deletions

View File

@@ -0,0 +1,9 @@
extension PandocClient {
/// Represents constant string values needed internally.
enum Constants {
static let pandocCommand = "pandoc"
static let defaultOutputFileName = "Report"
static let defaultPdfEngine = "xelatex"
}
}

View File

@@ -0,0 +1,245 @@
import CommandClient
import ConfigurationClient
import Dependencies
import Foundation
import PlaybookClient
extension PandocClient.RunOptions {
func run(
_ fileType: PandocClient.FileType,
_ environment: [String: String]
) async throws -> String {
@Dependency(\.commandClient) var commandClient
@Dependency(\.logger) var logger
@Dependency(\.playbookClient) var playbookClient
return try await commandClient.run(logging: loggingOptions, quiet: quiet, shell: shell) {
let ensuredOptions = try await self.ensuredOptions(fileType)
let projectDirectory = self.projectDirectory ?? environment["PWD"]
guard let projectDirectory else {
throw ProjectDirectoryNotSpecified()
}
if shouldBuildProject {
logger.debug("Building project...")
try await playbookClient.run.buildProject(.init(
projectDirectory: projectDirectory,
shared: .init(
extraOptions: nil,
inventoryFilePath: nil,
loggingOptions: loggingOptions,
quiet: quiet,
shell: shell
)
))
}
let outputDirectory = self.outputDirectory ?? projectDirectory
let outputPath = "\(outputDirectory)/\(ensuredOptions.ensuredExtensionFileName)"
let arguments = ensuredOptions.makeArguments(
outputPath: outputPath,
projectDirectory: projectDirectory
)
logger.debug("Pandoc arguments: \(arguments)")
return (arguments, outputPath)
}
}
func ensuredOptions(
_ fileType: PandocClient.FileType
) async throws -> EnsuredPandocOptions {
@Dependency(\.configurationClient) var configurationClient
@Dependency(\.logger) var logger
let configuration = try await configurationClient.findAndLoad()
logger.debug("Configuration: \(configuration)")
return try await ensurePandocOptions(
configuration: configuration,
fileType: fileType,
options: self
)
}
}
extension PandocClient.FileType {
var fileExtension: String {
switch self {
case .html: return "html"
case .latex: return "tex"
case .pdf: return "pdf"
}
}
}
@_spi(Internal)
public struct EnsuredPandocOptions: Equatable, Sendable {
public let buildDirectory: String
public let extraOptions: [String]?
public let files: [String]
public let includeInHeader: [String]
public let outputFileName: String
public let outputFileType: PandocClient.FileType
public let pdfEngine: String?
public var ensuredExtensionFileName: String {
let extensionString = ".\(outputFileType.fileExtension)"
if !outputFileName.hasSuffix(extensionString) {
return outputFileName + extensionString
}
return outputFileName
}
func makeArguments(
outputPath: String,
projectDirectory: String
) -> [String] {
var arguments = [PandocClient.Constants.pandocCommand]
arguments += includeInHeader.map {
"--include-in-header=\(projectDirectory)/\(buildDirectory)/\($0)"
}
if let pdfEngine {
arguments.append("--pdf-engine=\(pdfEngine)")
}
arguments.append("--output=\(outputPath)")
if let extraOptions {
arguments.append(contentsOf: extraOptions)
}
arguments += files.map {
"\(projectDirectory)/\(buildDirectory)/\($0)"
}
return arguments
}
}
@_spi(Internal)
public func ensurePandocOptions(
configuration: Configuration,
fileType: PandocClient.FileType,
options: PandocClient.RunOptions
) async throws -> EnsuredPandocOptions {
let defaults = Configuration.Generate.default
return .init(
buildDirectory: options.parseBuildDirectory(configuration.generate, defaults),
extraOptions: options.extraOptions,
files: options.parseFiles(configuration.generate, defaults),
includeInHeader: options.parseIncludeInHeader(configuration.generate, defaults),
outputFileName: options.parseOutputFileName(configuration.generate, defaults),
outputFileType: fileType,
pdfEngine: fileType.parsePdfEngine(configuration.generate, defaults)
)
}
@_spi(Internal)
public extension PandocClient.FileType {
func parsePdfEngine(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String? {
switch self {
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 PandocClient.Constants.defaultPdfEngine
}
}
}
}
@_spi(Internal)
public extension PandocClient.RunOptions {
func parseFiles(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> [String] {
@Dependency(\.logger) var logger
if let files = 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 []
}
}
func parseIncludeInHeader(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> [String] {
@Dependency(\.logger) var logger
if let files = 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 []
}
}
func parseOutputFileName(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String {
@Dependency(\.logger) var logger
if let output = 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 PandocClient.Constants.defaultOutputFileName
}
}
func parseBuildDirectory(
_ configuration: Configuration.Generate?,
_ defaults: Configuration.Generate
) -> String {
@Dependency(\.logger) var logger
if let output = 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"
}
}
}
struct ProjectDirectoryNotSpecified: Error {}

View File

@@ -0,0 +1,141 @@
import CommandClient
import ConfigurationClient
import Dependencies
import DependenciesMacros
import Foundation
public extension DependencyValues {
/// Represents interactions with the `pandoc` command line application.
///
/// The `pandoc` command line application is used to generate the final output
/// documents from a home performance assessment project.
///
var pandocClient: PandocClient {
get { self[PandocClient.self] }
set { self[PandocClient.self] = newValue }
}
}
@DependencyClient
public struct PandocClient: Sendable {
/// Run a pandoc command.
public var run: Run
/// Represents the pandoc commands that we can run.
@DependencyClient
public struct Run: Sendable {
/// Generate a latex file from the given options, returning the path the
/// generated file was written to.
public var generateLatex: @Sendable (RunOptions) async throws -> String
/// Generate an html file from the given options, returning the path the
/// generated file was written to.
public var generateHtml: @Sendable (RunOptions) async throws -> String
/// Generate a pdf file from the given options, returning the path the
/// generated file was written to.
public var generatePdf: @Sendable (RunOptions, String?) async throws -> String
/// Generate a pdf file from the given options, returning the path the
/// generated file was written to.
///
/// - Parameters:
/// - options: The shared run options.
/// - pdfEngine: The pdf-engine to use to generate the pdf file.
public func generatePdf(
_ options: RunOptions,
pdfEngine: String? = nil
) async throws -> String {
try await generatePdf(options, pdfEngine)
}
}
/// Represents the shared options used to run a `pandoc` command.
///
///
public struct RunOptions: Equatable, Sendable {
let buildDirectory: String?
let extraOptions: [String]?
let files: [String]?
let loggingOptions: LoggingOptions
let includeInHeader: [String]?
let outputDirectory: String?
let outputFileName: String?
let projectDirectory: String?
let quiet: Bool
let shell: String?
let shouldBuildProject: Bool
/// Create the shared run options.
///
/// - Parameters:
/// - buildDirectory: Specify the build directory of the project.
/// - extraOptions: Extra arguments / options passed to the `pandoc` command.
/// - file: Files used to build the final output.
/// - loggingOptions: The logging options used when running the command.
/// - includeInHeader: Files to include in the header of the final output document.
/// - outputDirectory: Optional output directory for the output file, defaults to current working directory.
/// - projectDirectory: Optional project directory, defaults to current working directory.
/// - outputFileName: Override the output file name.
/// - quiet: Don't log output from the command.
/// - shell: Optional shell to use when calling the pandoc command.
/// - shouldBuildProject: Build the project prior to generating the output file.
public init(
buildDirectory: String? = nil,
extraOptions: [String]? = nil,
files: [String]? = nil,
loggingOptions: LoggingOptions,
includeInHeader: [String]? = nil,
outputDirectory: String? = nil,
projectDirectory: String? = nil,
outputFileName: String? = nil,
quiet: Bool = false,
shell: String? = nil,
shouldBuild shouldBuildProject: Bool = true
) {
self.buildDirectory = buildDirectory
self.extraOptions = extraOptions
self.files = files
self.loggingOptions = loggingOptions
self.includeInHeader = includeInHeader
self.outputDirectory = outputDirectory
self.outputFileName = outputFileName
self.projectDirectory = projectDirectory
self.quiet = quiet
self.shell = shell
self.shouldBuildProject = shouldBuildProject
}
}
/// Represents the file types that we can generate output files for.
@_spi(Internal)
public enum FileType: Equatable, Sendable {
case html
case latex
case pdf(engine: String?)
}
}
extension PandocClient: DependencyKey {
public static let testValue: PandocClient = Self(run: Run())
public static func live(environment: [String: String]) -> PandocClient {
.init(
run: Run(
generateLatex: { try await $0.run(.latex, environment) },
generateHtml: { try await $0.run(.html, environment) },
generatePdf: { try await $0.run(.pdf(engine: $1), environment) }
)
)
}
public static var liveValue: PandocClient {
.live(environment: ProcessInfo.processInfo.environment)
}
}