feat: Adding documentation comments.
All checks were successful
CI / Run Tests (push) Successful in 2m17s

This commit is contained in:
2024-12-17 11:51:03 -05:00
parent f596975bbc
commit f8e89ed0fa
6 changed files with 179 additions and 21 deletions

View File

@@ -110,15 +110,33 @@ public struct CommandClient: Sendable {
} }
} }
/// Represents logger setup options used when running commands.
///
/// This set's up metadata tags and keys appropriately for the command being run,
/// which can aid in debugging.
///
public struct LoggingOptions: Equatable, Sendable { public struct LoggingOptions: Equatable, Sendable {
/// The command name / key that the logger is running for.
public let commandName: String public let commandName: String
/// The log level.
public let logLevel: Logger.Level public let logLevel: Logger.Level
/// Create the logging options.
///
/// - Parameters:
/// - commandName: The command name / key that the logger is running for.
/// - logLevel: The log level.
public init(commandName: String, logLevel: Logger.Level) { public init(commandName: String, logLevel: Logger.Level) {
self.commandName = commandName self.commandName = commandName
self.logLevel = logLevel self.logLevel = logLevel
} }
/// Perform an operation with a setup logger.
///
/// - Parameters:
/// - operation: The operation to perform with the setup logger.
@discardableResult @discardableResult
public func withLogger<T>( public func withLogger<T>(
operation: @Sendable @escaping () async throws -> T operation: @Sendable @escaping () async throws -> T

View File

@@ -52,11 +52,34 @@ public struct Configuration: Codable, Equatable, Sendable {
) )
} }
/// Configuration options for running `pandoc` commands that generate files from
/// a templated directory.
///
/// ## NOTE: Most of these can also be set by the template repository variables.
///
///
public struct Generate: Codable, Equatable, Sendable { public struct Generate: Codable, Equatable, Sendable {
/// Specifiy the name of the build directory, generally `.build`.
public let buildDirectory: String? public let buildDirectory: String?
/// Specifiy the files used in the `pandoc` command to generate a final output file.
///
/// - SeeAlso: `pandoc --help`
public let files: [String]? public let files: [String]?
/// Specifiy the files used in the `pandoc` command to include in the header.
///
/// - SeeAlso: `pandoc --help`
public let includeInHeader: [String]? public let includeInHeader: [String]?
/// The default name of the output file, this does not require the file extension as we will
/// add it based on the command that is called. Generally this is 'Report'.
///
public let outputFileName: String? public let outputFileName: String?
/// The default pdf engine to use when generating pdf files using `pandoc`. Generally
/// this is 'xelatex'.
public let pdfEngine: String? public let pdfEngine: String?
public init( public init(
@@ -73,8 +96,10 @@ public struct Configuration: Codable, Equatable, Sendable {
self.pdfEngine = pdfEngine self.pdfEngine = pdfEngine
} }
/// Represents the default configuration for generating files using `pandoc`.
public static let `default` = Self.mock public static let `default` = Self.mock
/// Represents mock configuration for generating files using `pandoc`.
public static var mock: Self { public static var mock: Self {
.init( .init(
buildDirectory: ".build", buildDirectory: ".build",
@@ -86,10 +111,22 @@ public struct Configuration: Codable, Equatable, Sendable {
} }
} }
/// Configuration options for the ansible-hpa-playbook. The playbook is
/// the primary driver of templating files, generating repository templates, and building
/// projects.
///
/// ## NOTE: These are generally only used for local development of the playbook.
///
///
public struct Playbook: Codable, Equatable, Sendable { public struct Playbook: Codable, Equatable, Sendable {
/// The directory location of the ansible playbook.
public let directory: String? public let directory: String?
/// The inventory file name / location.
public let inventory: String? public let inventory: String?
/// The playbook version, branch, or tag.
public let version: String? public let version: String?
public init( public init(
@@ -105,9 +142,28 @@ public struct Configuration: Codable, Equatable, Sendable {
public static var mock: Self { .init() } public static var mock: Self { .init() }
} }
/// Configuration settings for the user's template repository or directory.
///
/// A template is what is used to create projects for a user. Generally they setup
/// their own template by customizing the default template to their needs. This template
/// can then be stored as a git repository or on the local file system.
///
/// The template will hold variables and files that are used to generate the final output
/// files using `pandoc`. Generally the template will hold files that may only need setup once,
/// such as header files, footer files, and definitions.
///
/// The project directory contains dynamic variables / files that need edited in order
/// to generate the final output. This allows the project directory to remain smaller so the user
/// can more easily focus on the tasks required to generate the final output files.
public struct Template: Codable, Equatable, Sendable { public struct Template: Codable, Equatable, Sendable {
/// A url of the template when it is a remote git repository.
public let url: String? public let url: String?
/// The version, branch, or tag of the remote repository.
public let version: String? public let version: String?
/// The local directory that contains the template on the local file system.
public let directory: String? public let directory: String?
public init( public init(
@@ -129,8 +185,23 @@ public struct Configuration: Codable, Equatable, Sendable {
} }
} }
/// Configuration for `ansible-vault` commands. Ansible vault is used to encrypt and
/// decrypt sensitive data. This allows for variables, such as customer name and address
/// to be stored along with the project in an encrypted file so that it is safer to store them.
///
/// These may also be used in general `ansible-playbook` commands if the `useVaultArgs` is set
/// to `true` in a users configuration.
///
public struct Vault: Codable, Equatable, Sendable { public struct Vault: Codable, Equatable, Sendable {
/// A list of arguments / options that get passed to the `ansible-vault` command.
///
/// - SeeAlso: `ansible-vault --help`
public let args: [String]? public let args: [String]?
/// An id that is used during encrypting `ansible-vault` files.
///
/// - SeeAlso: `ansible-vault encrypt --help`
public let encryptId: String? public let encryptId: String?
public init( public init(

View File

@@ -6,24 +6,57 @@ import Foundation
import ShellClient import ShellClient
public extension DependencyValues { public extension DependencyValues {
/// Interacts with the user's configuration.
var configurationClient: ConfigurationClient { var configurationClient: ConfigurationClient {
get { self[ConfigurationClient.self] } get { self[ConfigurationClient.self] }
set { self[ConfigurationClient.self] = newValue } set { self[ConfigurationClient.self] = newValue }
} }
} }
/// Represents actions that can be taken on user's configuration files.
///
///
@DependencyClient @DependencyClient
public struct ConfigurationClient: Sendable { public struct ConfigurationClient: Sendable {
/// Find the user's configuration, searches in the current directory and default
/// locations where configuration can be stored. An error is thrown if no configuration
/// is found.
public var find: @Sendable () async throws -> File public var find: @Sendable () async throws -> File
/// Generate a configuration file for the user.
public var generate: @Sendable (GenerateOptions) async throws -> String public var generate: @Sendable (GenerateOptions) async throws -> String
/// Load a configuration file from the given file location. If the file is
/// not provided then we return an empty configuraion item.
public var load: @Sendable (File?) async throws -> Configuration public var load: @Sendable (File?) async throws -> Configuration
/// Write the configuration to the given file, optionally forcing an overwrite of
/// the file.
///
/// ## NOTE: This uses the `fileClient.write` under the hood, so if you need to control
/// what happens during tests, then you can customize the behavior of the fileClient.
///
var write: @Sendable (File, Configuration, Bool) async throws -> Void var write: @Sendable (File, Configuration, Bool) async throws -> Void
/// Find the user's configuration and load it.
public func findAndLoad() async throws -> Configuration { public func findAndLoad() async throws -> Configuration {
let file = try? await find() let file = try? await find()
return try await load(file) return try await load(file)
} }
/// Write the configuration to the given file, optionally forcing an overwrite of
/// the file. If a file already exists and force is not supplied then we will create
/// a backup of the existing file.
///
/// ## NOTE: This uses the `fileClient.write` under the hood, so if you need to control
/// what happens during tests, then you can customize the behavior of the fileClient.
///
/// - Parameters:
/// - configuration: The configuration to save.
/// - file: The file location and type to save.
/// - force: Force overwritting if a file already exists.
public func write( public func write(
_ configuration: Configuration, _ configuration: Configuration,
to file: File, to file: File,
@@ -60,15 +93,12 @@ extension ConfigurationClient: DependencyKey {
public static func live(environment: [String: String]) -> Self { public static func live(environment: [String: String]) -> Self {
let liveClient = LiveConfigurationClient(environment: environment) let liveClient = LiveConfigurationClient(environment: environment)
return .init { return .init(
try await liveClient.find() find: { try await liveClient.find() },
} generate: { generate: { try await liveClient.generate($0) },
try await liveClient.generate($0) load: { try await liveClient.load(file: $0) },
} load: { file in write: { try await liveClient.write($1, to: $0, force: $2) }
try await liveClient.load(file: file) )
} write: { file, configuration, force in
try await liveClient.write(configuration, to: file, force: force)
}
} }
public static var liveValue: Self { public static var liveValue: Self {
@@ -221,10 +251,12 @@ struct LiveConfigurationClient {
to file: File, to file: File,
force: Bool force: Bool
) async throws { ) async throws {
if !force { let exists = fileManager.fileExists(file.url)
guard !fileManager.fileExists(file.url) else {
throw ConfigurationError.fileExists(path: file.path) if !force, exists {
} let backupUrl = file.url.appendingPathExtension(".back")
logger.warning("File exists, creating a backup of the existing file at: \(backupUrl.cleanFilePath)")
try await fileManager.copy(file.url, backupUrl)
} }
let data: Data let data: Data

View File

@@ -1,3 +1,5 @@
/// Represents keys in the environment that can be used to locate a user's
/// configuration file.
@_spi(Internal) @_spi(Internal)
public enum EnvironmentKey { public enum EnvironmentKey {
static let xdgConfigHome = "XDG_CONFIG_HOME" static let xdgConfigHome = "XDG_CONFIG_HOME"
@@ -5,6 +7,7 @@ public enum EnvironmentKey {
static let hpaConfigFile = "HPA_CONFIG_FILE" static let hpaConfigFile = "HPA_CONFIG_FILE"
} }
/// Represents keys that are used internally for directory names, file names, etc.
@_spi(Internal) @_spi(Internal)
public enum HPAKey { public enum HPAKey {
public static let configDirName = "hpa" public static let configDirName = "hpa"

View File

@@ -2,11 +2,21 @@ import FileClient
import Foundation import Foundation
/// Represents a file location and type on disk for a configuration file. /// Represents a file location and type on disk for a configuration file.
///
/// Currently the supported formats are `json` or `toml`. Toml is the default /
/// preferred format because it allows comments in the file and is easy to understand.
///
public enum File: Equatable, Sendable { public enum File: Equatable, Sendable {
case json(URL) case json(URL)
case toml(URL) case toml(URL)
/// Attempts to create a file with the given url.
///
/// ## NOTE: There are no checks on if a file / path actually exists or not.
///
/// If the file does not have a suffix of `json` or `toml` then
/// we will return `nil`.
public init?(_ url: URL) { public init?(_ url: URL) {
if url.cleanFilePath.hasSuffix("json") { if url.cleanFilePath.hasSuffix("json") {
self = .json(url) self = .json(url)
@@ -17,18 +27,17 @@ public enum File: Equatable, Sendable {
} }
} }
/// Attempts to create a file with the given path.
///
/// ## NOTE: There are no checks on if a file / path actually exists or not.
///
/// If the file does not have a suffix of `json` or `toml` then
/// we will return `nil`.
public init?(_ path: String) { public init?(_ path: String) {
self.init(URL(filePath: path)) self.init(URL(filePath: path))
} }
public static func json(_ path: String) -> Self { /// Get the url of the file.
.json(URL(filePath: path))
}
public static func toml(_ path: String) -> Self {
.toml(URL(filePath: path))
}
public var url: URL { public var url: URL {
switch self { switch self {
case let .json(url): return url case let .json(url): return url
@@ -36,10 +45,14 @@ public enum File: Equatable, Sendable {
} }
} }
/// Get the path string of the file.
public var path: String { public var path: String {
url.cleanFilePath url.cleanFilePath
} }
/// Represents the default file path for a user's configuration.
///
/// Which is `~/.config/hpa/config.toml`
public static var `default`: Self { public static var `default`: Self {
let fileUrl = FileManager.default let fileUrl = FileManager.default
.homeDirectoryForCurrentUser .homeDirectoryForCurrentUser

View File

@@ -141,6 +141,27 @@ struct ConfigurationClientTests: TestCase {
} }
} }
} }
@Test
func writeCreatesBackupFile() async throws {
try await withDependencies {
$0.fileClient = .liveValue
} operation: {
let client = ConfigurationClient.liveValue
try await withGeneratedConfigFile(named: "config.toml", client: client) { configFile in
@Dependency(\.fileClient) var fileClient
let backupUrl = configFile.url.appendingPathExtension(".back")
#expect(fileClient.fileExists(backupUrl) == false)
let config = Configuration()
try await client.write(config, to: configFile)
#expect(fileClient.fileExists(backupUrl))
}
}
}
} }
func generateFindEnvironments(file: File) -> [[String: String]] { func generateFindEnvironments(file: File) -> [[String: String]] {