diff --git a/.bump-version.toml b/.bump-version.toml new file mode 100644 index 0000000..8d7d96e --- /dev/null +++ b/.bump-version.toml @@ -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' \ No newline at end of file diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swift-cli-version-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swift-cli-version-Package.xcscheme index 7fc889d..3342ecf 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/swift-cli-version-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/swift-cli-version-Package.xcscheme @@ -90,6 +90,20 @@ ReferencedContainer = "container:"> + + + + + + + + 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( + preRelease: PreReleaseStrategy? = nil, + requireExistingFile: Bool = true, + requireExistingSemVar: Bool = true + ) { + self.preRelease = preRelease + self.requireExistingFile = requireExistingFile + self.requireExistingSemVar = requireExistingSemVar + } + + } + + /// Represents the target where we will bump the version in. + /// + /// 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 { + + /// The module directory name. public let name: String + + /// The version file name located in the module directory. 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( _ name: String, 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 + } + + } } diff --git a/Sources/ConfigurationClient/ConfigurationClient.swift b/Sources/ConfigurationClient/ConfigurationClient.swift index ac6be15..c20ac99 100644 --- a/Sources/ConfigurationClient/ConfigurationClient.swift +++ b/Sources/ConfigurationClient/ConfigurationClient.swift @@ -4,18 +4,28 @@ import FileClient import Foundation public extension DependencyValues { + + /// Perform operations with configuration files. var configurationClient: ConfigurationClient { get { self[ConfigurationClient.self] } set { self[ConfigurationClient.self] = newValue } } } +/// Handles interactions with configuration files. @DependencyClient 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? + + /// Load a configuration file. public var load: @Sendable (ConfigruationFile) async throws -> Configuration + + /// Write a configuration file. 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 { guard let url = try await find(url) else { throw ConfigurationClientError.configurationNotFound @@ -30,7 +40,7 @@ extension ConfigurationClient: DependencyKey { public static var liveValue: ConfigurationClient { .init( find: { try await findConfiguration($0) }, - load: { try await $0.load() ?? .init() }, + load: { try await $0.load() ?? .mock }, write: { try await $1.write($0) } ) } diff --git a/Sources/ConfigurationClient/ConfigurationFile.swift b/Sources/ConfigurationClient/ConfigurationFile.swift index 568260f..09d93b2 100644 --- a/Sources/ConfigurationClient/ConfigurationFile.swift +++ b/Sources/ConfigurationClient/ConfigurationFile.swift @@ -2,10 +2,27 @@ import Dependencies import FileClient import Foundation +/// Represents a configuration file type and location. public enum ConfigruationFile: Equatable, Sendable { + + /// A json configuration file. case json(URL) + + /// A toml configuration file. 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) { if url.pathExtension == "toml" { self = .toml(url) @@ -16,6 +33,7 @@ public enum ConfigruationFile: Equatable, Sendable { } } + /// The url of the file. var url: URL { switch self { case let .json(url): return url diff --git a/Tests/ConfigurationClientTests/ConfigurationClientTests.swift b/Tests/ConfigurationClientTests/ConfigurationClientTests.swift new file mode 100644 index 0000000..aeab0e0 --- /dev/null +++ b/Tests/ConfigurationClientTests/ConfigurationClientTests.swift @@ -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() + } + } +}