feat: Begins update for more modern swift-dependencies implementation.

This commit is contained in:
2024-12-20 12:54:10 -05:00
parent 1885a90f62
commit 847ddbc7b5
19 changed files with 439 additions and 130 deletions

View File

@@ -0,0 +1,222 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import Dependencies
import DependenciesMacros
import ShellClient
public extension DependencyValues {
var cliClient: CliClient {
get { self[CliClient.self] }
set { self[CliClient.self] = newValue }
}
}
@DependencyClient
public struct CliClient {
public var build: @Sendable (BuildOptions) throws -> String
public var generate: @Sendable (GenerateOptions) throws -> String
public var update: @Sendable (UpdateOptions) throws -> String
}
extension CliClient: DependencyKey {
public static let testValue: CliClient = Self()
public static func live(environment: [String: String]) -> Self {
.init(
build: { try $0.run(environment) },
generate: { try $0.run() },
update: { try $0.run() }
)
}
public static var liveValue: CliClient {
.live(environment: ProcessInfo.processInfo.environment)
}
}
public extension CliClient {
// TODO: Use Int for `verbose`.
struct SharedOptions: Sendable {
let dryRun: Bool
let fileName: String
let target: String
let verbose: Bool
public init(
dryRun: Bool = false,
fileName: String,
target: String,
verbose: Bool = true
) {
self.dryRun = dryRun
self.fileName = fileName
self.target = target
self.verbose = verbose
}
}
struct BuildOptions: Sendable {
let gitDirectory: String?
let shared: SharedOptions
public init(
gitDirectory: String? = nil,
shared: SharedOptions
) {
self.gitDirectory = gitDirectory
self.shared = shared
}
}
struct GenerateOptions: Sendable {
let shared: SharedOptions
public init(shared: SharedOptions) {
self.shared = shared
}
}
struct UpdateOptions: Sendable {
let gitDirectory: String?
let shared: SharedOptions
public init(
gitDirectory: String? = nil,
shared: SharedOptions
) {
self.gitDirectory = gitDirectory
self.shared = shared
}
}
}
// MARK: Private
@_spi(Internal)
public extension CliClient.SharedOptions {
var fileUrl: URL {
url(for: target).appendingPathComponent(fileName)
}
func parseTarget() throws -> URL {
let targetUrl = fileUrl
.deletingLastPathComponent()
.deletingLastPathComponent()
guard targetUrl.lastPathComponent == "Sources" else {
return url(for: "Sources")
.appendingPathComponent(target)
.appendingPathComponent(fileName)
}
return fileUrl
}
@discardableResult
func run<T>(
_ operation: () throws -> T
) rethrows -> T {
try withDependencies {
$0.logger.logLevel = .init(verbose: verbose)
} operation: {
try operation()
}
}
}
private extension CliClient.BuildOptions {
func run(_ environment: [String: String]) throws -> String {
try shared.run {
@Dependency(\.gitVersionClient) var gitVersion
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
let gitDirectory = gitDirectory ?? environment["PWD"]
guard let gitDirectory else {
throw CliClientError.gitDirectoryNotFound
}
logger.debug("Building with git directory: \(gitDirectory)")
let fileUrl = shared.fileUrl
logger.debug("File url: \(fileUrl.cleanFilePath)")
let currentVersion = try gitVersion.currentVersion(in: gitDirectory)
let fileContents = buildTemplate
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
try fileClient.write(string: fileContents, to: fileUrl)
return fileUrl.cleanFilePath
}
}
}
private extension CliClient.GenerateOptions {
func run() throws -> String {
@Dependency(\.fileClient) var fileClient
@Dependency(\.logger) var logger
let targetUrl = try shared.parseTarget()
logger.debug("Generate target url: \(targetUrl.cleanFilePath)")
guard !fileClient.fileExists(targetUrl) else {
throw CliClientError.fileExists(path: targetUrl.cleanFilePath)
}
if !shared.dryRun {
try fileClient.write(string: optionalTemplate, to: targetUrl)
} else {
logger.debug("Skipping, due to dry-run being passed.")
}
return targetUrl.cleanFilePath
}
}
private extension CliClient.UpdateOptions {
func run() throws -> String {
@Dependency(\.fileClient) var fileClient
@Dependency(\.gitVersionClient) var gitVersionClient
@Dependency(\.logger) var logger
let targetUrl = try shared.parseTarget()
logger.debug("Target url: \(targetUrl.cleanFilePath)")
let currentVersion = try gitVersionClient.currentVersion(in: gitDirectory)
let fileContents = optionalTemplate
.replacingOccurrences(of: "nil", with: "\"\(currentVersion)\"")
if !shared.dryRun {
try fileClient.write(string: fileContents, to: targetUrl)
} else {
logger.debug("Skipping due to dry run being passed.")
logger.debug("Parsed version: \(currentVersion)")
}
return targetUrl.cleanFilePath
}
}
private let optionalTemplate = """
// Do not set this variable, it is set during the build process.
let VERSION: String? = nil
"""
private let buildTemplate = """
// Do not set this variable, it is set during the build process.
let VERSION: String = nil
"""
enum CliClientError: Error {
case gitDirectoryNotFound
case fileExists(path: String)
}

