Skip to content

Commit 4a27513

Browse files
committed
Refactor formatter APIs
feature/refactor-formatters
1 parent 9f7581f commit 4a27513

19 files changed

+348
-262
lines changed

README.md

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ Typically, modeling a CLI tool will begin with a `TopLevelCommandRepresentable`.
1818
```swift
1919
struct MyCommand: TopLevelCommandRepresentable {
2020
func commandValue() -> Command { "my-command" }
21-
var flagFormatter: FlagFormatter { .doubleDashPrefix }
22-
var optionFormatter: OptionFormatter { .doubleDashPrefix }
21+
let flagFormatter = FlagFormatter(prefix: .doubleDash) }
22+
let optionFormatter = OptionFormatter(prefix: .doubleDash) }
2323
}
2424
```
2525

@@ -30,8 +30,8 @@ Within `MyCommand` we need the ability to model a boolean value to enable/disabl
3030
```swift
3131
struct MyCommand: TopLevelCommandRepresentable {
3232
func commandValue() -> Command { "my-command" }
33-
var flagFormatter: FlagFormatter { .doubleDashPrefix }
34-
var optionFormatter: OptionFormatter { .doubleDashPrefix }
33+
let flagFormatter = FlagFormatter(prefix: .doubleDash) }
34+
let optionFormatter = OptionFormatter(prefix: .doubleDash) }
3535

3636
@Flag var myFlag: Bool = false
3737
}
@@ -42,8 +42,8 @@ In addition to modeling the ability to enable/disable a feature, we need to set
4242
```swift
4343
struct MyCommand: TopLevelCommandRepresentable {
4444
func commandValue() -> Command { "my-command" }
45-
var flagFormatter: FlagFormatter { .doubleDashPrefix }
46-
var optionFormatter: OptionFormatter { .doubleDashPrefix }
45+
let flagFormatter = FlagFormatter(prefix: .doubleDash) }
46+
let optionFormatter = OptionFormatter(prefix: .doubleDash) }
4747

4848
@Flag var myFlag: Bool = false
4949
@Option var myOption: Int = 0
@@ -56,8 +56,8 @@ Positional arguments that are just a value, with no key are supported through th
5656
```swift
5757
struct MyCommand: TopLevelCommandRepresentable {
5858
func commandValue() -> Command { "my-command" }
59-
var flagFormatter: FlagFormatter { .doubleDashPrefix }
60-
var optionFormatter: OptionFormatter { .doubleDashPrefix }
59+
let flagFormatter = FlagFormatter(prefix: .doubleDash) }
60+
let optionFormatter = OptionFormatter(prefix: .doubleDash) }
6161

6262
@Flag var myFlag: Bool = false
6363
@Option var myOption: Int = 0
@@ -130,17 +130,14 @@ import ArgumentEncoding
130130
enum SwiftCommand: TopLevelCommandRepresentable {
131131
func commandValue() -> Command { "swift" }
132132

133-
var flagFormatter: FlagFormatter { .doubleDashPrefixKebabCase }
134-
var optionFormatter: OptionFormatter { .doubleDashPrefixKebabCase }
133+
var flagFormatter: FlagFormatter { FlagFormatter(prefix: .doubleDash, body: .kebabCase) }
134+
var optionFormatter: OptionFormatter { OptionFormatter(prefix: .doubleDash, body: .kebabCase) }
135135

136136
case run(RunCommand)
137137
case test(TestCommand)
138138
}
139139

140140
struct RunCommand: CommandRepresentable {
141-
let flagFormatter: FlagFormatter = .doubleDashPrefixKebabCase
142-
let optionFormatter: OptionFormatter = .doubleDashPrefixKebabCase
143-
144141
@Positional var executable: String
145142
}
146143

@@ -151,9 +148,6 @@ extension RunCommand: ExpressibleByStringLiteral {
151148
}
152149

153150
struct TestCommand: CommandRepresentable {
154-
let flagFormatter: FlagFormatter = .doubleDashPrefixKebabCase
155-
let optionFormatter: OptionFormatter = .doubleDashPrefixKebabCase
156-
157151
@Flag var parallel: Bool = true
158152
@Option var numWorkers: Int = 1
159153
@Flag var showCodecovPath: Bool = false

Sources/ArgumentEncoding/CaseConverter.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import Foundation
77

88
/// Convert from Swift's typical camelCase to kebab-case and snake_case as some argument formats require them.
99
public enum CaseConverter {
10-
public static let kebabCase: (String) -> String = fromCamelCase(template: "$1-$2")
10+
public static let kebabCase: @Sendable (String) -> String = fromCamelCase(template: "$1-$2")
1111

12-
public static let snakeCase: (String) -> String = fromCamelCase(template: "$1_$2")
12+
public static let snakeCase: @Sendable (String) -> String = fromCamelCase(template: "$1_$2")
1313

14-
private static func fromCamelCase(template: String) -> (String) -> String {
14+
@Sendable
15+
private static func fromCamelCase(template: String) -> @Sendable (String) -> String {
1516
guard let regex = try? NSRegularExpression(pattern: "([a-z0-9])([A-Z])", options: []) else {
1617
return { $0 }
1718
}

Sources/ArgumentEncoding/CommandRepresentable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import Dependencies
1313
/// struct ParentGroup: CommandRepresentable {
1414
/// // Formatters to satisfy `FormatterNode` requirements
1515
/// let flagFormatter: FlagFormatter = .doubleDashPrefix
16-
/// let optionFormatter: OptionFormatter = .doubleDashPrefix
16+
/// let optionFormatter: OptionFormatter = OptionFormatter(prefix: .doubleDash)
1717
///
1818
/// // Properties that represent the child arguments
1919
/// @Flag var asyncMain: Bool

Sources/ArgumentEncoding/Flag.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Dependencies
2222
/// ```swift
2323
/// struct FlagContainer: ArgumentGroup, FormatterNode {
2424
/// let flagFormatter: FlagFormatter = .doubleDashPrefix
25-
/// let optionFormatter: OptionFormatter = .doubleDashPrefix
25+
/// let optionFormatter: OptionFormatter = OptionFormatter(prefix: .doubleDash)
2626
///
2727
/// @Flag var name: Bool = true
2828
/// }

