feat: Working on configuration client
This commit is contained in:
15
.bump-version.toml
Normal file
15
.bump-version.toml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[strategy.semvar]
|
||||||
|
requireExistingFile = true
|
||||||
|
requireExistingSemVar = true
|
||||||
|
|
||||||
|
[strategy.semvar.preRelease]
|
||||||
|
prefix = 'rc'
|
||||||
|
style = 'custom'
|
||||||
|
usesGitTag = false
|
||||||
|
|
||||||
|
[strategy.semvar.preRelease.branch]
|
||||||
|
includeCommitSha = true
|
||||||
|
|
||||||
|
[target.module]
|
||||||
|
fileName = 'Version.swift'
|
||||||
|
name = 'cli-version'
|
||||||
@@ -90,6 +90,20 @@
|
|||||||
ReferencedContainer = "container:">
|
ReferencedContainer = "container:">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildActionEntry>
|
</BuildActionEntry>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "CliClient"
|
||||||
|
BuildableName = "CliClient"
|
||||||
|
BlueprintName = "CliClient"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
</BuildActionEntries>
|
</BuildActionEntries>
|
||||||
</BuildAction>
|
</BuildAction>
|
||||||
<TestAction
|
<TestAction
|
||||||
@@ -109,6 +123,16 @@
|
|||||||
ReferencedContainer = "container:">
|
ReferencedContainer = "container:">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</TestableReference>
|
</TestableReference>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "ConfigurationClientTests"
|
||||||
|
BuildableName = "ConfigurationClientTests"
|
||||||
|
BlueprintName = "ConfigurationClientTests"
|
||||||
|
ReferencedContainer = "container:">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
</Testables>
|
</Testables>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ let package = Package(
|
|||||||
.product(name: "TOMLKit", package: "TOMLKit")
|
.product(name: "TOMLKit", package: "TOMLKit")
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
.testTarget(
|
||||||
|
name: "ConfigurationClientTests",
|
||||||
|
dependencies: ["ConfigurationClient", "TestSupport"]
|
||||||
|
),
|
||||||
.target(
|
.target(
|
||||||
name: "FileClient",
|
name: "FileClient",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
|||||||
@@ -1,68 +1,204 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import TOMLKit
|
||||||
|
|
||||||
public struct Configuration: Codable, Sendable {
|
/// Represents configuration that can be set via a file, generally in the root of the
|
||||||
|
/// project directory.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
public struct Configuration: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The target for the version.
|
||||||
public let target: Target?
|
public let target: Target?
|
||||||
|
|
||||||
|
/// The strategy used for deriving the version.
|
||||||
public let strategy: VersionStrategy?
|
public let strategy: VersionStrategy?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
target: Target? = nil,
|
target: Target? = nil,
|
||||||
strategy: VersionStrategy? = .semvar()
|
strategy: VersionStrategy? = .init(semvar: .init())
|
||||||
) {
|
) {
|
||||||
self.target = target
|
self.target = target
|
||||||
self.strategy = strategy
|
self.strategy = strategy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static var mock: Self {
|
||||||
|
.init(
|
||||||
|
target: .init(module: .init("cli-version")),
|
||||||
|
strategy: .init()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
public static var customPreRelease: Self {
|
||||||
|
.init(
|
||||||
|
target: .init(module: .init("cli-version")),
|
||||||
|
strategy: .init(semvar: .init(
|
||||||
|
preRelease: .customBranchPrefix("rc")
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Configuration {
|
public extension Configuration {
|
||||||
|
|
||||||
enum VersionStrategy: Codable, Equatable, Sendable {
|
/// Represents a branch version or pre-release strategy.
|
||||||
case branch(Branch = .init())
|
///
|
||||||
case semvar(SemVar = .init())
|
/// This derives the version from the branch name and short version
|
||||||
|
/// of the commit sha if configured.
|
||||||
|
struct Branch: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
public struct Branch: Codable, Equatable, Sendable {
|
/// Include the commit sha in the output for this strategy.
|
||||||
let includeCommitSha: Bool
|
let includeCommitSha: Bool
|
||||||
|
|
||||||
|
/// Create a new branch strategy.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - includeCommitSha: Whether to include the commit sha.
|
||||||
public init(includeCommitSha: Bool = true) {
|
public init(includeCommitSha: Bool = true) {
|
||||||
self.includeCommitSha = includeCommitSha
|
self.includeCommitSha = includeCommitSha
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum PreReleaseStrategy: Codable, Equatable, Sendable {
|
/// Represents version strategy for pre-release.
|
||||||
/// Use output of tag, with branch and commit sha.
|
///
|
||||||
case branch(Branch = .init())
|
/// This appends a suffix to the version that get's generated from the version strategy.
|
||||||
|
/// For example: `1.0.0-rc-1`
|
||||||
|
///
|
||||||
|
struct PreReleaseStrategy: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
/// Provide a custom pre-release tag.
|
/// Use branch and commit sha as pre-release suffix.
|
||||||
indirect case custom(String, PreReleaseStrategy? = nil)
|
public let branch: Branch?
|
||||||
|
|
||||||
/// Use the output of `git describe --tags`
|
/// Use a custom prefix string.
|
||||||
case gitTag
|
public let prefix: String?
|
||||||
|
|
||||||
|
/// An identifier for the type of pre-release.
|
||||||
|
public let style: StyleId
|
||||||
|
|
||||||
|
/// Whether we use `git describe --tags` for part of the suffix, this is only used
|
||||||
|
/// if we have a custom style.
|
||||||
|
public let usesGitTag: Bool
|
||||||
|
|
||||||
|
init(
|
||||||
|
style: StyleId,
|
||||||
|
branch: Branch? = nil,
|
||||||
|
prefix: String? = nil,
|
||||||
|
usesGitTag: Bool = false
|
||||||
|
) {
|
||||||
|
self.branch = branch
|
||||||
|
self.prefix = prefix
|
||||||
|
self.style = style
|
||||||
|
self.usesGitTag = usesGitTag
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SemVar: Codable, Equatable, Sendable {
|
/// Represents a pre-release strategy that is derived from calling
|
||||||
let preReleaseStrategy: PreReleaseStrategy?
|
/// `git describe --tags`.
|
||||||
let requireExistingFile: Bool
|
public static let gitTag = Self(style: StyleId.gitTag)
|
||||||
let requireExistingSemVar: Bool
|
|
||||||
|
/// Represents a pre-release strategy that is derived from the branch and commit sha.
|
||||||
|
public static func branch(_ branch: Branch = .init()) -> Self {
|
||||||
|
.init(style: .branch, branch: branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a custom strategy that uses the given value, not deriving any other
|
||||||
|
/// data.
|
||||||
|
public static func custom(_ prefix: String) -> Self {
|
||||||
|
.init(style: .custom, prefix: prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a custom strategy that uses a prefix along with the branch and
|
||||||
|
/// commit sha.
|
||||||
|
public static func customBranchPrefix(
|
||||||
|
_ prefix: String,
|
||||||
|
branch: Branch = .init()
|
||||||
|
) -> Self {
|
||||||
|
.init(style: .custom, branch: branch, prefix: prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a custom strategy that uses a prefix along with the output from
|
||||||
|
/// calling `git describe --tags`.
|
||||||
|
public static func customGitTagPrefix(_ prefix: String) -> Self {
|
||||||
|
.init(style: StyleId.custom, prefix: prefix, usesGitTag: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StyleId: String, Codable, Sendable {
|
||||||
|
case branch
|
||||||
|
case custom
|
||||||
|
case gitTag
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a semvar version strategy.
|
||||||
|
///
|
||||||
|
/// ## Example: 1.0.0
|
||||||
|
///
|
||||||
|
struct SemVar: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// Optional pre-releas suffix strategy.
|
||||||
|
public let preRelease: PreReleaseStrategy?
|
||||||
|
|
||||||
|
/// Fail if an existing version file does not exist in the target.
|
||||||
|
public let requireExistingFile: Bool
|
||||||
|
|
||||||
|
/// Fail if an existing semvar is not parsed from the file or version generation strategy.
|
||||||
|
public let requireExistingSemVar: Bool
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
preReleaseStrategy: PreReleaseStrategy? = nil,
|
preRelease: PreReleaseStrategy? = nil,
|
||||||
requireExistingFile: Bool = true,
|
requireExistingFile: Bool = true,
|
||||||
requireExistingSemVar: Bool = true
|
requireExistingSemVar: Bool = true
|
||||||
) {
|
) {
|
||||||
self.preReleaseStrategy = preReleaseStrategy
|
self.preRelease = preRelease
|
||||||
self.requireExistingFile = requireExistingFile
|
self.requireExistingFile = requireExistingFile
|
||||||
self.requireExistingSemVar = requireExistingSemVar
|
self.requireExistingSemVar = requireExistingSemVar
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Target: Codable, Equatable, Sendable {
|
/// Represents the target where we will bump the version in.
|
||||||
case path(String)
|
///
|
||||||
case module(Module)
|
/// This can either be a path to a version file or a module used to
|
||||||
|
/// locate the version file.
|
||||||
|
struct Target: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The path to a version file.
|
||||||
|
public let path: String?
|
||||||
|
|
||||||
|
/// A module to find the version file in.
|
||||||
|
public let module: Module?
|
||||||
|
|
||||||
|
/// Create a target for the given path.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - path: The path to the version file.
|
||||||
|
public init(path: String) {
|
||||||
|
self.path = path
|
||||||
|
self.module = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a target for the given module.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - module: The module for the version file.
|
||||||
|
public init(module: Module) {
|
||||||
|
self.path = nil
|
||||||
|
self.module = module
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a module target for a version file.
|
||||||
|
///
|
||||||
public struct Module: Codable, Equatable, Sendable {
|
public struct Module: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// The module directory name.
|
||||||
public let name: String
|
public let name: String
|
||||||
|
|
||||||
|
/// The version file name located in the module directory.
|
||||||
public let fileName: String
|
public let fileName: String
|
||||||
|
|
||||||
|
/// Create a new module target.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The module directory name.
|
||||||
|
/// - fileName: The file name located in the module directory.
|
||||||
public init(
|
public init(
|
||||||
_ name: String,
|
_ name: String,
|
||||||
fileName: String = "Version.swift"
|
fileName: String = "Version.swift"
|
||||||
@@ -72,4 +208,37 @@ public extension Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Strategy used to generate a version.
|
||||||
|
///
|
||||||
|
/// Typically a `SemVar` strategy or `Branch`.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
struct VersionStrategy: Codable, Equatable, Sendable {
|
||||||
|
|
||||||
|
/// Set if we're using the branch and commit sha to derive the version.
|
||||||
|
let branch: Branch?
|
||||||
|
|
||||||
|
/// Set if we're using semvar to derive the version.
|
||||||
|
let semvar: SemVar?
|
||||||
|
|
||||||
|
/// Create a new version strategy that uses branch and commit sha to derive the version.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - branch: The branch strategy options.
|
||||||
|
public init(branch: Branch) {
|
||||||
|
self.branch = branch
|
||||||
|
self.semvar = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new version strategy that uses semvar to derive the version.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - semvar: The semvar strategy options.
|
||||||
|
public init(semvar: SemVar = .init()) {
|
||||||
|
self.branch = nil
|
||||||
|
self.semvar = semvar
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,28 @@ import FileClient
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public extension DependencyValues {
|
public extension DependencyValues {
|
||||||
|
|
||||||
|
/// Perform operations with configuration files.
|
||||||
var configurationClient: ConfigurationClient {
|
var configurationClient: ConfigurationClient {
|
||||||
get { self[ConfigurationClient.self] }
|
get { self[ConfigurationClient.self] }
|
||||||
set { self[ConfigurationClient.self] = newValue }
|
set { self[ConfigurationClient.self] = newValue }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles interactions with configuration files.
|
||||||
@DependencyClient
|
@DependencyClient
|
||||||
public struct ConfigurationClient: Sendable {
|
public struct ConfigurationClient: Sendable {
|
||||||
|
|
||||||
|
/// 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 -> ConfigruationFile?
|
||||||
|
|
||||||
|
/// Load a configuration file.
|
||||||
public var load: @Sendable (ConfigruationFile) async throws -> Configuration
|
public var load: @Sendable (ConfigruationFile) async throws -> Configuration
|
||||||
|
|
||||||
|
/// Write a configuration file.
|
||||||
public var write: @Sendable (Configuration, ConfigruationFile) async throws -> Void
|
public var write: @Sendable (Configuration, ConfigruationFile) async throws -> Void
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
guard let url = try await find(url) else {
|
guard let url = try await find(url) else {
|
||||||
throw ConfigurationClientError.configurationNotFound
|
throw ConfigurationClientError.configurationNotFound
|
||||||
@@ -30,7 +40,7 @@ extension ConfigurationClient: DependencyKey {
|
|||||||
public static var liveValue: ConfigurationClient {
|
public static var liveValue: ConfigurationClient {
|
||||||
.init(
|
.init(
|
||||||
find: { try await findConfiguration($0) },
|
find: { try await findConfiguration($0) },
|
||||||
load: { try await $0.load() ?? .init() },
|
load: { try await $0.load() ?? .mock },
|
||||||
write: { try await $1.write($0) }
|
write: { try await $1.write($0) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,27 @@ import Dependencies
|
|||||||
import FileClient
|
import FileClient
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
/// Represents a configuration file type and location.
|
||||||
public enum ConfigruationFile: Equatable, Sendable {
|
public enum ConfigruationFile: Equatable, Sendable {
|
||||||
|
|
||||||
|
/// A json configuration file.
|
||||||
case json(URL)
|
case json(URL)
|
||||||
|
|
||||||
|
/// A toml configuration file.
|
||||||
case toml(URL)
|
case toml(URL)
|
||||||
|
|
||||||
|
/// Default configuration file, which is a toml file
|
||||||
|
/// with the name of '.bump-version.toml'
|
||||||
|
public static var `default`: Self {
|
||||||
|
.toml(URL(
|
||||||
|
filePath: "\(ConfigurationClient.Constants.defaultFileNameWithoutExtension).toml"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new file location from the given url.
|
||||||
|
///
|
||||||
|
/// - Parameters:
|
||||||
|
/// - url: The url for the file.
|
||||||
public init?(url: URL) {
|
public init?(url: URL) {
|
||||||
if url.pathExtension == "toml" {
|
if url.pathExtension == "toml" {
|
||||||
self = .toml(url)
|
self = .toml(url)
|
||||||
@@ -16,6 +33,7 @@ public enum ConfigruationFile: Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The url of the file.
|
||||||
var url: URL {
|
var url: URL {
|
||||||
switch self {
|
switch self {
|
||||||
case let .json(url): return url
|
case let .json(url): return url
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import ConfigurationClient
|
||||||
|
import Dependencies
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
@Suite("ConfigurationClientTests")
|
||||||
|
struct ConfigurationClientTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func codable() async throws {
|
||||||
|
try await run {
|
||||||
|
@Dependency(\.configurationClient) var configurationClient
|
||||||
|
@Dependency(\.coders) var coders
|
||||||
|
|
||||||
|
let configuration = Configuration.customPreRelease
|
||||||
|
let encoded = try coders.jsonEncoder().encode(configuration)
|
||||||
|
let decoded = try coders.jsonDecoder().decode(Configuration.self, from: encoded)
|
||||||
|
|
||||||
|
#expect(decoded == configuration)
|
||||||
|
|
||||||
|
let tomlEncoded = try coders.tomlEncoder().encode(configuration)
|
||||||
|
let tomlDecoded = try coders.tomlDecoder().decode(
|
||||||
|
Configuration.self,
|
||||||
|
from: tomlEncoded
|
||||||
|
)
|
||||||
|
#expect(tomlDecoded == configuration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(arguments: ["foo", ".foo"])
|
||||||
|
func configurationFile(fileName: String) {
|
||||||
|
for ext in ["toml", "json", "bar"] {
|
||||||
|
let file = ConfigruationFile(url: URL(filePath: "\(fileName).\(ext)"))
|
||||||
|
switch ext {
|
||||||
|
case "toml":
|
||||||
|
#expect(file == .toml(URL(filePath: "\(fileName).toml")))
|
||||||
|
case "json":
|
||||||
|
#expect(file == .json(URL(filePath: "\(fileName).json")))
|
||||||
|
default:
|
||||||
|
#expect(file == nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run(
|
||||||
|
setupDependencies: @escaping (inout DependencyValues) -> Void = { _ in },
|
||||||
|
operation: () async throws -> Void
|
||||||
|
) async throws {
|
||||||
|
try await withDependencies {
|
||||||
|
$0.coders = .liveValue
|
||||||
|
$0.fileClient = .liveValue
|
||||||
|
$0.configurationClient = .liveValue
|
||||||
|
setupDependencies(&$0)
|
||||||
|
} operation: {
|
||||||
|
try await operation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user