[GSoC] Conversation for required @Argument 2 (#457)

* Fix array parsing as supplemental input

* Add test for `ExpressibleByArgument`

* Add options for `CaseIterable`

* Add test for `@Argument(transform:)`
This commit is contained in:
Kth
2022-07-25 19:00:33 +09:00
committed by GitHub
parent d1d0eb4baa
commit eda9ea2dde
3 changed files with 182 additions and 21 deletions
@@ -22,7 +22,7 @@ struct CommandParser {
let commandTree: Tree<ParsableCommand.Type>
var currentNode: Tree<ParsableCommand.Type>
var decodedArguments: [DecodedArguments] = []
var lineStack: [String]?
var lineStack: [String]? = nil
var rootCommand: ParsableCommand.Type {
commandTree.element
@@ -37,7 +37,7 @@ struct CommandParser {
}
}
internal init(_ rootCommand: ParsableCommand.Type, lines: [String]?) {
internal init(_ rootCommand: ParsableCommand.Type, lines: [String]) {
self.init(rootCommand)
self.lineStack = lines
lineStack?.reverse()
@@ -158,7 +158,7 @@ extension CommandParser {
defaultCapturesAll: currentNode.element.defaultIncludesUnconditionalArguments)
} catch {
// Try to fix error by interacting with the user
guard canInteract(error: error, split: &split) else { throw error }
guard try canInteract(error: error, split: &split) else { throw error }
return try getValues()
}
}
@@ -173,7 +173,7 @@ extension CommandParser {
return (decoder, decodedResult)
} catch {
// Try to fix error by interacting with the user
if canInteract(error: error, arguments: commandArguments, values: &values) {
if try canInteract(error: error, arguments: commandArguments, values: &values) {
return try getDecoderAndResult()
} else {
// If decoding this command failed, see if they were asking for
@@ -15,16 +15,13 @@ extension CommandParser {
/// - error: A parsing error thrown by `lenientParse(_:subcommands:defaultCapturesAll:)`.
/// - split: A collection of parsed arguments which needs to be modified.
/// - Returns: Whether the dialog resolve the error.
mutating func canInteract(error: Error, split: inout SplitArguments) -> Bool {
mutating func canInteract(error: Error, split: inout SplitArguments) throws -> Bool {
guard lineStack == nil || !lineStack!.isEmpty else { return false }
guard let error = error as? ParserError else { return false }
guard case let .missingValueForOption(inputOrigin, name) = error else { return false }
var input = lineStack?.removeLast()
while input?.isEmpty ?? true {
print("? Please enter value for '\(name.synopsisString)': ", terminator: "")
input = readLine() ?? nil
}
print("? Please enter value for '\(name.synopsisString)': ", terminator: "")
let input = getInput()
let inputIndex = inputOrigin.elements.first!.baseIndex! + 1
split._elements.insert(.init(value: .value(input!),
@@ -46,7 +43,7 @@ extension CommandParser {
/// - arguments: A nested tree of argument definitions which can provide modification method.
/// - values: The resulting values after parsing the arguments which needs to be modified.
/// - Returns: Whether the dialog resolve the error.
mutating func canInteract(error: Error, arguments: ArgumentSet, values: inout ParsedValues) -> Bool {
mutating func canInteract(error: Error, arguments: ArgumentSet, values: inout ParsedValues) throws -> Bool {
guard lineStack == nil || !lineStack!.isEmpty else { return false }
guard let error = error as? ParserError else { return false }
@@ -55,22 +52,26 @@ extension CommandParser {
let label = key.rawValue
guard label != "generateCompletionScript" else { break }
// Extract the parameters used in the test.
var input = lineStack?.removeLast()
while input?.isEmpty ?? true {
print("? Please enter '\(label)': ", terminator: "")
input = readLine() ?? nil
}
// Retrieve the correct `ArgumentDefinition` for the required transformation
// before storing the new value received from the user.
guard let definition = arguments.content.first(where: { $0.valueName == label }) else { break }
let name = definition.names.first // (where: { $0.case == .long } )
guard case let .unary(update) = definition.update else { break }
let name = definition.names.first // (where: { $0.case == .long } )
let input: [String]
let allValues = definition.help.allValues
if allValues.isEmpty {
// Get normal value
input = getNormalValue(label: label)
} else {
allValues.enumerated().forEach { print("\($0 + 1). \($1)") }
// Get CaseIterable Enum
input = getCaseIterableEnum(label: label, allValues: allValues)
}
// Split array input like "1 2 3".
for input in input!.components(separatedBy: " ") {
try! update(InputOrigin(elements: [.interactive]), name, input, &values)
for element in input {
try update(InputOrigin(elements: [.interactive]), name, element, &values)
}
return true
@@ -80,4 +81,48 @@ extension CommandParser {
return false
}
fileprivate mutating func getNormalValue(label: String) -> [String] {
print("? Please enter '\(label)': ", terminator: "")
return getInput()?.components(separatedBy: " ") ?? [""]
}
fileprivate mutating func getCaseIterableEnum(label: String, allValues: [String]) -> [String] {
print("? Please select '\(label)': ", terminator: "")
let strs = getInput()?.components(separatedBy: " ") ?? [""]
var nums: [String] = []
let range = 1 ... allValues.count
for str in strs {
guard let index = Int(str) else {
print("Error: '\(str)' is not a serial number.\n")
return getCaseIterableEnum(label: label, allValues: allValues)
}
guard range.contains(index) else {
print("Error: '\(index)' is not in the range of \(range) \n")
return getCaseIterableEnum(label: label, allValues: allValues)
}
nums.append(allValues[index - 1])
}
if nums.count == 1 {
print("You select '\(nums[0])'.\n")
} else {
print("You select '\(nums.joined(separator: "', '"))'.\n")
}
return nums
}
fileprivate mutating func getInput() -> String? {
if lineStack != nil {
// Extract the parameters used in the test.
return lineStack!.removeLast()
} else {
// Get values from user input.
return readLine()
}
}
}
@@ -49,6 +49,50 @@ extension ArgumentInteractiveTests {
// MARK: -
private struct ExpressibleValue: ParsableCommand {
enum Mode: String, ExpressibleByArgument {
case foo, bar, baz
}
@Argument var mode: Mode
}
private struct TransformableValue: ParsableCommand {
enum Format: Equatable {
case text
case other(String)
init(_ string: String) throws {
if string == "text" {
self = .text
} else {
self = .other(string)
}
}
}
@Argument(transform: Format.init) var format: Format
}
extension ArgumentInteractiveTests {
func testParsing_ExpressibleValue() throws {
AssertParseCommand(ExpressibleValue.self, ExpressibleValue.self, [], lines: ["foo"]) { value in
XCTAssertEqual(value.mode, .foo)
}
}
func testParsing_TransformableValue() throws {
AssertParseCommand(TransformableValue.self, TransformableValue.self, [], lines: ["text"]) { value in
XCTAssertEqual(value.format, .text)
}
AssertParseCommand(TransformableValue.self, TransformableValue.self, [], lines: ["keynote"]) { value in
XCTAssertEqual(value.format, .other("keynote"))
}
}
}
// MARK: -
private struct StringArray: ParsableCommand {
@Argument var values: [String]
}
@@ -80,3 +124,75 @@ extension ArgumentInteractiveTests {
}
}
}
// MARK: -
private struct PositionalArray1: ParsableCommand {
@Argument var values: [Int]
@Option var count: Int
@Flag var verbose = false
}
private struct PositionalArray2: ParsableCommand {
@Option var count: Int
@Argument var values: [Int]
@Flag var verbose = false
}
private struct PositionalArray3: ParsableCommand {
@Option var count: Int
@Flag var verbose = false
@Argument var values: [Int]
}
extension ArgumentInteractiveTests {
func testParsing_PositionalArray() throws {
AssertParseCommand(PositionalArray1.self, PositionalArray1.self, ["--count", "3", "--verbose"], lines: ["1 2"]) { value in
XCTAssertEqual(value.count, 3)
XCTAssertEqual(value.verbose, true)
XCTAssertEqual(value.values, [1, 2])
}
AssertParseCommand(PositionalArray2.self, PositionalArray2.self, ["--count", "3", "--verbose"], lines: ["1 2"]) { value in
XCTAssertEqual(value.count, 3)
XCTAssertEqual(value.verbose, true)
XCTAssertEqual(value.values, [1, 2])
}
AssertParseCommand(PositionalArray3.self, PositionalArray3.self, ["--count", "3", "--verbose"], lines: ["1 2"]) { value in
XCTAssertEqual(value.count, 3)
XCTAssertEqual(value.verbose, true)
XCTAssertEqual(value.values, [1, 2])
}
}
}
// MARK: -
private struct CaseIterableArgument: ParsableCommand {
enum Mode: String, CaseIterable, ExpressibleByArgument {
case foo, bar, baz
}
@Argument var mode: Mode
@Argument var modes: [Mode]
}
extension ArgumentInteractiveTests {
func testParsing_CaseIterableArgument() throws {
AssertParseCommand(CaseIterableArgument.self, CaseIterableArgument.self, ["foo"], lines: ["2 3"]) { value in
XCTAssertEqual(value.mode, .foo)
XCTAssertEqual(value.modes, [.bar, .baz])
}
AssertParseCommand(CaseIterableArgument.self, CaseIterableArgument.self, [], lines: ["1", "2 3"]) { value in
XCTAssertEqual(value.mode, .foo)
XCTAssertEqual(value.modes, [.bar, .baz])
}
AssertParseCommand(CaseIterableArgument.self, CaseIterableArgument.self, [], lines: ["foo", "0", "2 3", "1"]) { value in
XCTAssertEqual(value.mode, .baz)
XCTAssertEqual(value.modes, [.foo])
}
}
}