From 4ea9880d0525284af427fa022e61ba3f36eaa820 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Wed, 14 Feb 2024 23:54:55 -0600 Subject: [PATCH 1/3] Add value formatting to OptionFormatter feature/option-value-formatting --- Sources/ArgumentEncoding/Formatters.swift | 16 ++++++++++++---- Tests/ArgumentEncodingTests/FormatterTests.swift | 7 +++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Sources/ArgumentEncoding/Formatters.swift b/Sources/ArgumentEncoding/Formatters.swift index 01988e0..99c1355 100644 --- a/Sources/ArgumentEncoding/Formatters.swift +++ b/Sources/ArgumentEncoding/Formatters.swift @@ -56,9 +56,10 @@ public struct OptionFormatter: Sendable { public let prefix: @Sendable () -> String public let body: @Sendable (_ key: String) -> String public let separator: @Sendable () -> String + public let value: @Sendable (_ value: String) -> String public func format(key: String, value: String) -> String { - prefix() + body(key) + separator() + value + prefix() + body(key) + separator() + self.value(value) } func format(encoding: OptionEncoding) -> String { @@ -71,14 +72,17 @@ public struct OptionFormatter: Sendable { /// - prefix: Closure that returns the prefix string /// - body: Closure that transforms the key string for formatting /// - separator: Closure that returns the string that separates the key and value + /// - value: Closure that transforms the value string for formatting public init( prefix: @escaping @Sendable () -> String, body: @escaping @Sendable (_ key: String) -> String, - separator: @escaping @Sendable () -> String + separator: @escaping @Sendable () -> String, + value: @escaping @Sendable (_ value: String) -> String ) { self.prefix = prefix self.body = body self.separator = separator + self.value = value } /// Initialize a new formatter @@ -87,15 +91,18 @@ public struct OptionFormatter: Sendable { /// - prefix: Name spaced closure that returns the prefix string for a Flag /// - body: Name spaced closure that transforms the key string for formatting /// - separator: Name spaced closure that returns the string that separates the key and value + /// - value: Name spaced closure that transforms the value string for formatting public init( prefix: PrefixFormatter = .empty, body: BodyFormatter = .empty, - separator: SeparatorFormatter = .space + separator: SeparatorFormatter = .space, + value: BodyFormatter = .empty ) { self.init( prefix: prefix.transform, body: body.transform, - separator: separator.transform + separator: separator.transform, + value: value.transform ) } } @@ -126,6 +133,7 @@ public struct BodyFormatter: Sendable { public static let empty = Self { $0 } public static let kebabCase = Self(CaseConverter.kebabCase) public static let snakeCase = Self(CaseConverter.snakeCase) + public static let singleQuote = Self { "'\($0)'" } } /// Name space for a closure that returns the separator string between an Option's key and value diff --git a/Tests/ArgumentEncodingTests/FormatterTests.swift b/Tests/ArgumentEncodingTests/FormatterTests.swift index edc770c..177ce64 100644 --- a/Tests/ArgumentEncodingTests/FormatterTests.swift +++ b/Tests/ArgumentEncodingTests/FormatterTests.swift @@ -87,4 +87,11 @@ final class FormatterTests: XCTestCase { "optionKey=optionValue" ) } + + func testOptionFormatterSingleQuoteValue() throws { + XCTAssertEqual( + OptionFormatter(value: .singleQuote).format(key: "optionKey", value: "optionValue"), + "optionKey 'optionValue'" + ) + } } From 75dfc22f660a7b18c03502ab42fd383d5be8efae Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 15 Feb 2024 08:43:01 -0600 Subject: [PATCH 2/3] feature/option-value-formatting - Change SeparatorFormatter to take the key and value as arguments and return an array of strings. This is what I think most would expect and works better with `Process` which seems to be picky about an option's key and value being separate elements in the array. - Rename BodyFormatter to KeyFormatter which is a more clear name - Rename SeparatorFormatter.space to separate which is a more clear name --- Sources/ArgumentEncoding/Formatters.swift | 56 +++++++++---------- Sources/ArgumentEncoding/Option.swift | 2 +- Sources/ArgumentEncoding/OptionSet.swift | 2 +- .../ArgumentGroupTests.swift | 10 ++-- .../CommandRepresentableTests.swift | 11 ++-- .../FormatterTests.swift | 22 ++++---- .../OptionSetTests.swift | 6 +- Tests/ArgumentEncodingTests/OptionTests.swift | 6 +- .../TopLevelCommandRepresentableTests.swift | 11 ++-- 9 files changed, 64 insertions(+), 62 deletions(-) diff --git a/Sources/ArgumentEncoding/Formatters.swift b/Sources/ArgumentEncoding/Formatters.swift index 99c1355..af4a35d 100644 --- a/Sources/ArgumentEncoding/Formatters.swift +++ b/Sources/ArgumentEncoding/Formatters.swift @@ -13,11 +13,11 @@ import XCTestDynamicOverlay public struct FlagFormatter: Sendable { /// Formats a key string public let prefix: @Sendable () -> String - public let body: @Sendable (_ key: String) -> String + public let key: @Sendable (_ key: String) -> String @Sendable public func format(key: String) -> String { - prefix() + body(key) + prefix() + self.key(key) } @Sendable @@ -29,24 +29,24 @@ public struct FlagFormatter: Sendable { /// /// - Parameters /// - prefix: Closure that returns the prefix string - /// - body: Closure that transforms the key string for formatting + /// - key: Closure that transforms the key string for formatting public init( prefix: @escaping @Sendable () -> String, - body: @escaping @Sendable (_ key: String) -> String + key: @escaping @Sendable (_ key: String) -> String ) { self.prefix = prefix - self.body = body + self.key = key } /// Initialize a new formatter /// /// - Parameters /// - prefix: Name spaced closure that returns the prefix string for a Flag - /// - body: Name spaced closure that transforms the key string for formatting - public init(prefix: PrefixFormatter = .empty, body: BodyFormatter = .empty) { + /// - key: Name spaced closure that transforms the key string for formatting + public init(prefix: PrefixFormatter = .empty, key: KeyFormatter = .empty) { self.init( prefix: prefix.transform, - body: body.transform + key: key.transform ) } } @@ -54,15 +54,15 @@ public struct FlagFormatter: Sendable { /// Formats `Option`s to match how different executables format arguments public struct OptionFormatter: Sendable { public let prefix: @Sendable () -> String - public let body: @Sendable (_ key: String) -> String - public let separator: @Sendable () -> String + public let key: @Sendable (_ key: String) -> String + public let separator: @Sendable (_ key: String, _ value: String) -> [String] public let value: @Sendable (_ value: String) -> String - public func format(key: String, value: String) -> String { - prefix() + body(key) + separator() + self.value(value) + public func format(key: String, value: String) -> [String] { + separator(prefix() + self.key(key), self.value(value)) } - func format(encoding: OptionEncoding) -> String { + func format(encoding: OptionEncoding) -> [String] { format(key: encoding.key, value: encoding.value) } @@ -70,17 +70,17 @@ public struct OptionFormatter: Sendable { /// /// - Parameters /// - prefix: Closure that returns the prefix string - /// - body: Closure that transforms the key string for formatting + /// - key: Closure that transforms the key string for formatting /// - separator: Closure that returns the string that separates the key and value /// - value: Closure that transforms the value string for formatting public init( prefix: @escaping @Sendable () -> String, - body: @escaping @Sendable (_ key: String) -> String, - separator: @escaping @Sendable () -> String, + key: @escaping @Sendable (_ key: String) -> String, + separator: @escaping @Sendable (_ key: String, _ value: String) -> [String], value: @escaping @Sendable (_ value: String) -> String ) { self.prefix = prefix - self.body = body + self.key = key self.separator = separator self.value = value } @@ -89,18 +89,18 @@ public struct OptionFormatter: Sendable { /// /// - Parameters /// - prefix: Name spaced closure that returns the prefix string for a Flag - /// - body: Name spaced closure that transforms the key string for formatting + /// - key: Name spaced closure that transforms the key string for formatting /// - separator: Name spaced closure that returns the string that separates the key and value /// - value: Name spaced closure that transforms the value string for formatting public init( prefix: PrefixFormatter = .empty, - body: BodyFormatter = .empty, - separator: SeparatorFormatter = .space, - value: BodyFormatter = .empty + key: KeyFormatter = .empty, + separator: SeparatorFormatter = .separate, + value: KeyFormatter = .empty ) { self.init( prefix: prefix.transform, - body: body.transform, + key: key.transform, separator: separator.transform, value: value.transform ) @@ -123,7 +123,7 @@ public struct PrefixFormatter: Sendable { } /// Name space for a closure that transforms a Flag or Option's key -public struct BodyFormatter: Sendable { +public struct KeyFormatter: Sendable { public let transform: @Sendable (_ key: String) -> String public init(_ transform: @escaping @Sendable (_ key: String) -> String) { @@ -136,16 +136,16 @@ public struct BodyFormatter: Sendable { public static let singleQuote = Self { "'\($0)'" } } -/// Name space for a closure that returns the separator string between an Option's key and value +/// Name space for a closure that returns the Option's key and value separated by a string or as separate elements in an array public struct SeparatorFormatter: Sendable { - public let transform: @Sendable () -> String + public let transform: @Sendable (_ key: String, _ value: String) -> [String] - public init(_ transform: @escaping @Sendable () -> String) { + public init(_ transform: @escaping @Sendable (_ key: String, _ value: String) -> [String]) { self.transform = transform } - public static let space = Self { StaticString.space.description } - public static let equal = Self { StaticString.equal.description } + public static let separate = Self { [$0, $1] } + public static let equal = Self { ["\($0)\(StaticString.equal.description)\($1)"] } } // MARK: Dependency diff --git a/Sources/ArgumentEncoding/Option.swift b/Sources/ArgumentEncoding/Option.swift index 647e466..3a3bc2c 100644 --- a/Sources/ArgumentEncoding/Option.swift +++ b/Sources/ArgumentEncoding/Option.swift @@ -338,7 +338,7 @@ struct OptionEncoding { let value: String func arguments() -> [String] { - [formatter.format(encoding: self)] + formatter.format(encoding: self) } } diff --git a/Sources/ArgumentEncoding/OptionSet.swift b/Sources/ArgumentEncoding/OptionSet.swift index fa9c840..0e28308 100644 --- a/Sources/ArgumentEncoding/OptionSet.swift +++ b/Sources/ArgumentEncoding/OptionSet.swift @@ -263,7 +263,7 @@ struct OptionSetEncoding { let values: [OptionEncoding] func arguments() -> [String] { - values.map { formatter.format(encoding: $0) } + values.flatMap { formatter.format(encoding: $0) } } } diff --git a/Tests/ArgumentEncodingTests/ArgumentGroupTests.swift b/Tests/ArgumentEncodingTests/ArgumentGroupTests.swift index 836b017..d086268 100644 --- a/Tests/ArgumentEncodingTests/ArgumentGroupTests.swift +++ b/Tests/ArgumentEncodingTests/ArgumentGroupTests.swift @@ -42,7 +42,7 @@ final class ArgumentGroupTests: XCTestCase { numThreads: 2, target: "target" ).arguments(), - ["--numThreads 2", "target"] + ["--numThreads", "2", "target"] ) XCTAssertEqual( @@ -51,7 +51,7 @@ final class ArgumentGroupTests: XCTestCase { numThreads: 0, target: "target" ).arguments(), - ["--asyncMain", "--numThreads 0", "target"] + ["--asyncMain", "--numThreads", "0", "target"] ) } @@ -106,7 +106,7 @@ final class ArgumentGroupTests: XCTestCase { target: "target" ) ).arguments(), - ["--numThreads 2", "target", "-configuration arm64", "target"] + ["--numThreads", "2", "target", "-configuration", "arm64", "target"] ) XCTAssertEqual( @@ -120,7 +120,7 @@ final class ArgumentGroupTests: XCTestCase { target: "target" ) ).arguments(), - ["--asyncMain", "--numThreads 1", "target", "-configuration x86_64", "-buildTests", "target"] + ["--asyncMain", "--numThreads", "1", "target", "-configuration", "x86_64", "-buildTests", "target"] ) } @@ -206,7 +206,7 @@ final class ArgumentGroupTests: XCTestCase { func testEnumGroupTest() throws { XCTAssertEqual( ParentEnumGroup.test(numWorkers: 2, testProduct: "PackageTarget").arguments(), - ["-numWorkers 2", "-testProduct PackageTarget"] + ["-numWorkers", "2", "-testProduct", "PackageTarget"] ) } diff --git a/Tests/ArgumentEncodingTests/CommandRepresentableTests.swift b/Tests/ArgumentEncodingTests/CommandRepresentableTests.swift index c2b4711..a17fdd6 100644 --- a/Tests/ArgumentEncodingTests/CommandRepresentableTests.swift +++ b/Tests/ArgumentEncodingTests/CommandRepresentableTests.swift @@ -53,7 +53,8 @@ final class CommandRepresentableTests: XCTestCase { )).arguments(), [ "command", - "--product Target", + "--product", + "Target", ] ) @@ -116,7 +117,7 @@ final class CommandRepresentableTests: XCTestCase { buildTests: true ) )).arguments(), - ["command", "--product OtherTarget", "child", "-configuration arm64", "-buildTests"] + ["command", "--product", "OtherTarget", "child", "-configuration", "arm64", "-buildTests"] ) XCTAssertEqual( @@ -128,7 +129,7 @@ final class CommandRepresentableTests: XCTestCase { buildTests: false ) )).arguments(), - ["command", "--verbose", "child", "-configuration x86_64"] + ["command", "--verbose", "child", "-configuration", "x86_64"] ) } @@ -165,14 +166,14 @@ final class CommandRepresentableTests: XCTestCase { func testEnumTest() throws { XCTAssertEqual( ParentEnumCommand.test(numWorkers: 2, testProduct: "PackageTarget").arguments(), - ["test", "-numWorkers 2", "-testProduct PackageTarget"] + ["test", "-numWorkers", "2", "-testProduct", "PackageTarget"] ) } func testEnumChild() throws { XCTAssertEqual( ParentEnumCommand.child(ChildCommand(configuration: .arm64, buildTests: true)).arguments(), - ["child", "-configuration arm64", "-buildTests"] + ["child", "-configuration", "arm64", "-buildTests"] ) } } diff --git a/Tests/ArgumentEncodingTests/FormatterTests.swift b/Tests/ArgumentEncodingTests/FormatterTests.swift index 177ce64..aaa32f9 100644 --- a/Tests/ArgumentEncodingTests/FormatterTests.swift +++ b/Tests/ArgumentEncodingTests/FormatterTests.swift @@ -34,14 +34,14 @@ final class FormatterTests: XCTestCase { func testFlagFormatterKebabCaseBody() throws { XCTAssertEqual( - FlagFormatter(body: .kebabCase).format(key: "flagKey"), + FlagFormatter(key: .kebabCase).format(key: "flagKey"), "flag-key" ) } func testFlagFormatterSnakeCaseBody() throws { XCTAssertEqual( - FlagFormatter(body: .snakeCase).format(key: "flagKey"), + FlagFormatter(key: .snakeCase).format(key: "flagKey"), "flag_key" ) } @@ -49,49 +49,49 @@ final class FormatterTests: XCTestCase { func testOptionFormatterSingleDashPrefix() throws { XCTAssertEqual( OptionFormatter(prefix: .singleDash).format(key: "optionKey", value: "optionValue"), - "-optionKey optionValue" + ["-optionKey", "optionValue"] ) } func testOptionFormatterDoubleDashPrefix() throws { XCTAssertEqual( OptionFormatter(prefix: .doubleDash).format(key: "optionKey", value: "optionValue"), - "--optionKey optionValue" + ["--optionKey", "optionValue"] ) } func testOptionFormatterEmptyPrefix() throws { XCTAssertEqual( OptionFormatter(prefix: .empty).format(key: "optionKey", value: "optionValue"), - "optionKey optionValue" + ["optionKey", "optionValue"] ) } func testOptionFormatterKebabCaseBody() throws { XCTAssertEqual( - OptionFormatter(body: .kebabCase).format(key: "optionKey", value: "optionValue"), - "option-key optionValue" + OptionFormatter(key: .kebabCase).format(key: "optionKey", value: "optionValue"), + ["option-key", "optionValue"] ) } func testOptionFormatterSnakeCaseBody() throws { XCTAssertEqual( - OptionFormatter(body: .snakeCase).format(key: "optionKey", value: "optionValue"), - "option_key optionValue" + OptionFormatter(key: .snakeCase).format(key: "optionKey", value: "optionValue"), + ["option_key", "optionValue"] ) } func testOptionFormatterEqualSeparator() throws { XCTAssertEqual( OptionFormatter(separator: .equal).format(key: "optionKey", value: "optionValue"), - "optionKey=optionValue" + ["optionKey=optionValue"] ) } func testOptionFormatterSingleQuoteValue() throws { XCTAssertEqual( OptionFormatter(value: .singleQuote).format(key: "optionKey", value: "optionValue"), - "optionKey 'optionValue'" + ["optionKey", "'optionValue'"] ) } } diff --git a/Tests/ArgumentEncodingTests/OptionSetTests.swift b/Tests/ArgumentEncodingTests/OptionSetTests.swift index 996d151..31cf6d6 100644 --- a/Tests/ArgumentEncodingTests/OptionSetTests.swift +++ b/Tests/ArgumentEncodingTests/OptionSetTests.swift @@ -18,7 +18,7 @@ final class OptionSetTests: XCTestCase { } operation: { optionSet.arguments() } - XCTAssertEqual(args, ["--configuration release", "--configuration debug"]) + XCTAssertEqual(args, ["--configuration", "release", "--configuration", "debug"]) } func testBothRawValueAndStringConvertible() throws { @@ -34,7 +34,7 @@ final class OptionSetTests: XCTestCase { } operation: { optionSet.arguments() } - XCTAssertEqual(args, ["--configuration release", "--configuration debug"]) + XCTAssertEqual(args, ["--configuration", "release", "--configuration", "debug"]) } func testBothRawValueAndStringConvertibleContainer() throws { @@ -47,7 +47,7 @@ final class OptionSetTests: XCTestCase { } operation: { container.arguments() } - XCTAssertEqual(args, ["--configuration release", "--configuration debug"]) + XCTAssertEqual(args, ["--configuration", "release", "--configuration", "debug"]) } } diff --git a/Tests/ArgumentEncodingTests/OptionTests.swift b/Tests/ArgumentEncodingTests/OptionTests.swift index 1ab11c8..7e87a9b 100644 --- a/Tests/ArgumentEncodingTests/OptionTests.swift +++ b/Tests/ArgumentEncodingTests/OptionTests.swift @@ -18,7 +18,7 @@ final class OptionTests: XCTestCase { } operation: { option.arguments() } - XCTAssertEqual(args, ["--configuration release"]) + XCTAssertEqual(args, ["--configuration", "release"]) } func testBothRawValueAndStringConvertible() throws { @@ -28,7 +28,7 @@ final class OptionTests: XCTestCase { } operation: { option.arguments() } - XCTAssertEqual(args, ["--configuration release"]) + XCTAssertEqual(args, ["--configuration", "release"]) } func testBothRawValueAndStringConvertibleContainer() throws { @@ -38,7 +38,7 @@ final class OptionTests: XCTestCase { } operation: { container.arguments() } - XCTAssertEqual(args, ["--configuration release"]) + XCTAssertEqual(args, ["--configuration", "release"]) } } diff --git a/Tests/ArgumentEncodingTests/TopLevelCommandRepresentableTests.swift b/Tests/ArgumentEncodingTests/TopLevelCommandRepresentableTests.swift index e1900b3..1072eef 100644 --- a/Tests/ArgumentEncodingTests/TopLevelCommandRepresentableTests.swift +++ b/Tests/ArgumentEncodingTests/TopLevelCommandRepresentableTests.swift @@ -44,7 +44,8 @@ final class TopLevelCommandRepresentableTests: XCTestCase { ).arguments(), [ "swift", - "--product Target", + "--product", + "Target", ] ) @@ -107,7 +108,7 @@ final class TopLevelCommandRepresentableTests: XCTestCase { buildTests: true ) ).arguments(), - ["parent", "--product OtherTarget", "child", "-configuration arm64", "-buildTests"] + ["parent", "--product", "OtherTarget", "child", "-configuration", "arm64", "-buildTests"] ) XCTAssertEqual( @@ -119,7 +120,7 @@ final class TopLevelCommandRepresentableTests: XCTestCase { buildTests: false ) ).arguments(), - ["parent", "--verbose", "child", "-configuration x86_64"] + ["parent", "--verbose", "child", "-configuration", "x86_64"] ) } @@ -178,14 +179,14 @@ final class TopLevelCommandRepresentableTests: XCTestCase { func testEnumTest() throws { XCTAssertEqual( ParentEnumCommand.test(numWorkers: 2, testProduct: "PackageTarget").arguments(), - ["parent", "-numWorkers 2", "-testProduct PackageTarget"] + ["parent", "-numWorkers", "2", "-testProduct", "PackageTarget"] ) } func testEnumChild() throws { XCTAssertEqual( ParentEnumCommand.child(ChildEnumCommand(configuration: .arm64, buildTests: true)).arguments(), - ["parent", "child", "-configuration arm64", "-buildTests"] + ["parent", "child", "-configuration", "arm64", "-buildTests"] ) } } From ef4335eae098068575d35863805431c52ad23963 Mon Sep 17 00:00:00 2001 From: Andrew Roan Date: Thu, 15 Feb 2024 08:44:09 -0600 Subject: [PATCH 3/3] Run swiftformat feature/option-value-formatting --- Sources/ArgumentEncoding/Formatters.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ArgumentEncoding/Formatters.swift b/Sources/ArgumentEncoding/Formatters.swift index af4a35d..f73147f 100644 --- a/Sources/ArgumentEncoding/Formatters.swift +++ b/Sources/ArgumentEncoding/Formatters.swift @@ -136,7 +136,8 @@ public struct KeyFormatter: Sendable { public static let singleQuote = Self { "'\($0)'" } } -/// Name space for a closure that returns the Option's key and value separated by a string or as separate elements in an array +/// Name space for a closure that returns the Option's key and value separated by a string or as separate elements in an +/// array public struct SeparatorFormatter: Sendable { public let transform: @Sendable (_ key: String, _ value: String) -> [String]