feat: Integrates cli-client dependency into cli-version executable

This commit is contained in:
2024-12-21 13:14:31 -05:00
parent ba7d63606c
commit 7959ec274b
14 changed files with 210 additions and 207 deletions

View File

@@ -22,7 +22,7 @@ struct GenerateVersionBuildPlugin: BuildToolPlugin {
.buildCommand(
displayName: "Build With Version Plugin",
executable: tool.path,
arguments: ["build", "--verbose", "--git-directory", gitDirectoryPath.string, outputPath.string],
arguments: ["build", "--verbose", "--git-directory", gitDirectoryPath.string, "--target", outputPath.string],
environment: [:],
inputFiles: target.sourceFiles.map(\.path),
outputFiles: [outputFile]

View File

@@ -1,5 +1,5 @@
import PackagePlugin
import Foundation
import PackagePlugin
@main
struct GenerateVersionPlugin: CommandPlugin {
@@ -27,4 +27,3 @@ struct GenerateVersionPlugin: CommandPlugin {
}
}
}

View File

@@ -34,26 +34,26 @@ public struct CliClient: Sendable {
case major, minor, patch
}
// TODO: Use Int for `verbose`.
public struct SharedOptions: Equatable, Sendable {
let gitDirectory: String?
let dryRun: Bool
let fileName: String
let gitDirectory: String?
let logLevel: Logger.Level
let target: String
let verbose: Bool
public init(
gitDirectory: String? = nil,
dryRun: Bool = false,
fileName: String = "Version.swift",
target: String,
verbose: Bool = true
logLevel: Logger.Level = .debug
) {
self.gitDirectory = gitDirectory
self.dryRun = dryRun
self.fileName = fileName
self.target = target
self.verbose = verbose
self.logLevel = logLevel
}
}
@@ -101,7 +101,7 @@ public extension CliClient.SharedOptions {
_ operation: () async throws -> T
) async rethrows -> T {
try await withDependencies {
$0.logger.logLevel = .init(verbose: verbose)
$0.logger.logLevel = logLevel
} operation: {
try await operation()
}
@@ -114,6 +114,7 @@ public extension CliClient.SharedOptions {
try await fileClient.write(string: string, to: url)
} else {
logger.debug("Skipping, due to dry-run being passed.")
logger.debug("\(string)")
}
}
}
@@ -147,7 +148,7 @@ private extension CliClient.SharedOptions {
}
}
private func getVersionString() async throws -> (String, Bool) {
private func getVersionString() async throws -> (version: String, usesOptionalType: Bool) {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitVersionClient) var gitVersionClient
@Dependency(\.logger) var logger
@@ -290,6 +291,10 @@ public struct Template: Sendable {
}
public static func build(_ version: String? = nil) -> String {
nonOptional(version)
}
public static func nonOptional(_ version: String? = nil) -> String {
Self(type: .string, version: version).value
}

View File

@@ -7,11 +7,12 @@ import Logging
@_spi(Internal)
public extension Logger.Level {
init(verbose: Bool) {
if verbose {
self = .debug
} else {
self = .info
init(verbose: Int) {
switch verbose {
case 1: self = .warning
case 2: self = .debug
case 3...: self = .trace
default: self = .info
}
}
}

View File

@@ -10,40 +10,13 @@ extension CliVersionCommand {
discussion: "This should generally not be interacted with directly, outside of the build plugin."
)
@OptionGroup var shared: SharedOptions
@OptionGroup var globals: GlobalOptions
@Option(
name: .customLong("git-directory"),
help: "The git directory for the version."
)
var gitDirectory: String
// TODO: Use CliClient
func run() async throws {
try await withDependencies {
$0.logger.logLevel = .debug
$0.fileClient = .liveValue
$0.gitVersionClient = .liveValue
} operation: {
@Dependency(\.gitVersionClient) var gitVersion
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger: Logger
logger.info("Building with git-directory: \(gitDirectory)")
let fileUrl = URL(fileURLWithPath: shared.target)
.appendingPathComponent(shared.fileName)
let fileString = fileUrl.fileString()
logger.info("File Url: \(fileString)")
let currentVersion = try await gitVersion.currentVersion(in: gitDirectory)
let fileContents = buildTemplate
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
try await fileClient.write(string: fileContents, to: fileUrl)
logger.info("Updated version file: \(fileString)")
try await globals.run {
@Dependency(\.cliClient) var cliClient
let output = try await cliClient.build(globals.shared)
print(output)
}
}
}

View File

@@ -0,0 +1,28 @@
import ArgumentParser
import CliVersion
import Dependencies
extension CliVersionCommand {
struct Bump: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "bump",
abstract: "Bump version of a command-line tool."
)
@OptionGroup var globals: GlobalOptions
@Flag
var bumpOption: CliClient.BumpOption = .patch
func run() async throws {
try await globals.run {
@Dependency(\.cliClient) var cliClient
let output = try await cliClient.bump(bumpOption, globals.shared)
print(output)
}
}
}
}
extension CliClient.BumpOption: EnumerableFlag {}

View File

@@ -8,6 +8,7 @@ struct CliVersionCommand: AsyncParsableCommand {
version: VERSION ?? "0.0.0",
subcommands: [
Build.self,
Bump.self,
Generate.self,
Update.self
]

View File

@@ -12,28 +12,13 @@ extension CliVersionCommand {
version: VERSION ?? "0.0.0"
)
@OptionGroup var shared: SharedOptions
@OptionGroup var globals: GlobalOptions
// TODO: Use CliClient
func run() async throws {
@Dependency(\.logger) var logger: Logger
@Dependency(\.fileClient) var fileClient
let targetUrl = parseTarget(shared.target)
let fileUrl = targetUrl.appendingPathComponent(shared.fileName)
let fileString = fileUrl.fileString()
guard !FileManager.default.fileExists(atPath: fileUrl.absoluteString) else {
logger.info("File already exists at path.")
throw GenerationError.fileExists(path: fileString)
}
if !shared.dryRun {
try await fileClient.write(string: optionalTemplate, to: fileUrl)
logger.info("Generated file at: \(fileString)")
} else {
logger.info("Would generate file at: \(fileString)")
try await globals.run {
@Dependency(\.cliClient) var cliClient
let output = try await cliClient.generate(globals.shared)
print(output)
}
}
}

View File

@@ -0,0 +1,88 @@
import ArgumentParser
@_spi(Internal) import CliVersion
import Dependencies
import Foundation
func parseTarget(_ target: String) -> URL {
let url = URL(fileURLWithPath: target)
let urlTest = url
.deletingLastPathComponent()
guard urlTest.lastPathComponent == "Sources" else {
return URL(fileURLWithPath: "Sources")
.appendingPathComponent(target)
}
return url
}
extension URL {
func fileString() -> String {
absoluteString
.replacingOccurrences(of: "file://", with: "")
}
}
let optionalTemplate = """
// Do not set this variable, it is set during the build process.
let VERSION: String? = nil
"""
let buildTemplate = """
// Do not set this variable, it is set during the build process.
let VERSION: String = nil
"""
struct GlobalOptions: ParsableArguments {
@Option(
name: .customLong("git-directory"),
help: "The git directory for the version (default: current directory)"
)
var gitDirectory: String?
@Option(
name: .shortAndLong,
help: "The target for the version file."
)
var target: String
@Option(
name: .customLong("filename"),
help: "Specify the file name for the version file in the target."
)
var fileName: String = "Version.swift"
@Flag(name: .customLong("dry-run"))
var dryRun: Bool = false
@Flag(
name: .shortAndLong,
help: "Increase logging level, can be passed multiple times (example: -vvv)."
)
var verbose: Int
}
extension GlobalOptions {
var shared: CliClient.SharedOptions {
.init(
gitDirectory: gitDirectory,
dryRun: dryRun,
fileName: fileName,
target: target,
logLevel: .init(verbose: verbose)
)
}
func run(_ operation: () async throws -> Void) async throws {
try await withDependencies {
$0.fileClient = .liveValue
$0.gitVersionClient = .liveValue
$0.cliClient = .liveValue
} operation: {
try await operation()
}
}
}

View File

@@ -1,52 +0,0 @@
import ArgumentParser
import Foundation
func parseTarget(_ target: String) -> URL {
let url = URL(fileURLWithPath: target)
let urlTest = url
.deletingLastPathComponent()
guard urlTest.lastPathComponent == "Sources" else {
return URL(fileURLWithPath: "Sources")
.appendingPathComponent(target)
}
return url
}
extension URL {
func fileString() -> String {
absoluteString
.replacingOccurrences(of: "file://", with: "")
}
}
let optionalTemplate = """
// Do not set this variable, it is set during the build process.
let VERSION: String? = nil
"""
let buildTemplate = """
// Do not set this variable, it is set during the build process.
let VERSION: String = nil
"""
// TODO: Use Int for `verbose`.
struct SharedOptions: ParsableArguments {
@Argument(help: "The target for the version file.")
var target: String
@Option(
name: .customLong("filename"),
help: "Specify the file name for the version file."
)
var fileName: String = "Version.swift"
@Flag(name: .customLong("dry-run"))
var dryRun: Bool = false
@Flag(name: .long, help: "Increase logging level.")
var verbose: Bool = false
}

View File

@@ -12,45 +12,13 @@ extension CliVersionCommand {
discussion: "This command can be interacted with directly outside of the plugin context."
)
@OptionGroup var shared: SharedOptions
@OptionGroup var globals: GlobalOptions
@Option(
name: .customLong("git-directory"),
help: "The git directory for the version."
)
var gitDirectory: String?
// TODO: Use CliClient
func run() async throws {
try await withDependencies {
$0.logger.logLevel = shared.verbose ? .debug : .info
$0.fileClient = .liveValue
$0.gitVersionClient = .liveValue
$0.asyncShellClient = .liveValue
} operation: {
@Dependency(\.gitVersionClient) var gitVersion
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
@Dependency(\.asyncShellClient) var shell
let targetUrl = parseTarget(shared.target)
let fileUrl = targetUrl
.appendingPathComponent(shared.fileName)
let fileString = fileUrl.fileString()
let currentVersion = try await gitVersion.currentVersion(in: gitDirectory)
let fileContents = optionalTemplate
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
if !shared.dryRun {
try await fileClient.write(string: fileContents, to: fileUrl)
logger.info("Updated version file: \(fileString)")
} else {
logger.info("Would update file contents to:")
logger.info("\(fileContents)")
}
try await globals.run {
@Dependency(\.cliClient) var cliClient
let output = try await cliClient.update(globals.shared)
print(output)
}
}
}

View File

@@ -1,2 +1,2 @@
// Do not set this variable, it is set during the build process.
let VERSION: String? = "0.1.0"
let VERSION: String? = "0.1.1"

View File

@@ -1,6 +1,7 @@
@_spi(Internal) import CliVersion
import Dependencies
import Foundation
import Logging
import Testing
import TestSupport
@@ -12,16 +13,8 @@ struct CliClientTests {
)
func testBuild(target: String) async throws {
try await run {
let client = CliClient.liveValue
let output = try await client.build(.init(
gitDirectory: "/baz",
dryRun: false,
fileName: "foo",
target: target,
verbose: true
))
@Dependency(\.cliClient) var client
let output = try await client.build(.testOptions(target: target))
#expect(output == "/baz/Sources/bar/foo")
}
}
@@ -35,27 +28,21 @@ struct CliClientTests {
$0.fileClient.fileExists = { _ in true }
$0.fileClient.read = { @Sendable _ in template }
} operation: {
let client = CliClient.liveValue
let output = try await client.bump(
type,
.init(
gitDirectory: "/baz",
dryRun: false,
fileName: "foo",
target: "bar",
verbose: true
)
)
@Dependency(\.cliClient) var client
let output = try await client.bump(type, .testOptions())
#expect(output == "/baz/Sources/bar/foo")
} assert: { string, _ in
#expect(string != nil)
let typeString = optional ? "String?" : "String"
switch type {
case .major:
#expect(string.contains("let VERSION: \(typeString) = \"2.0.0\""))
#expect(string!.contains("let VERSION: \(typeString) = \"2.0.0\""))
case .minor:
#expect(string.contains("let VERSION: \(typeString) = \"1.1.0\""))
#expect(string!.contains("let VERSION: \(typeString) = \"1.1.0\""))
case .patch:
#expect(string.contains("let VERSION: \(typeString) = \"1.0.1\""))
#expect(string!.contains("let VERSION: \(typeString) = \"1.0.1\""))
}
}
}
@@ -64,42 +51,34 @@ struct CliClientTests {
arguments: TestArguments.testCases
)
func generate(target: String) async throws {
// let (stream, continuation) = AsyncStream<Data>.makeStream()
try await run {
let client = CliClient.liveValue
let output = try await client.generate(.init(
gitDirectory: "/baz",
dryRun: false,
fileName: "foo",
target: target,
verbose: true
))
@Dependency(\.cliClient) var client
let output = try await client.generate(.testOptions(target: target))
#expect(output == "/baz/Sources/bar/foo")
}
}
@Test(
arguments: TestArguments.testCases
arguments: TestArguments.updateCases
)
func update(target: String) async throws {
// let (stream, continuation) = AsyncStream<Data>.makeStream()
func update(target: String, dryRun: Bool) async throws {
try await run {
let client = CliClient.liveValue
let output = try await client.update(.init(
gitDirectory: "/baz",
dryRun: false,
fileName: "foo",
target: target,
verbose: true
))
$0.fileClient.fileExists = { _ in false }
} operation: {
@Dependency(\.cliClient) var client
let output = try await client.update(.testOptions(dryRun: dryRun, target: target))
#expect(output == "/baz/Sources/bar/foo")
} assert: { string, _ in
if dryRun {
#expect(string == nil)
}
}
}
func run(
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
operation: @Sendable @escaping () async throws -> Void,
assert: @escaping (String, URL) -> Void = { _, _ in }
assert: @escaping (String?, URL?) -> Void = { _, _ in }
) async throws {
let captured = CapturingWrite()
@@ -108,18 +87,17 @@ struct CliClientTests {
$0.fileClient = .capturing(captured)
$0.fileClient.fileExists = { _ in false }
$0.gitVersionClient = .init { _, _ in "1.0.0" }
$0.cliClient = .liveValue
setupDependencies(&$0)
} operation: {
try await operation()
}
let data = await captured.data
let url = await captured.url
var string: String?
guard let data,
let string = String(bytes: data, encoding: .utf8),
let url
else {
throw TestError()
if let data {
string = String(bytes: data, encoding: .utf8)
}
assert(string, url)
@@ -132,6 +110,26 @@ enum TestArguments {
$0.append(($1, true))
$0.append(($1, false))
}
static let updateCases = testCases.map { ($0, Bool.random()) }
}
struct TestError: Error {}
extension CliClient.SharedOptions {
static func testOptions(
gitDirectory: String? = "/baz",
dryRun: Bool = false,
fileName: String = "foo",
target: String = "bar",
logLevel: Logger.Level = .trace
) -> Self {
.init(
gitDirectory: gitDirectory,
dryRun: dryRun,
fileName: fileName,
target: target,
logLevel: logLevel
)
}
}

9
justfile Normal file
View File

@@ -0,0 +1,9 @@
build configuration="release":
@swift build --configuration {{configuration}}
run *ARGS:
@swift run cli-version {{ARGS}}
clean:
rm -rf .build