feat: Commit pre integrating configuration client into cli-client.

This commit is contained in:
2024-12-23 14:26:41 -05:00
parent c07a0ef13b
commit 9b60ba0e6c
13 changed files with 347 additions and 31 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "b7e59cc29214df682ee1aa756371133e94660f8aabd03c193d455ba057ed5d17", "originHash" : "a6f314a56cd0c1a50e5cace4aacf75ecda58c4cc00e5e13bf7ec110289f943bf",
"pins" : [ "pins" : [
{ {
"identity" : "combine-schedulers", "identity" : "combine-schedulers",
@@ -46,6 +46,15 @@
"version" : "1.3.1" "version" : "1.3.1"
} }
}, },
{
"identity" : "swift-custom-dump",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-custom-dump.git",
"state" : {
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
"version" : "1.3.3"
}
},
{ {
"identity" : "swift-dependencies", "identity" : "swift-dependencies",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -19,19 +19,22 @@ let package = Package(
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"), .package(url: "https://github.com/apple/swift-log.git", from: "1.6.2"),
.package(url: "https://github.com/LebJe/TOMLKit.git", from: "0.5.0") .package(url: "https://github.com/LebJe/TOMLKit.git", from: "0.5.0"),
.package(url: "https://github.com/pointfreeco/swift-custom-dump.git", from: "1.3.3")
], ],
targets: [ targets: [
.executableTarget( .executableTarget(
name: "cli-version", name: "cli-version",
dependencies: [ dependencies: [
"CliClient", "CliClient",
.product(name: "ArgumentParser", package: "swift-argument-parser") .product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "CustomDump", package: "swift-custom-dump")
] ]
), ),
.target( .target(
name: "CliClient", name: "CliClient",
dependencies: [ dependencies: [
"ConfigurationClient",
"FileClient", "FileClient",
"GitClient", "GitClient",
.product(name: "Logging", package: "swift-log") .product(name: "Logging", package: "swift-log")

View File

@@ -1,3 +1,4 @@
import ConfigurationClient
import Dependencies import Dependencies
import DependenciesMacros import DependenciesMacros
import FileClient import FileClient
@@ -5,6 +6,8 @@ import Foundation
import GitClient import GitClient
import ShellClient import ShellClient
// TODO: Integrate ConfigurationClient
public extension DependencyValues { public extension DependencyValues {
var cliClient: CliClient { var cliClient: CliClient {
@@ -45,6 +48,8 @@ public struct CliClient: Sendable {
case branchAndCommit case branchAndCommit
case semVar(SemVarOptions) case semVar(SemVarOptions)
// public typealias SemVarOptions = Configuration.SemVar
public struct SemVarOptions: Equatable, Sendable { public struct SemVarOptions: Equatable, Sendable {
let preReleaseStrategy: PreReleaseStrategy? let preReleaseStrategy: PreReleaseStrategy?
let requireExistingFile: Bool let requireExistingFile: Bool

View File

@@ -0,0 +1,148 @@
import ConfigurationClient
import Dependencies
import Foundation
import GitClient
extension Configuration.Target {
func url(gitDirectory: String?) throws -> URL {
let filePath: String
if let path {
filePath = path
} else {
guard let module else {
throw ConfigurationParsingError.pathOrModuleNotSet
}
var path = module.name
if !path.hasPrefix("Sources") || !path.hasPrefix("./Sources") {
path = "Sources/\(path)"
}
if path.hasPrefix("./") {
path = String(path.dropFirst(2))
}
filePath = "\(path)/\(module.fileName)"
}
if let gitDirectory {
return URL(filePath: "\(gitDirectory)/\(filePath)")
}
return URL(filePath: filePath)
}
}
extension GitClient {
func version(branch: Configuration.Branch, gitDirectory: String?) async throws -> String {
@Dependency(\.gitClient) var gitClient
return try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .branch(commitSha: branch.includeCommitSha)
)).description
}
}
extension Configuration.PreReleaseStrategy {
func preReleaseString(gitDirectory: String?) async throws -> String {
@Dependency(\.gitClient) var gitClient
let preReleaseString: String
if let branch {
preReleaseString = try await gitClient.version(branch: branch, gitDirectory: gitDirectory)
} else {
preReleaseString = try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .tag(exactMatch: false)
)).description
}
if let prefix {
return "\(prefix)-\(preReleaseString)"
}
return preReleaseString
}
}
@_spi(Internal)
public extension Configuration.SemVar {
private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
guard let preReleaseStrategy = self.preRelease else { return semVar }
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
return semVar.applyingPreRelease(preRelease)
}
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitClient) var gitClient
let fileOutput = try? await fileClient.semVar(file: file, gitDirectory: gitDirectory)
var semVar = fileOutput?.semVar
let usesOptionalType = fileOutput?.usesOptionalType
if requireExistingFile {
guard let semVar else {
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
}
return try await .semVar(
applyingPreRelease(semVar, gitDirectory),
usesOptionalType: usesOptionalType ?? false
)
}
// Didn't have existing semVar loaded from file, so check for git-tag.
semVar = try await gitClient.version(.init(
gitDirectory: gitDirectory,
style: .tag(exactMatch: false)
)).semVar
if requireExistingSemVar {
guard let semVar else {
fatalError()
}
return try await .semVar(
applyingPreRelease(semVar, gitDirectory),
usesOptionalType: usesOptionalType ?? false
)
}
return try await .semVar(
applyingPreRelease(.init(), gitDirectory),
usesOptionalType: usesOptionalType ?? false
)
}
}
extension Configuration.VersionStrategy {
func currentVersion(targetUrl: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
@Dependency(\.gitClient) var gitClient
guard let branch else {
guard let semvar else {
throw ConfigurationParsingError.versionStrategyError(
message: "Neither branch nor semvar set on configuration."
)
}
return try await .init(
targetUrl: targetUrl,
version: semvar.currentVersion(file: targetUrl, gitDirectory: gitDirectory)
)
}
return try await .init(
targetUrl: targetUrl,
version: .string(
gitClient.version(branch: branch, gitDirectory: gitDirectory)
)
)
}
}
enum ConfigurationParsingError: Error {
case pathOrModuleNotSet
case versionStrategyError(message: String)
}

View File

@@ -1,3 +1,4 @@
import ConfigurationClient
import Dependencies import Dependencies
import FileClient import FileClient
import struct Foundation.URL import struct Foundation.URL

View File

@@ -47,7 +47,7 @@ public extension Configuration {
struct Branch: Codable, Equatable, Sendable { struct Branch: Codable, Equatable, Sendable {
/// Include the commit sha in the output for this strategy. /// Include the commit sha in the output for this strategy.
let includeCommitSha: Bool public let includeCommitSha: Bool
/// Create a new branch strategy. /// Create a new branch strategy.
/// ///
@@ -217,10 +217,10 @@ public extension Configuration {
struct VersionStrategy: Codable, Equatable, Sendable { struct VersionStrategy: Codable, Equatable, Sendable {
/// Set if we're using the branch and commit sha to derive the version. /// Set if we're using the branch and commit sha to derive the version.
let branch: Branch? public let branch: Branch?
/// Set if we're using semvar to derive the version. /// Set if we're using semvar to derive the version.
let semvar: SemVar? public let semvar: SemVar?
/// Create a new version strategy that uses branch and commit sha to derive the version. /// Create a new version strategy that uses branch and commit sha to derive the version.
/// ///

View File

@@ -17,13 +17,13 @@ public extension DependencyValues {
public struct ConfigurationClient: Sendable { public struct ConfigurationClient: Sendable {
/// 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 -> ConfigruationFile? public var find: @Sendable (URL?) async throws -> ConfigurationFile?
/// Load a configuration file. /// Load a configuration file.
public var load: @Sendable (ConfigruationFile) async throws -> Configuration public var load: @Sendable (ConfigurationFile) async throws -> Configuration
/// Write a configuration file. /// Write a configuration file.
public var write: @Sendable (Configuration, ConfigruationFile) async throws -> Void public var write: @Sendable (Configuration, ConfigurationFile) async throws -> Void
/// Find a configuration file and load it if found. /// Find a configuration file and load it if found.
public func findAndLoad(_ url: URL? = nil) async throws -> Configuration { public func findAndLoad(_ url: URL? = nil) async throws -> Configuration {
@@ -46,7 +46,7 @@ extension ConfigurationClient: DependencyKey {
} }
} }
private func findConfiguration(_ url: URL?) async throws -> ConfigruationFile? { private func findConfiguration(_ url: URL?) async throws -> ConfigurationFile? {
@Dependency(\.fileClient) var fileClient @Dependency(\.fileClient) var fileClient
var url: URL! = url var url: URL! = url
@@ -55,8 +55,10 @@ private func findConfiguration(_ url: URL?) async throws -> ConfigruationFile? {
} }
// Check if url is a valid configuration url. // Check if url is a valid configuration url.
var configurationFile = ConfigruationFile(url: url) var configurationFile = ConfigurationFile(url: url)
if let configurationFile { return configurationFile } if let configurationFile, fileClient.fileExists(configurationFile.url) {
return configurationFile
}
guard try await fileClient.isDirectory(url.cleanFilePath) else { guard try await fileClient.isDirectory(url.cleanFilePath) else {
throw ConfigurationClientError.invalidConfigurationDirectory(path: url.cleanFilePath) throw ConfigurationClientError.invalidConfigurationDirectory(path: url.cleanFilePath)
@@ -64,13 +66,17 @@ private func findConfiguration(_ url: URL?) async throws -> ConfigruationFile? {
// Check for toml file. // Check for toml file.
let tomlUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).toml") let tomlUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).toml")
configurationFile = ConfigruationFile(url: tomlUrl) configurationFile = ConfigurationFile(url: tomlUrl)
if let configurationFile { return configurationFile } if let configurationFile, fileClient.fileExists(configurationFile.url) {
return configurationFile
}
// Check for json file. // Check for json file.
let jsonUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).json") let jsonUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).json")
configurationFile = ConfigruationFile(url: jsonUrl) configurationFile = ConfigurationFile(url: jsonUrl)
if let configurationFile { return configurationFile } if let configurationFile, fileClient.fileExists(configurationFile.url) {
return configurationFile
}
// Couldn't find valid configuration file. // Couldn't find valid configuration file.
return nil return nil

View File

@@ -3,7 +3,7 @@ import FileClient
import Foundation import Foundation
/// Represents a configuration file type and location. /// Represents a configuration file type and location.
public enum ConfigruationFile: Equatable, Sendable { public enum ConfigurationFile: Equatable, Sendable {
/// A json configuration file. /// A json configuration file.
case json(URL) case json(URL)
@@ -34,7 +34,7 @@ public enum ConfigruationFile: Equatable, Sendable {
} }
/// The url of the file. /// The url of the file.
var url: URL { public var url: URL {
switch self { switch self {
case let .json(url): return url case let .json(url): return url
case let .toml(url): return url case let .toml(url): return url
@@ -42,7 +42,7 @@ public enum ConfigruationFile: Equatable, Sendable {
} }
} }
extension ConfigruationFile { extension ConfigurationFile {
func load() async throws -> Configuration? { func load() async throws -> Configuration? {
@Dependency(\.coders) var coders @Dependency(\.coders) var coders

View File

@@ -9,7 +9,8 @@ struct CliVersionCommand: AsyncParsableCommand {
subcommands: [ subcommands: [
Build.self, Build.self,
Bump.self, Bump.self,
Generate.self Generate.self,
UtilsCommand.self
], ],
defaultSubcommand: Bump.self defaultSubcommand: Bump.self
) )

View File

@@ -1,5 +1,6 @@
import ArgumentParser import ArgumentParser
@_spi(Internal) import CliClient @_spi(Internal) import CliClient
import ConfigurationClient
import Dependencies import Dependencies
import Foundation import Foundation
import Rainbow import Rainbow
@@ -173,6 +174,16 @@ private extension TargetOptions {
} }
struct InvalidTargetOption: Error {} struct InvalidTargetOption: Error {}
func configTarget() throws -> Configuration.Target? {
guard let path else {
guard let module else {
return nil
}
return .init(module: .init(module, fileName: fileName))
}
return .init(path: path)
}
} }
extension PreReleaseOptions { extension PreReleaseOptions {
@@ -197,6 +208,25 @@ extension PreReleaseOptions {
} }
} }
func configPreReleaseStrategy() throws -> Configuration.PreReleaseStrategy? {
guard let custom else {
if useBranchAsPreRelease {
return .branch()
} else if useTagAsPreRelease {
return .gitTag
} else {
return nil
}
}
if useBranchAsPreRelease {
return .customBranchPrefix(custom)
} else if useTagAsPreRelease {
return .customGitTagPrefix(custom)
} else {
return .custom(custom)
}
}
} }
extension SemVarOptions { extension SemVarOptions {
@@ -207,4 +237,13 @@ extension SemVarOptions {
requireExistingSemVar: requireExistingSemvar requireExistingSemVar: requireExistingSemvar
) )
} }
func configSemVarOptions() throws -> Configuration.SemVar {
try .init(
preRelease: preRelease.configPreReleaseStrategy(),
requireExistingFile: requireExistingFile,
requireExistingSemVar: requireExistingSemvar
)
}
} }

View File

@@ -0,0 +1,48 @@
import ArgumentParser
import ConfigurationClient
import CustomDump
import Dependencies
import FileClient
import Foundation
struct UtilsCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "utils",
abstract: "Utility commands",
subcommands: [
DumpConfig.self
]
)
}
extension UtilsCommand {
struct DumpConfig: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "dump-config",
abstract: "Show the parsed configuration."
)
@Argument(
help: """
Optional path to the configuration file, if not supplied will search the current directory
""",
completion: .file(extensions: ["toml", "json"])
)
var file: String?
func run() async throws {
try await withDependencies {
$0.fileClient = .liveValue
$0.configurationClient = .liveValue
} operation: {
@Dependency(\.configurationClient) var configurationClient
let configuration = try await configurationClient.findAndLoad(
file != nil ? URL(filePath: file!) : nil
)
customDump(configuration)
}
}
}
}

View File

@@ -29,16 +29,16 @@ final class GitVersionTests: XCTestCase {
.cleanFilePath .cleanFilePath
} }
#if !os(Linux) // #if !os(Linux)
func test_live() async throws { // func test_live() async throws {
@Dependency(\.gitClient) var versionClient: GitClient // @Dependency(\.gitClient) var versionClient: GitClient
//
let version = try await versionClient.currentVersion(in: gitDir) // let version = try await versionClient.currentVersion(in: gitDir)
print("VERSION: \(version)") // print("VERSION: \(version)")
// can't really have a predictable result for the live client. // // can't really have a predictable result for the live client.
XCTAssertNotEqual(version, "blob") // XCTAssertNotEqual(version, "blob")
} // }
#endif // #endif
func test_file_client() async throws { func test_file_client() async throws {
try await withTemporaryDirectory { tmpDir in try await withTemporaryDirectory { tmpDir in

View File

@@ -2,6 +2,7 @@ import ConfigurationClient
import Dependencies import Dependencies
import Foundation import Foundation
import Testing import Testing
import TestSupport
@Suite("ConfigurationClientTests") @Suite("ConfigurationClientTests")
struct ConfigurationClientTests { struct ConfigurationClientTests {
@@ -30,7 +31,7 @@ struct ConfigurationClientTests {
@Test(arguments: ["foo", ".foo"]) @Test(arguments: ["foo", ".foo"])
func configurationFile(fileName: String) { func configurationFile(fileName: String) {
for ext in ["toml", "json", "bar"] { for ext in ["toml", "json", "bar"] {
let file = ConfigruationFile(url: URL(filePath: "\(fileName).\(ext)")) let file = ConfigurationFile(url: URL(filePath: "\(fileName).\(ext)"))
switch ext { switch ext {
case "toml": case "toml":
#expect(file == .toml(URL(filePath: "\(fileName).toml"))) #expect(file == .toml(URL(filePath: "\(fileName).toml")))
@@ -42,6 +43,61 @@ struct ConfigurationClientTests {
} }
} }
@Test
func writeAndLoad() async throws {
try await withTemporaryDirectory { url in
try await run {
@Dependency(\.configurationClient) var configurationClient
for ext in ["toml", "json"] {
let fileUrl = url.appending(path: "test.\(ext)")
let configuration = Configuration.mock
let configurationFile = ConfigurationFile(url: fileUrl)!
try await configurationClient.write(configuration, configurationFile)
let loaded = try await configurationClient.load(configurationFile)
#expect(loaded == configuration)
let findAndLoaded = try await configurationClient.findAndLoad(configurationFile.url)
#expect(findAndLoaded == configuration)
try FileManager.default.removeItem(at: fileUrl)
}
}
}
}
@Test
func findAndLoad() async throws {
try await withTemporaryDirectory { url in
try await run {
@Dependency(\.configurationClient) var configurationClient
let shouldBeNil = try await configurationClient.find(url)
#expect(shouldBeNil == nil)
do {
_ = try await configurationClient.findAndLoad(url)
#expect(Bool(false))
} catch {
#expect(Bool(true))
}
for ext in ["toml", "json"] {
let fileUrl = url.appending(path: ".bump-version.\(ext)")
let configuration = Configuration.mock
let configurationFile = ConfigurationFile(url: fileUrl)!
try await configurationClient.write(configuration, configurationFile)
let loaded = try await configurationClient.findAndLoad(url)
#expect(loaded == configuration)
try FileManager.default.removeItem(at: fileUrl)
}
}
}
}
func run( func run(
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in }, setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
operation: () async throws -> Void operation: () async throws -> Void