Sources/ArgumentEncoding/FlagFormatter.swift

Lines changed: 0 additions & 68 deletions
This file was deleted.
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Formatters.swift
2+
// ArgumentEncoding
3+
//
4+
// Copyright © 2023 MFB Technologies, Inc. All rights reserved.
5+
6+
import Dependencies
7+
import XCTestDynamicOverlay
8+
9+
/// Formats `Flag`s to match how different executables format arguments
10+
public struct FlagFormatter: Sendable {
11+
/// Formats a key string
12+
public let prefix: @Sendable () -> String
13+
public let body: @Sendable (_ key: String) -> String
14+
15+
@Sendable
16+
public func format(key: String) -> String {
17+
prefix() + body(key)
18+
}
19+
20+
@Sendable
21+
internal func _format(encoding: FlagEncoding) -> String {
22+
format(key: encoding.key)
23+
}
24+
25+
/// Initialize a new formatter
26+
///
27+
/// - Parameters
28+
/// - prefix: Closure that returns the prefix string
29+
/// - body: Closure that transforms the key string for formatting
30+
public init(
31+
prefix: @escaping @Sendable () -> String,
32+
body: @escaping @Sendable (_ key: String) -> String
33+
) {
34+
self.prefix = prefix
35+
self.body = body
36+
}
37+
38+
/// Initialize a new formatter
39+
///
40+
/// - Parameters
41+
/// - prefix: Name spaced closure that returns the prefix string for a Flag
42+
/// - body: Name spaced closure that transforms the key string for formatting
43+
public init(prefix: PrefixFormatter = .empty, body: BodyFormatter = .empty) {
44+
self.init(
45+
prefix: prefix.transform,
46+
body: body.transform
47+
)
48+
}
49+
}
50+
51+
/// Formats `Option`s to match how different executables format arguments
52+
public struct OptionFormatter: Sendable {
53+
public let prefix: @Sendable () -> String
54+
public let body: @Sendable (_ key: String) -> String
55+
public let separator: @Sendable () -> String
56+
57+
public func format(key: String, value: String) -> String {
58+
prefix() + body(key) + separator() + value
59+
}
60+
61+
internal func format(encoding: OptionEncoding) -> String {
62+
format(key: encoding.key, value: encoding.value)
63+
}
64+
65+
/// Initialize a new formatter
66+
///
67+
/// - Parameters
68+
/// - prefix: Closure that returns the prefix string
69+
/// - body: Closure that transforms the key string for formatting
70+
/// - separator: Closure that returns the string that separates the key and value
71+
public init(
72+
prefix: @escaping @Sendable () -> String,
73+
body: @escaping @Sendable (_ key: String) -> String,
74+
separator: @escaping @Sendable () -> String
75+
) {
76+
self.prefix = prefix
77+
self.body = body
78+
self.separator = separator
79+
}
80+
81+
/// Initialize a new formatter
82+
///
83+
/// - Parameters
84+
/// - prefix: Name spaced closure that returns the prefix string for a Flag
85+
/// - body: Name spaced closure that transforms the key string for formatting
86+
/// - separator: Name spaced closure that returns the string that separates the key and value
87+
public init(
88+
prefix: PrefixFormatter = .empty,
89+
body: BodyFormatter = .empty,
90+
separator: SeparatorFormatter = .space
91+
) {
92+
self.init(
93+
prefix: prefix.transform,
94+
body: body.transform,
95+
separator: separator.transform
96+
)
97+
}
98+
}
99+
100+
// MARK: Supporting formatters
101+
102+
/// Name space for a closure that returns a string that prefixes a Flag or Option's key
103+
public struct PrefixFormatter: Sendable {
104+
public let transform: @Sendable () -> String
105+
106+
public init(_ transform: @escaping @Sendable () -> String) {
107+
self.transform = transform
108+
}
109+
110+
public static let empty = Self { "" }
111+
public static let singleDash = Self { StaticString.singleDash.description }
112+
public static let doubleDash = Self { StaticString.doubleDash.description }
113+
}
114+
115+
/// Name space for a closure that transforms a Flag or Option's key
116+
public struct BodyFormatter: Sendable {
117+
public let transform: @Sendable (_ key: String) -> String
118+
119+
public init(_ transform: @escaping @Sendable (_ key: String) -> String) {
120+
self.transform = transform
121+
}
122+
123+
public static let empty = Self { $0 }
124+
public static let kebabCase = Self(CaseConverter.kebabCase)
125+
public static let snakeCase = Self(CaseConverter.snakeCase)
126+
}
127+
128+
/// Name space for a closure that returns the separator string between an Option's key and value
129+
public struct SeparatorFormatter: Sendable {
130+
public let transform: @Sendable () -> String
131+
132+
public init(_ transform: @escaping @Sendable () -> String) {
133+
self.transform = transform
134+
}
135+
136+
public static let space = Self { StaticString.space.description }
137+
public static let equal = Self { StaticString.equal.description }
138+
}
139+
140+
// MARK: Dependency
141+
142+
extension FlagFormatter: TestDependencyKey {
143+
public static let testValue: FlagFormatter = .unimplemented
144+
}
145+
146+
extension DependencyValues {
147+
public var flagFormatter: FlagFormatter {
148+
get { self[FlagFormatter.self] }
149+
set { self[FlagFormatter.self] = newValue }
150+
}
151+
}
152+
153+
extension FlagFormatter {
154+
public static let unimplemented: FlagFormatter = XCTestDynamicOverlay.unimplemented(placeholder: FlagFormatter())
155+
}
156+
157+
extension OptionFormatter: TestDependencyKey {
158+
public static let testValue: OptionFormatter = .unimplemented
159+
}
160+
161+
extension DependencyValues {
162+
public var optionFormatter: OptionFormatter {
163+
get { self[OptionFormatter.self] }
164+
set { self[OptionFormatter.self] = newValue }
165+
}
166+
}
167+
168+
extension OptionFormatter {
169+
public static let unimplemented: OptionFormatter = XCTestDynamicOverlay
170+
.unimplemented(placeholder: OptionFormatter())
171+
}

Sources/ArgumentEncoding/Option.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import Foundation
2222
/// ```swift
2323
/// struct Container: ArgumentGroup, FormatterNode {
2424
/// let flagFormatter: FlagFormatter = .doubleDashPrefix
25-
/// let optionFormatter: OptionFormatter = .doubleDashPrefix
25+
/// let optionFormatter: OptionFormatter = OptionFormatter(prefix: .doubleDash)
2626
///
2727
/// @Option var name: String = "value"
2828
/// }
@@ -336,7 +336,7 @@ struct OptionEncoding {
336336
let value: String
337337

338338
func arguments() -> [String] {
339-
formatter.format(encoding: self)
339+
[formatter.format(encoding: self)]
340340
}
341341
}
342342

0 commit comments

Comments
 (0)