feat: Begin working on configuration client.
This commit is contained in:
3
Sources/CliClient/Internal/Constants.swift
Normal file
3
Sources/CliClient/Internal/Constants.swift
Normal file
@@ -0,0 +1,3 @@
|
||||
enum Constants {
|
||||
static let defaultFileName = "Version.swift"
|
||||
}
|
||||
48
Sources/CliClient/Internal/FileClient+semVar.swift
Normal file
48
Sources/CliClient/Internal/FileClient+semVar.swift
Normal file
@@ -0,0 +1,48 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
import GitClient
|
||||
|
||||
@_spi(Internal)
|
||||
public extension FileClient {
|
||||
private func getVersionString(
|
||||
fileUrl: URL,
|
||||
gitDirectory: String?
|
||||
) async throws -> (version: String, usesOptionalType: Bool) {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = fileUrl
|
||||
|
||||
guard fileExists(targetUrl) else {
|
||||
throw CliClientError.fileDoesNotExist(path: fileUrl.cleanFilePath)
|
||||
}
|
||||
|
||||
let contents = try await read(targetUrl)
|
||||
let versionLine = contents.split(separator: "\n")
|
||||
.first { $0.hasPrefix("let VERSION:") }
|
||||
|
||||
guard let versionLine else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
logger.debug("Version line: \(versionLine)")
|
||||
|
||||
let isOptional = versionLine.contains("String?")
|
||||
logger.debug("Uses optional: \(isOptional)")
|
||||
|
||||
let versionString = versionLine.split(separator: "let VERSION: \(isOptional ? "String?" : "String") = ").last
|
||||
guard let versionString else {
|
||||
throw CliClientError.failedToParseVersionFile
|
||||
}
|
||||
return (String(versionString), isOptional)
|
||||
}
|
||||
|
||||
func semVar(
|
||||
file: URL,
|
||||
gitDirectory: String?
|
||||
) async throws -> (semVar: SemVar?, usesOptionalType: Bool) {
|
||||
let (string, usesOptionalType) = try await getVersionString(fileUrl: file, gitDirectory: gitDirectory)
|
||||
return (SemVar(string: string), usesOptionalType)
|
||||
}
|
||||
|
||||
}
|
||||
15
Sources/CliClient/Internal/Helpers.swift
Normal file
15
Sources/CliClient/Internal/Helpers.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Logging
|
||||
|
||||
// TODO: Move.
|
||||
@_spi(Internal)
|
||||
public extension Logger.Level {
|
||||
|
||||
init(verbose: Int) {
|
||||
switch verbose {
|
||||
case 1: self = .warning
|
||||
case 2: self = .debug
|
||||
case 3...: self = .trace
|
||||
default: self = .info
|
||||
}
|
||||
}
|
||||
}
|
||||
144
Sources/CliClient/Internal/Internal.swift
Normal file
144
Sources/CliClient/Internal/Internal.swift
Normal file
@@ -0,0 +1,144 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
import GitClient
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.SharedOptions {
|
||||
|
||||
func parseTargetUrl() async throws -> URL {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
|
||||
let target = target.hasPrefix(".") ? String(target.dropFirst()) : target
|
||||
let targetHasSources = target.hasPrefix("Sources") || target.hasPrefix("/Sources")
|
||||
|
||||
var url = url(for: gitDirectory ?? (targetHasSources ? target : "Sources"))
|
||||
|
||||
if gitDirectory != nil {
|
||||
if !targetHasSources {
|
||||
url.appendPathComponent("Sources")
|
||||
}
|
||||
url.appendPathComponent(target)
|
||||
}
|
||||
|
||||
let isDirectory = try await fileClient.isDirectory(url.cleanFilePath)
|
||||
|
||||
if isDirectory {
|
||||
url.appendPathComponent(Constants.defaultFileName)
|
||||
}
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func run(
|
||||
_ operation: (CurrentVersionContainer) async throws -> Void
|
||||
) async rethrows -> String {
|
||||
try await withDependencies {
|
||||
$0.logger.logLevel = logLevel
|
||||
} operation: {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let targetUrl = try await parseTargetUrl()
|
||||
logger.debug("Target: \(targetUrl.cleanFilePath)")
|
||||
|
||||
try await operation(
|
||||
versionStrategy.currentVersion(
|
||||
file: targetUrl,
|
||||
gitDirectory: gitDirectory
|
||||
)
|
||||
)
|
||||
|
||||
return targetUrl.cleanFilePath
|
||||
}
|
||||
}
|
||||
|
||||
func write(_ string: String, to url: URL) async throws {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.logger) var logger
|
||||
if !dryRun {
|
||||
try await fileClient.write(string: string, to: url)
|
||||
} else {
|
||||
logger.debug("Skipping, due to dry-run being passed.")
|
||||
logger.debug("\(string)")
|
||||
}
|
||||
}
|
||||
|
||||
func write(_ currentVersion: CurrentVersionContainer) async throws {
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
let version = try currentVersion.version.string(allowPreReleaseTag: allowPreReleaseTag)
|
||||
logger.debug("Version: \(version)")
|
||||
|
||||
let template = currentVersion.usesOptionalType ? Template.optional(version) : Template.nonOptional(version)
|
||||
logger.trace("Template string: \(template)")
|
||||
|
||||
try await write(template, to: currentVersion.targetUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public struct CurrentVersionContainer: Sendable {
|
||||
|
||||
let targetUrl: URL
|
||||
let version: Version
|
||||
|
||||
var usesOptionalType: Bool {
|
||||
switch version {
|
||||
case .string: return false
|
||||
case let .semVar(_, usesOptionalType): return usesOptionalType
|
||||
}
|
||||
}
|
||||
|
||||
public enum Version: Sendable {
|
||||
case string(String)
|
||||
case semVar(SemVar, usesOptionalType: Bool = true)
|
||||
|
||||
func string(allowPreReleaseTag: Bool) throws -> String {
|
||||
switch self {
|
||||
case let .string(string):
|
||||
return string
|
||||
case let .semVar(semVar, usesOptionalType: _):
|
||||
return semVar.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension CliClient.SharedOptions {
|
||||
|
||||
func build(_ environment: [String: String]) async throws -> String {
|
||||
try await run { currentVersion in
|
||||
try await write(currentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func bump(_ type: CliClient.BumpOption?) async throws -> String {
|
||||
guard let type else {
|
||||
return try await generate()
|
||||
}
|
||||
|
||||
return try await run { container in
|
||||
|
||||
@Dependency(\.logger) var logger
|
||||
|
||||
switch container.version {
|
||||
case .string: // When we did not parse a semVar, just write whatever we parsed for the current version.
|
||||
try await write(container)
|
||||
|
||||
case let .semVar(semVar, usesOptionalType: usesOptionalType):
|
||||
let bumped = semVar.bump(type, preRelease: nil) // preRelease is already set on semVar.
|
||||
let version = bumped.versionString(withPreReleaseTag: allowPreReleaseTag)
|
||||
logger.debug("Bumped version: \(version)")
|
||||
let template = usesOptionalType ? Template.optional(version) : Template.build(version)
|
||||
try await write(template, to: container.targetUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generate() async throws -> String {
|
||||
try await run { currentVersion in
|
||||
try await write(currentVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
Sources/CliClient/Internal/SemVar.swift
Normal file
123
Sources/CliClient/Internal/SemVar.swift
Normal file
@@ -0,0 +1,123 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import Foundation
|
||||
import GitClient
|
||||
|
||||
// Container for sem-var version.
|
||||
@_spi(Internal)
|
||||
public struct SemVar: CustomStringConvertible, Equatable, Sendable {
|
||||
/// The major version.
|
||||
public let major: Int
|
||||
/// The minor version.
|
||||
public let minor: Int
|
||||
/// The patch version.
|
||||
public let patch: Int
|
||||
/// Extra pre-release tag.
|
||||
public let preRelease: String?
|
||||
|
||||
public init(
|
||||
major: Int,
|
||||
minor: Int,
|
||||
patch: Int,
|
||||
preRelease: String? = nil
|
||||
) {
|
||||
self.major = major
|
||||
self.minor = minor
|
||||
self.patch = patch
|
||||
self.preRelease = preRelease
|
||||
}
|
||||
|
||||
public init(preRelease: String? = nil) {
|
||||
self.init(
|
||||
major: 0,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
preRelease: preRelease
|
||||
)
|
||||
}
|
||||
|
||||
public init?(string: String) {
|
||||
let parts = string.split(separator: ".")
|
||||
guard parts.count >= 3 else {
|
||||
return nil
|
||||
}
|
||||
let major = Int(String(parts[0].replacingOccurrences(of: "\"", with: "")))
|
||||
let minor = Int(String(parts[1]))
|
||||
|
||||
let patchParts = parts[2].split(separator: "-")
|
||||
let patch = Int(patchParts.first ?? "0")
|
||||
let preRelease = String(patchParts.dropFirst().joined(separator: "-"))
|
||||
|
||||
self.init(
|
||||
major: major ?? 0,
|
||||
minor: minor ?? 0,
|
||||
patch: patch ?? 0,
|
||||
preRelease: preRelease
|
||||
)
|
||||
}
|
||||
|
||||
public var description: String { versionString() }
|
||||
|
||||
// Create a version string, optionally appending a suffix.
|
||||
public func versionString(withPreReleaseTag: Bool = true) -> String {
|
||||
let string = "\(major).\(minor).\(patch)"
|
||||
|
||||
guard withPreReleaseTag else { return string }
|
||||
|
||||
guard let suffix = preRelease, suffix.count > 0 else {
|
||||
return string
|
||||
}
|
||||
|
||||
if !suffix.hasPrefix("-") {
|
||||
return "\(string)-\(suffix)"
|
||||
}
|
||||
|
||||
return "\(string)\(suffix)"
|
||||
}
|
||||
|
||||
// Bumps the sem-var by the given option (major, minor, patch)
|
||||
public func bump(_ option: CliClient.BumpOption, preRelease: String?) -> Self {
|
||||
switch option {
|
||||
case .major:
|
||||
return .init(
|
||||
major: major + 1,
|
||||
minor: 0,
|
||||
patch: 0,
|
||||
preRelease: preRelease
|
||||
)
|
||||
case .minor:
|
||||
return .init(
|
||||
major: major,
|
||||
minor: minor + 1,
|
||||
patch: 0,
|
||||
preRelease: preRelease
|
||||
)
|
||||
case .patch:
|
||||
return .init(
|
||||
major: major,
|
||||
minor: minor,
|
||||
patch: patch + 1,
|
||||
preRelease: preRelease
|
||||
)
|
||||
case .preRelease:
|
||||
guard let preRelease else { return self }
|
||||
return applyingPreRelease(preRelease)
|
||||
}
|
||||
}
|
||||
|
||||
public func applyingPreRelease(_ preRelease: String) -> Self {
|
||||
.init(
|
||||
major: major,
|
||||
minor: minor,
|
||||
patch: patch,
|
||||
preRelease: preRelease
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension GitClient.Version {
|
||||
var semVar: SemVar? {
|
||||
.init(string: description)
|
||||
}
|
||||
}
|
||||
30
Sources/CliClient/Internal/Template.swift
Normal file
30
Sources/CliClient/Internal/Template.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
@_spi(Internal)
|
||||
public struct Template: Sendable {
|
||||
let type: TemplateType
|
||||
let version: String?
|
||||
|
||||
enum TemplateType: String, Sendable {
|
||||
case optionalString = "String?"
|
||||
case string = "String"
|
||||
}
|
||||
|
||||
var value: String {
|
||||
let versionString = version != nil ? "\"\(version!)\"" : "nil"
|
||||
return """
|
||||
// Do not set this variable, it is set during the build process.
|
||||
let VERSION: \(type.rawValue) = \(versionString)
|
||||
"""
|
||||
}
|
||||
|
||||
public static func build(_ version: String? = nil) -> String {
|
||||
nonOptional(version)
|
||||
}
|
||||
|
||||
public static func nonOptional(_ version: String? = nil) -> String {
|
||||
Self(type: .string, version: version).value
|
||||
}
|
||||
|
||||
public static func optional(_ version: String? = nil) -> String {
|
||||
Self(type: .optionalString, version: version).value
|
||||
}
|
||||
}
|
||||
103
Sources/CliClient/Internal/VersionStrategy+currentVersion.swift
Normal file
103
Sources/CliClient/Internal/VersionStrategy+currentVersion.swift
Normal file
@@ -0,0 +1,103 @@
|
||||
import Dependencies
|
||||
import FileClient
|
||||
import struct Foundation.URL
|
||||
import GitClient
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.PreReleaseStrategy {
|
||||
|
||||
func preReleaseString(gitDirectory: String?) async throws -> String {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
switch self {
|
||||
case let .custom(string, child):
|
||||
guard let child else { return string }
|
||||
return try await "\(string)-\(child.preReleaseString(gitDirectory: gitDirectory))"
|
||||
case .tag:
|
||||
return try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .tag(exactMatch: false)
|
||||
)).description
|
||||
case .branchAndCommit:
|
||||
return try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .branch(commitSha: true)
|
||||
)).description
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.VersionStrategy.SemVarOptions {
|
||||
|
||||
private func applyingPreRelease(_ semVar: SemVar, _ gitDirectory: String?) async throws -> SemVar {
|
||||
guard let preReleaseStrategy else { return semVar }
|
||||
let preRelease = try await preReleaseStrategy.preReleaseString(gitDirectory: gitDirectory)
|
||||
return semVar.applyingPreRelease(preRelease)
|
||||
}
|
||||
|
||||
func currentVersion(file: URL, gitDirectory: String? = nil) async throws -> CurrentVersionContainer.Version {
|
||||
@Dependency(\.fileClient) var fileClient
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
let fileOutput = try? await fileClient.semVar(file: file, gitDirectory: gitDirectory)
|
||||
var semVar = fileOutput?.semVar
|
||||
let usesOptionalType = fileOutput?.usesOptionalType
|
||||
|
||||
if requireExistingFile {
|
||||
guard let semVar else {
|
||||
throw CliClientError.fileDoesNotExist(path: file.cleanFilePath)
|
||||
}
|
||||
return try await .semVar(
|
||||
applyingPreRelease(semVar, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
// Didn't have existing semVar loaded from file, so check for git-tag.
|
||||
|
||||
semVar = try await gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .tag(exactMatch: false)
|
||||
)).semVar
|
||||
if requireExistingSemVar {
|
||||
guard let semVar else {
|
||||
fatalError()
|
||||
}
|
||||
return try await .semVar(
|
||||
applyingPreRelease(semVar, gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
|
||||
return try await .semVar(
|
||||
applyingPreRelease(.init(), gitDirectory),
|
||||
usesOptionalType: usesOptionalType ?? false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension CliClient.VersionStrategy {
|
||||
|
||||
func currentVersion(file: URL, gitDirectory: String?) async throws -> CurrentVersionContainer {
|
||||
@Dependency(\.gitClient) var gitClient
|
||||
|
||||
switch self {
|
||||
case .branchAndCommit:
|
||||
return try await .init(
|
||||
targetUrl: file,
|
||||
version: .string(
|
||||
gitClient.version(.init(
|
||||
gitDirectory: gitDirectory,
|
||||
style: .branch(commitSha: true)
|
||||
)).description
|
||||
)
|
||||
)
|
||||
case let .semVar(options):
|
||||
return try await .init(
|
||||
targetUrl: file,
|
||||
version: options.currentVersion(file: file, gitDirectory: gitDirectory)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user