feat: Begin working on configuration client.

This commit is contained in:
2024-12-22 23:30:25 -05:00
parent 9c62c06ebe
commit d716348088
24 changed files with 958 additions and 409 deletions

View File

@@ -0,0 +1,38 @@
import Dependencies
import DependenciesMacros
import Foundation
import TOMLKit
public extension DependencyValues {
var coders: Coders {
get { self[Coders.self] }
set { self[Coders.self] = newValue }
}
}
@DependencyClient
public struct Coders: Sendable {
public var jsonDecoder: @Sendable () -> JSONDecoder = { .init() }
public var jsonEncoder: @Sendable () -> JSONEncoder = { .init() }
public var tomlDecoder: @Sendable () -> TOMLDecoder = { .init() }
public var tomlEncoder: @Sendable () -> TOMLEncoder = { .init() }
}
extension Coders: DependencyKey {
public static var testValue: Coders {
.init(
jsonDecoder: { .init() },
jsonEncoder: { defaultJsonEncoder },
tomlDecoder: { .init() },
tomlEncoder: { .init() }
)
}
public static var liveValue: Coders { .testValue }
private static let defaultJsonEncoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
return encoder
}()
}

View File

@@ -0,0 +1,75 @@
import Foundation
public struct Configuration: Codable, Sendable {
public let target: Target?
public let strategy: VersionStrategy?
public init(
target: Target? = nil,
strategy: VersionStrategy? = .semvar()
) {
self.target = target
self.strategy = strategy
}
}
public extension Configuration {
enum VersionStrategy: Codable, Equatable, Sendable {
case branch(Branch = .init())
case semvar(SemVar = .init())
public struct Branch: Codable, Equatable, Sendable {
let includeCommitSha: Bool
public init(includeCommitSha: Bool = true) {
self.includeCommitSha = includeCommitSha
}
}
public enum PreReleaseStrategy: Codable, Equatable, Sendable {
/// Use output of tag, with branch and commit sha.
case branch(Branch = .init())
/// Provide a custom pre-release tag.
indirect case custom(String, PreReleaseStrategy? = nil)
/// Use the output of `git describe --tags`
case gitTag
}
public struct SemVar: Codable, Equatable, Sendable {
let preReleaseStrategy: PreReleaseStrategy?
let requireExistingFile: Bool
let requireExistingSemVar: Bool
public init(
preReleaseStrategy: PreReleaseStrategy? = nil,
requireExistingFile: Bool = true,
requireExistingSemVar: Bool = true
) {
self.preReleaseStrategy = preReleaseStrategy
self.requireExistingFile = requireExistingFile
self.requireExistingSemVar = requireExistingSemVar
}
}
}
enum Target: Codable, Equatable, Sendable {
case path(String)
case module(Module)
public struct Module: Codable, Equatable, Sendable {
public let name: String
public let fileName: String
public init(
_ name: String,
fileName: String = "Version.swift"
) {
self.name = name
self.fileName = fileName
}
}
}
}

View File

@@ -0,0 +1,72 @@
import Dependencies
import DependenciesMacros
import FileClient
import Foundation
public extension DependencyValues {
var configurationClient: ConfigurationClient {
get { self[ConfigurationClient.self] }
set { self[ConfigurationClient.self] = newValue }
}
}
@DependencyClient
public struct ConfigurationClient: Sendable {
public var find: @Sendable (URL?) async throws -> ConfigruationFile?
public var load: @Sendable (ConfigruationFile) async throws -> Configuration
public var write: @Sendable (Configuration, ConfigruationFile) async throws -> Void
public func findAndLoad(_ url: URL? = nil) async throws -> Configuration {
guard let url = try await find(url) else {
throw ConfigurationClientError.configurationNotFound
}
return try await load(url)
}
}
extension ConfigurationClient: DependencyKey {
public static let testValue: ConfigurationClient = Self()
public static var liveValue: ConfigurationClient {
.init(
find: { try await findConfiguration($0) },
load: { try await $0.load() ?? .init() },
write: { try await $1.write($0) }
)
}
}
private func findConfiguration(_ url: URL?) async throws -> ConfigruationFile? {
@Dependency(\.fileClient) var fileClient
var url: URL! = url
if url == nil {
url = try await URL(filePath: fileClient.currentDirectory())
}
// Check if url is a valid configuration url.
var configurationFile = ConfigruationFile(url: url)
if let configurationFile { return configurationFile }
guard try await fileClient.isDirectory(url.cleanFilePath) else {
throw ConfigurationClientError.invalidConfigurationDirectory(path: url.cleanFilePath)
}
// Check for toml file.
let tomlUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).toml")
configurationFile = ConfigruationFile(url: tomlUrl)
if let configurationFile { return configurationFile }
// Check for json file.
let jsonUrl = url.appending(path: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).json")
configurationFile = ConfigruationFile(url: jsonUrl)
if let configurationFile { return configurationFile }
// Couldn't find valid configuration file.
return nil
}
enum ConfigurationClientError: Error {
case configurationNotFound
case invalidConfigurationDirectory(path: String)
}

View File

@@ -0,0 +1,58 @@
import Dependencies
import FileClient
import Foundation
public enum ConfigruationFile: Equatable, Sendable {
case json(URL)
case toml(URL)
public init?(url: URL) {
if url.pathExtension == "toml" {
self = .toml(url)
} else if url.pathExtension == "json" {
self = .json(url)
} else {
return nil
}
}
var url: URL {
switch self {
case let .json(url): return url
case let .toml(url): return url
}
}
}
extension ConfigruationFile {
func load() async throws -> Configuration? {
@Dependency(\.coders) var coders
@Dependency(\.fileClient) var fileClient
switch self {
case .json:
let data = try await Data(fileClient.read(url.cleanFilePath).utf8)
return try? coders.jsonDecoder().decode(Configuration.self, from: data)
case .toml:
let string = try await fileClient.read(url.cleanFilePath)
return try? coders.tomlDecoder().decode(Configuration.self, from: string)
}
}
func write(_ configuration: Configuration) async throws {
@Dependency(\.coders) var coders
@Dependency(\.fileClient) var fileClient
let data: Data
switch self {
case .json:
data = try coders.jsonEncoder().encode(configuration)
case .toml:
data = try Data(coders.tomlEncoder().encode(configuration).utf8)
}
try await fileClient.write(data, url)
}
}

View File

@@ -0,0 +1,7 @@
import Foundation
extension ConfigurationClient {
enum Constants {
static let defaultFileNameWithoutExtension = ".bump-version"
}
}