feat: Moves logging extensions into it's own module, moves configuration merging into config-client, fixes merging bug.

This commit is contained in:
2024-12-25 20:06:52 -05:00
parent fbb4a22e98
commit 56359f3488
11 changed files with 269 additions and 135 deletions

View File

@@ -13,6 +13,7 @@ let package = Package(
.library(name: "ConfigurationClient", targets: ["ConfigurationClient"]), .library(name: "ConfigurationClient", targets: ["ConfigurationClient"]),
.library(name: "FileClient", targets: ["FileClient"]), .library(name: "FileClient", targets: ["FileClient"]),
.library(name: "GitClient", targets: ["GitClient"]), .library(name: "GitClient", targets: ["GitClient"]),
.library(name: "LoggingExtensions", targets: ["LoggingExtensions"]),
.plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]), .plugin(name: "BuildWithVersionPlugin", targets: ["BuildWithVersionPlugin"]),
.plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]), .plugin(name: "GenerateVersionPlugin", targets: ["GenerateVersionPlugin"]),
.plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"]) .plugin(name: "UpdateVersionPlugin", targets: ["UpdateVersionPlugin"])
@@ -40,6 +41,7 @@ let package = Package(
"ConfigurationClient", "ConfigurationClient",
"FileClient", "FileClient",
"GitClient", "GitClient",
"LoggingExtensions",
.product(name: "Logging", package: "swift-log"), .product(name: "Logging", package: "swift-log"),
.product(name: "CustomDump", package: "swift-custom-dump") .product(name: "CustomDump", package: "swift-custom-dump")
] ]
@@ -81,6 +83,13 @@ let package = Package(
name: "GitClientTests", name: "GitClientTests",
dependencies: ["GitClient"] dependencies: ["GitClient"]
), ),
.target(
name: "LoggingExtensions",
dependencies: [
.product(name: "Dependencies", package: "swift-dependencies"),
.product(name: "ShellClient", package: "swift-shell-client")
]
),
.target(name: "TestSupport"), .target(name: "TestSupport"),
.plugin( .plugin(
name: "BuildWithVersionPlugin", name: "BuildWithVersionPlugin",

View File

@@ -4,6 +4,7 @@ import DependenciesMacros
import FileClient import FileClient
import Foundation import Foundation
import GitClient import GitClient
import LoggingExtensions
import ShellClient import ShellClient
public extension DependencyValues { public extension DependencyValues {
@@ -34,24 +35,6 @@ public struct CliClient: Sendable {
case major, minor, patch, preRelease case major, minor, patch, preRelease
} }
// TODO: Need a quiet option, as default log level is warning, need a way to set it to ignore logs.
public struct LoggingOptions: Equatable, Sendable {
let command: String
let executableName: String
let verbose: Int
public init(
executableName: String = "bump-version",
command: String,
verbose: Int
) {
self.executableName = executableName
self.command = command
self.verbose = verbose
}
}
public struct SharedOptions: Equatable, Sendable { public struct SharedOptions: Equatable, Sendable {
let allowPreReleaseTag: Bool let allowPreReleaseTag: Bool
@@ -95,7 +78,9 @@ extension CliClient: DependencyKey {
bump: { try await $1.bump($0) }, bump: { try await $1.bump($0) },
generate: { try await $0.generate() }, generate: { try await $0.generate() },
parsedConfiguration: { options in parsedConfiguration: { options in
try await options.withMergedConfiguration { $0 } try await options.loggingOptions.withLogger {
try await options.withMergedConfiguration { $0 }
}
} }
) )
} }

View File

@@ -47,18 +47,32 @@ public extension CliClient.SharedOptions {
func withMergedConfiguration<T>( func withMergedConfiguration<T>(
operation: (Configuration) async throws -> T operation: (Configuration) async throws -> T
) async throws -> T { ) async throws -> T {
try await withConfiguration(path: configurationFile) { configuration in @Dependency(\.configurationClient) var configurationClient
var configuration = configuration @Dependency(\.logger) var logger
configuration = configuration.mergingTarget(target)
if configuration.strategy?.branch != nil, let branch { var strategy: Configuration.VersionStrategy?
configuration = configuration.mergingStrategy(.branch(branch))
} else if let semvar {
configuration = configuration.mergingStrategy(.semvar(semvar))
}
return try await operation(configuration) if let branch {
logger.trace("Merging branch strategy.")
strategy = .branch(branch)
} else if let semvar {
logger.trace("Merging semvar strategy.")
var semvarString = ""
customDump(semvar, to: &semvarString)
logger.trace("\(semvarString)")
strategy = .semvar(semvar)
} }
let configuration = Configuration(
target: target,
strategy: strategy
)
return try await configurationClient.withConfiguration(
path: configurationFile,
merging: configuration,
operation: operation
)
} }
func write(_ string: String, to url: URL) async throws { func write(_ string: String, to url: URL) async throws {

View File

@@ -1,82 +1,82 @@
import ConfigurationClient // import ConfigurationClient
import Dependencies // import Dependencies
import FileClient // import FileClient
import Foundation // import Foundation
//
extension Configuration { // extension Configuration {
//
func mergingTarget(_ otherTarget: Configuration.Target?) -> Self { // func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
.init( // .init(
target: otherTarget ?? target, // target: otherTarget ?? target,
strategy: strategy // strategy: strategy
) // )
} // }
//
func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self { // func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
.init( // .init(
target: target, // target: target,
strategy: strategy?.merging(otherStrategy) // strategy: strategy?.merging(otherStrategy)
) // )
} // }
} // }
//
extension Configuration.PreRelease { // extension Configuration.PreRelease {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
.init( // .init(
prefix: other?.prefix ?? prefix, // prefix: other?.prefix ?? prefix,
strategy: other?.strategy ?? strategy // strategy: other?.strategy ?? strategy
) // )
} // }
} // }
//
extension Configuration.Branch { // extension Configuration.Branch {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha) // return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
} // }
} // }
//
extension Configuration.SemVar { // extension Configuration.SemVar {
// TODO: Merge strategy ?? // // TODO: Merge strategy ??
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
.init( // .init(
allowPreRelease: other?.allowPreRelease ?? allowPreRelease, // allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
preRelease: preRelease?.merging(other?.preRelease), // preRelease: preRelease?.merging(other?.preRelease),
requireExistingFile: other?.requireExistingFile ?? requireExistingFile, // requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar, // requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,
strategy: other?.strategy ?? strategy // strategy: other?.strategy ?? strategy
) // )
} // }
} // }
//
extension Configuration.VersionStrategy { // extension Configuration.VersionStrategy {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
guard let branch else { // guard let branch else {
guard let semvar else { return self } // guard let semvar else { return self }
return .semvar(semvar.merging(other?.semvar)) // return .semvar(semvar.merging(other?.semvar))
} // }
return .branch(branch.merging(other?.branch)) // return .branch(branch.merging(other?.branch))
} // }
} // }
//
extension Configuration { // extension Configuration {
func merging(_ other: Self?) -> Self { // func merging(_ other: Self?) -> Self {
var output = self // var output = self
output = output.mergingTarget(other?.target) // output = output.mergingTarget(other?.target)
output = output.mergingStrategy(other?.strategy) // output = output.mergingStrategy(other?.strategy)
return output // return output
} // }
} // }
//
@discardableResult // @discardableResult
func withConfiguration<T>( // func withConfiguration<T>(
path: String?, // path: String?,
_ operation: (Configuration) async throws -> T // _ operation: (Configuration) async throws -> T
) async throws -> T { // ) async throws -> T {
@Dependency(\.configurationClient) var configurationClient // @Dependency(\.configurationClient) var configurationClient
//
let configuration = try await configurationClient.findAndLoad( // let configuration = try await configurationClient.findAndLoad(
path != nil ? URL(filePath: path!) : nil // path != nil ? URL(filePath: path!) : nil
) // )
//
return try await operation(configuration) // return try await operation(configuration)
} // }

View File

@@ -0,0 +1,76 @@
import Dependencies
import FileClient
import Foundation
import ShellClient
@_spi(Internal)
public extension Configuration {
func merging(_ other: Self?) -> Self {
mergingTarget(other?.target).mergingStrategy(other?.strategy)
}
private func mergingTarget(_ otherTarget: Configuration.Target?) -> Self {
.init(
target: otherTarget ?? target,
strategy: strategy
)
}
private func mergingStrategy(_ otherStrategy: Configuration.VersionStrategy?) -> Self {
.init(
target: target,
strategy: strategy?.merging(otherStrategy)
)
}
}
@_spi(Internal)
public extension Configuration.PreRelease {
func merging(_ other: Self?) -> Self {
return .init(
prefix: other?.prefix ?? prefix,
strategy: other?.strategy ?? strategy
)
}
}
@_spi(Internal)
public extension Configuration.Branch {
func merging(_ other: Self?) -> Self {
return .init(includeCommitSha: other?.includeCommitSha ?? includeCommitSha)
}
}
@_spi(Internal)
public extension Configuration.SemVar {
func merging(_ other: Self?) -> Self {
.init(
allowPreRelease: other?.allowPreRelease ?? allowPreRelease,
preRelease: preRelease == nil ? other?.preRelease : preRelease!.merging(other?.preRelease),
requireExistingFile: other?.requireExistingFile ?? requireExistingFile,
requireExistingSemVar: other?.requireExistingSemVar ?? requireExistingSemVar,
strategy: other?.strategy ?? strategy
)
}
}
@_spi(Internal)
public extension Configuration.VersionStrategy {
func merging(_ other: Self?) -> Self {
if let otherBranch = other?.branch {
guard let branch else {
return .branch(otherBranch)
}
return .branch(branch.merging(otherBranch))
}
guard let branch else {
guard let semvar else {
return other ?? self
}
return .semvar(semvar.merging(other?.semvar))
}
return .branch(branch.merging(other?.branch))
}
}

View File

@@ -16,6 +16,9 @@ public extension DependencyValues {
@DependencyClient @DependencyClient
public struct ConfigurationClient: Sendable { public struct ConfigurationClient: Sendable {
/// The default file name for a configuration file.
public var defaultFileName: @Sendable () -> String = { "test.json" }
/// Find a configuration file in the given directory or in current working directory. /// Find a configuration file in the given directory or in current working directory.
public var find: @Sendable (URL?) async throws -> URL? public var find: @Sendable (URL?) async throws -> URL?
@@ -32,6 +35,25 @@ public struct ConfigurationClient: Sendable {
} }
return (try? await load(url)) ?? .default return (try? await load(url)) ?? .default
} }
/// Loads configuration from the given path, or searches for the default file and loads it.
/// Optionally merges other configuration, then perform an operation with the loaded configuration.
///
/// - Parameters:
/// - path: Optional file path of the configuration to load.
/// - other: Optional configuration to merge with the loaded configuration.
/// - operation: The operation to perform with the loaded configuration.
@discardableResult
public func withConfiguration<T>(
path: String?,
merging other: Configuration? = nil,
operation: (Configuration) async throws -> T
) async throws -> T {
let configuration = try await findAndLoad(
path != nil ? URL(filePath: path!) : nil
)
return try await operation(configuration.merging(other))
}
} }
extension ConfigurationClient: DependencyKey { extension ConfigurationClient: DependencyKey {
@@ -39,6 +61,7 @@ extension ConfigurationClient: DependencyKey {
public static var liveValue: ConfigurationClient { public static var liveValue: ConfigurationClient {
.init( .init(
defaultFileName: { "\(Constants.defaultFileNameWithoutExtension).json" },
find: { try await findConfiguration($0) }, find: { try await findConfiguration($0) },
load: { try await loadConfiguration($0) }, load: { try await loadConfiguration($0) },
write: { try await writeConfiguration($0, to: $1) } write: { try await writeConfiguration($0, to: $1) }

View File

@@ -5,27 +5,19 @@ import LoggingFormatAndPipe
import Rainbow import Rainbow
import ShellClient import ShellClient
// MARK: Custom colors.
extension String { extension String {
var orange: Self { var orange: Self {
bit24(255, 165, 0) bit24(255, 165, 0)
} }
var magena: Self { var magenta: Self {
// bit24(186, 85, 211)
bit24(238, 130, 238) bit24(238, 130, 238)
} }
} }
@_spi(Internal) extension Logger.Level {
public extension Logger.Level {
init(verbose: Int) {
switch verbose {
case 1: self = .debug
case 2...: self = .trace
default: self = .warning
}
}
var coloredString: String { var coloredString: String {
switch self { switch self {
@@ -45,7 +37,7 @@ public extension Logger.Level {
} }
} }
struct LevelFormatter: LoggingFormatAndPipe.Formatter { private struct LevelFormatter: LoggingFormatAndPipe.Formatter {
let basic: BasicFormatter let basic: BasicFormatter
@@ -138,11 +130,11 @@ struct LevelFormatter: LoggingFormatAndPipe.Formatter {
} }
extension CliClient.LoggingOptions { extension LoggingOptions {
func makeLogger() -> Logger { func makeLogger() -> Logger {
let formatters: [LogComponent] = [ let formatters: [LogComponent] = [
.text(executableName.magena), .text(executableName.magenta),
.text(command.blue), .text(command.blue),
.level, .level,
.group([ .group([
@@ -161,12 +153,4 @@ extension CliClient.LoggingOptions {
} }
} }
func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
try await withDependencies {
$0.logger = makeLogger()
$0.logger.logLevel = .init(verbose: verbose)
} operation: {
try await operation()
}
}
} }

View File

@@ -0,0 +1,13 @@
import Logging
@_spi(Internal)
public extension Logger.Level {
init(verbose: Int) {
switch verbose {
case 1: self = .debug
case 2...: self = .trace
default: self = .warning
}
}
}

View File

@@ -0,0 +1,28 @@
import Dependencies
import ShellClient
public struct LoggingOptions: Equatable, Sendable {
let command: String
let executableName: String
let verbose: Int
public init(
executableName: String = "bump-version",
command: String,
verbose: Int
) {
self.executableName = executableName
self.command = command
self.verbose = verbose
}
public func withLogger<T>(_ operation: () async throws -> T) async rethrows -> T {
try await withDependencies {
$0.logger = makeLogger()
$0.logger.logLevel = .init(verbose: verbose)
} operation: {
try await operation()
}
}
}

View File

@@ -105,6 +105,7 @@ extension ConfigCommand {
} }
} }
// TODO: Add verbose.
@dynamicMemberLookup @dynamicMemberLookup
struct ConfigCommandOptions: ParsableArguments { struct ConfigCommandOptions: ParsableArguments {
@@ -133,7 +134,7 @@ extension ConfigCommand {
private extension ConfigCommand.ConfigCommandOptions { private extension ConfigCommand.ConfigCommandOptions {
func shared(command: String) throws -> CliClient.SharedOptions { func shared(command: String) throws -> CliClient.SharedOptions {
try configOptions.shared(command: command, extraOptions: extraOptions) try configOptions.shared(command: command, extraOptions: extraOptions, verbose: 2)
} }
func handlePrintJson(_ configuration: Configuration) throws { func handlePrintJson(_ configuration: Configuration) throws {

View File

@@ -167,6 +167,7 @@ extension ConfigurationOptions {
) )
} }
// TODO: Need to potentially do something different with passing branch.
func shared( func shared(
command: String, command: String,
dryRun: Bool = true, dryRun: Bool = true,
@@ -180,7 +181,7 @@ extension ConfigurationOptions {
gitDirectory: gitDirectory, gitDirectory: gitDirectory,
loggingOptions: .init(command: command, verbose: verbose), loggingOptions: .init(command: command, verbose: verbose),
target: target(), target: target(),
branch: .init(includeCommitSha: commitSha), branch: semvarOptions.gitTag ? nil : .init(includeCommitSha: commitSha),
semvar: semvarOptions(extraOptions: extraOptions), semvar: semvarOptions(extraOptions: extraOptions),
configurationFile: configurationFile configurationFile: configurationFile
) )