View File

@@ -1,10 +1,21 @@
import Dependencies
import DependenciesMacros
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import XCTestDynamicOverlay
public extension DependencyValues {
/// Access a basic ``FileClient`` that can read / write data to the file system.
///
var fileClient: FileClient {
get { self[FileClient.self] }
set { self[FileClient.self] = newValue }
}
}
/// Represents the interactions with the file system. It is able
/// to read from and write to files.
///
@@ -13,25 +24,23 @@ import XCTestDynamicOverlay
/// @Dependency(\.fileClient) var fileClient
/// ```
///
public struct FileClient {
@DependencyClient
public struct FileClient: Sendable {
public var fileExists: @Sendable (URL) -> Bool = { _ in true }
/// Read the contents of a file.
public var read: @Sendable (URL) throws -> String
/// Write `Data` to a file `URL`.
public private(set) var write: (Data, URL) throws -> Void
public private(set) var write: @Sendable (Data, URL) throws -> Void
/// Create a new ``GitVersion/FileClient`` instance.
///
/// This is generally not interacted with directly, instead access as a dependency.
///
///```swift
/// @Dependency(\.fileClient) var fileClient
///```
/// Read the contents of a file at the given path.
///
/// - Parameters:
/// - write: Write the data to a file.
public init(
write: @escaping (Data, URL) throws -> Void
) {
self.write = write
/// - path: The file path to read from.
public func read(_ path: String) throws -> String {
try read(url(for: path))
}
/// Write's the the string to a file path.
@@ -40,8 +49,8 @@ public struct FileClient {
/// - string: The string to write to the file.
/// - path: The file path.
public func write(string: String, to path: String) throws {
let url = try url(for: path)
try self.write(string: string, to: url)
let url = url(for: path)
try write(string: string, to: url)
}
/// Write's the the string to a file path.
@@ -50,49 +59,27 @@ public struct FileClient {
/// - string: The string to write to the file.
/// - url: The file url.
public func write(string: String, to url: URL) throws {
try self.write(Data(string.utf8), url)
try write(Data(string.utf8), url)
}
}
extension FileClient: DependencyKey {
/// A ``FileClient`` that does not do anything.
public static let noop = FileClient.init(
public static let noop = FileClient(
fileExists: { _ in true },
read: { _ in "" },
write: { _, _ in }
)
/// An `unimplemented` ``FileClient``.
public static let testValue = FileClient(
write: unimplemented("\(Self.self).write")
)
public static let testValue = FileClient()
/// The live ``FileClient``
public static let liveValue = FileClient(
fileExists: { FileManager.default.fileExists(atPath: $0.cleanFilePath) },
read: { try String(contentsOf: $0, encoding: .utf8) },
write: { try $0.write(to: $1, options: .atomic) }
)
}
extension DependencyValues {
/// Access a basic ``FileClient`` that can read / write data to the file system.
///
public var fileClient: FileClient {
get { self[FileClient.self] }
set { self[FileClient.self] = newValue }
}
}
// MARK: - Private
fileprivate func url(for path: String) throws -> URL {
#if os(Linux)
return URL(fileURLWithPath: path)
#else
if #available(macOS 13.0, *) {
return URL(filePath: path)
} else {
// Fallback on earlier versions
return URL(fileURLWithPath: path)
}
#endif
}

View File

@@ -40,7 +40,7 @@ public struct GitVersionClient {
/// - Parameters:
/// - gitDirectory: The directory to run the command in.
public func currentVersion(in gitDirectory: String? = nil) throws -> String {
try self.currentVersion(gitDirectory)
try currentVersion(gitDirectory)
}
/// Override the `currentVersion` command and return the passed in version string.
@@ -50,7 +50,7 @@ public struct GitVersionClient {
/// - Parameters:
/// - version: The version string to return when `currentVersion` is called.
public mutating func override(with version: String) {
self.currentVersion = { _ in version }
currentVersion = { _ in version }
}
}
@@ -69,32 +69,33 @@ extension GitVersionClient: TestDependencyKey {
}
}
extension DependencyValues {
public extension DependencyValues {
/// A ``GitVersionClient`` that can retrieve the current version from a
/// git directory.
public var gitVersionClient: GitVersionClient {
var gitVersionClient: GitVersionClient {
get { self[GitVersionClient.self] }
set { self[GitVersionClient.self] = newValue }
}
}
extension ShellCommand {
public static func gitCurrentSha(gitDirectory: String? = nil) -> Self {
public extension ShellCommand {
static func gitCurrentSha(gitDirectory: String? = nil) -> Self {
GitVersion(workingDirectory: gitDirectory).command(for: .commit)
}
public static func gitCurrentBranch(gitDirectory: String? = nil) -> Self {
static func gitCurrentBranch(gitDirectory: String? = nil) -> Self {
GitVersion(workingDirectory: gitDirectory).command(for: .branch)
}
public static func gitCurrentTag(gitDirectory: String? = nil) -> Self {
static func gitCurrentTag(gitDirectory: String? = nil) -> Self {
GitVersion(workingDirectory: gitDirectory).command(for: .describe)
}
}
// MARK: - Private
fileprivate struct GitVersion {
private struct GitVersion {
@Dependency(\.logger) var logger: Logger
@Dependency(\.shellClient) var shell: ShellClient
@@ -113,7 +114,7 @@ fileprivate struct GitVersion {
}
}
internal func command(for argument: VersionArgs) -> ShellCommand {
func command(for argument: VersionArgs) -> ShellCommand {
.init(
shell: .env,
environment: nil,
@@ -123,7 +124,7 @@ fileprivate struct GitVersion {
}
}
fileprivate extension GitVersion {
private extension GitVersion {
func run(command: ShellCommand) throws -> String {
try shell.background(command, trimmingCharactersIn: .whitespacesAndNewlines)
}

View File

@@ -0,0 +1,40 @@
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import Logging
@_spi(Internal)
public extension Logger.Level {
init(verbose: Bool) {
if verbose {
self = .debug
} else {
self = .info
}
}
}
@_spi(Internal)
public extension URL {
var cleanFilePath: String {
absoluteString.replacingOccurrences(of: "file://", with: "")
}
}
// MARK: - Private
@_spi(Internal)
public func url(for path: String) -> URL {
#if os(Linux)
return URL(fileURLWithPath: path)
#else
if #available(macOS 13.0, *) {
return URL(filePath: path)
} else {
// Fallback on earlier versions
return URL(fileURLWithPath: path)
}
#endif
}