Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 7 additions & 24 deletions Sources/Commandant/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -124,38 +124,21 @@ internal func informativeUsageError<T: ArgumentType, ClientError>(argument: Argu
/// Constructs an error that describes how to use the option, with the given
/// example of key (and value, if applicable) usage.
internal func informativeUsageError<T, ClientError>(keyValueExample: String, option: Option<T>) -> CommandantError<ClientError> {
if option.defaultValue != nil {
return informativeUsageError("[\(keyValueExample)]", usage: option.usage)
} else {
return informativeUsageError(keyValueExample, usage: option.usage)
}
return informativeUsageError("[\(keyValueExample)]", usage: option.usage)
}

/// Constructs an error that describes how to use the option.
internal func informativeUsageError<T: ArgumentType, ClientError>(option: Option<T>) -> CommandantError<ClientError> {
var example = "--\(option.key) "

var valueExample = ""
if let defaultValue = option.defaultValue {
valueExample = "\(defaultValue)"
}

if valueExample.isEmpty {
example += "(\(T.name))"
} else {
example += valueExample
}
return informativeUsageError("--\(option.key) \(option.defaultValue)", option: option)
}

return informativeUsageError(example, option: option)
/// Constructs an error that describes how to use the option.
internal func informativeUsageError<T: ArgumentType, ClientError>(option: Option<T?>) -> CommandantError<ClientError> {
return informativeUsageError("--\(option.key) (\(T.name))", option: option)
}

/// Constructs an error that describes how to use the given boolean option.
internal func informativeUsageError<ClientError>(option: Option<Bool>) -> CommandantError<ClientError> {
let key = option.key

if let defaultValue = option.defaultValue {
return informativeUsageError((defaultValue ? "--no-\(key)" : "--\(key)"), option: option)
} else {
return informativeUsageError("--(no-)\(key)", option: option)
}
return informativeUsageError((option.defaultValue ? "--no-\(key)" : "--\(key)"), option: option)
}
40 changes: 20 additions & 20 deletions Sources/Commandant/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ public struct Option<T> {

/// The default value for this option. This is the value that will be used
/// if the option is never explicitly specified on the command line.
///
/// If this is nil, this option is always required.
public let defaultValue: T?
public let defaultValue: T

/// A human-readable string describing the purpose of this option. This will
/// be shown in help messages.
Expand All @@ -72,18 +70,11 @@ public struct Option<T> {
/// differently from the default).
public let usage: String

public init(key: String, defaultValue: T? = nil, usage: String) {
public init(key: String, defaultValue: T, usage: String) {
self.key = key
self.defaultValue = defaultValue
self.usage = usage
}

/// Constructs an `InvalidArgument` error that describes how the option was
/// used incorrectly. `value` should be the invalid value given by the user.
private func invalidUsageError<ClientError>(value: String) -> CommandantError<ClientError> {
let description = "Invalid value for '\(self)': \(value)"
return .UsageError(description: description)
}
}

extension Option: CustomStringConvertible {
Expand Down Expand Up @@ -161,10 +152,22 @@ public func <*> <T, U, ClientError>(f: Result<(T -> U), CommandantError<ClientEr
/// If parsing command line arguments, and no value was specified on the command
/// line, the option's `defaultValue` is used.
public func <| <T: ArgumentType, ClientError>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError<ClientError>> {
let wrapped = Option<T?>(key: option.key, defaultValue: option.defaultValue, usage: option.usage)
// Since we are passing a non-nil default value, we can safely unwrap the
// result.
return (mode <| wrapped).map { $0! }
}

/// Evaluates the given option in the given mode.
///
/// If parsing command line arguments, and no value was specified on the command
/// line, `nil` is used.
public func <| <T: ArgumentType, ClientError>(mode: CommandMode, option: Option<T?>) -> Result<T?, CommandantError<ClientError>> {
let key = option.key
switch mode {
case let .Arguments(arguments):
var stringValue: String?
switch arguments.consumeValueForKey(option.key) {
switch arguments.consumeValueForKey(key) {
case let .Success(value):
stringValue = value

Expand All @@ -182,12 +185,11 @@ public func <| <T: ArgumentType, ClientError>(mode: CommandMode, option: Option<
if let value = T.fromString(stringValue) {
return .Success(value)
}

return .Failure(option.invalidUsageError(stringValue))
} else if let defaultValue = option.defaultValue {
return .Success(defaultValue)

let description = "Invalid value for '--\(key)': \(stringValue)"
return .Failure(.UsageError(description: description))
} else {
return .Failure(missingArgumentError(option.description))
return .Success(option.defaultValue)
}

case .Usage:
Expand All @@ -204,10 +206,8 @@ public func <| <ClientError>(mode: CommandMode, option: Option<Bool>) -> Result<
case let .Arguments(arguments):
if let value = arguments.consumeBooleanKey(option.key) {
return .Success(value)
} else if let defaultValue = option.defaultValue {
return .Success(defaultValue)
} else {
return .Failure(missingArgumentError(option.description))
return .Success(option.defaultValue)
}

case .Usage:
Expand Down
32 changes: 17 additions & 15 deletions Tests/OptionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,49 +29,49 @@ class OptionsTypeSpec: QuickSpec {

it("should succeed without optional arguments") {
let value = tryArguments("required").value
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalFilename: "filename", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalStringValue: nil, optionalFilename: "filename", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
expect(value).to(equal(expected))
}

it("should succeed with some optional arguments") {
let value = tryArguments("required", "--intValue", "3", "fuzzbuzz").value
let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
let value = tryArguments("required", "--intValue", "3", "--optionalStringValue", "baz", "fuzzbuzz").value
let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalStringValue: "baz", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
expect(value).to(equal(expected))
}

it("should override previous optional arguments") {
let value = tryArguments("required", "--intValue", "3", "--stringValue", "fuzzbuzz", "--intValue", "5", "--stringValue", "bazbuzz").value
let expected = TestOptions(intValue: 5, stringValue: "bazbuzz", optionalFilename: "filename", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
let expected = TestOptions(intValue: 5, stringValue: "bazbuzz", optionalStringValue: nil, optionalFilename: "filename", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
expect(value).to(equal(expected))
}

it("should enable a boolean flag") {
let value = tryArguments("required", "--enabled", "--intValue", "3", "fuzzbuzz").value
let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: true, force: false, glob: false, arguments: [])
let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalStringValue: nil, optionalFilename: "fuzzbuzz", requiredName: "required", enabled: true, force: false, glob: false, arguments: [])
expect(value).to(equal(expected))
}

it("should re-disable a boolean flag") {
let value = tryArguments("required", "--enabled", "--no-enabled", "--intValue", "3", "fuzzbuzz").value
let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalStringValue: nil, optionalFilename: "fuzzbuzz", requiredName: "required", enabled: false, force: false, glob: false, arguments: [])
expect(value).to(equal(expected))
}

it("should enable multiple boolean flags") {
let value = tryArguments("required", "-fg").value
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalFilename: "filename", requiredName: "required", enabled: false, force: true, glob: true, arguments: [])
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalStringValue: nil, optionalFilename: "filename", requiredName: "required", enabled: false, force: true, glob: true, arguments: [])
expect(value).to(equal(expected))
}

it("should consume the rest of positional arguments") {
let value = tryArguments("required", "optional", "value1", "value2").value
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalFilename: "optional", requiredName: "required", enabled: false, force: false, glob: false, arguments: [ "value1", "value2" ])
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalStringValue: nil, optionalFilename: "optional", requiredName: "required", enabled: false, force: false, glob: false, arguments: [ "value1", "value2" ])
expect(value).to(equal(expected))
}

it("should treat -- as the end of valued options") {
let value = tryArguments("--", "--intValue").value
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalFilename: "filename", requiredName: "--intValue", enabled: false, force: false, glob: false, arguments: [])
let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalStringValue: nil, optionalFilename: "filename", requiredName: "--intValue", enabled: false, force: false, glob: false, arguments: [])
expect(value).to(equal(expected))
}
}
Expand All @@ -91,6 +91,7 @@ class OptionsTypeSpec: QuickSpec {
struct TestOptions: OptionsType, Equatable {
let intValue: Int
let stringValue: String
let optionalStringValue: String?
let optionalFilename: String
let requiredName: String
let enabled: Bool
Expand All @@ -100,16 +101,17 @@ struct TestOptions: OptionsType, Equatable {

typealias ClientError = NoError

static func create(a: Int) -> String -> String -> String -> Bool -> Bool -> Bool -> [String] -> TestOptions {
return { b in { c in { d in { e in { f in { g in { h in
return self.init(intValue: a, stringValue: b, optionalFilename: d, requiredName: c, enabled: e, force: f, glob: g, arguments: h)
} } } } } } }
static func create(a: Int) -> String -> String? -> String -> String -> Bool -> Bool -> Bool -> [String] -> TestOptions {
return { b in { c in { d in { e in { f in { g in { h in { i in
return self.init(intValue: a, stringValue: b, optionalStringValue: c, optionalFilename: e, requiredName: d, enabled: f, force: g, glob: h, arguments: i)
} } } } } } } }
}

static func evaluate(m: CommandMode) -> Result<TestOptions, CommandantError<NoError>> {
return create
<*> m <| Option(key: "intValue", defaultValue: 42, usage: "Some integer value")
<*> m <| Option(key: "stringValue", defaultValue: "foobar", usage: "Some string value")
<*> m <| Option<String?>(key: "optionalStringValue", defaultValue: nil, usage: "Some string value")
<*> m <| Argument(usage: "A name you're required to specify")
<*> m <| Argument(defaultValue: "filename", usage: "A filename that you can optionally specify")
<*> m <| Option(key: "enabled", defaultValue: false, usage: "Whether to be enabled")
Expand All @@ -120,11 +122,11 @@ struct TestOptions: OptionsType, Equatable {
}

func ==(lhs: TestOptions, rhs: TestOptions) -> Bool {
return lhs.intValue == rhs.intValue && lhs.stringValue == rhs.stringValue && lhs.optionalFilename == rhs.optionalFilename && lhs.requiredName == rhs.requiredName && lhs.enabled == rhs.enabled && lhs.force == rhs.force && lhs.glob == rhs.glob && lhs.arguments == rhs.arguments
return lhs.intValue == rhs.intValue && lhs.stringValue == rhs.stringValue && lhs.optionalStringValue == rhs.optionalStringValue && lhs.optionalFilename == rhs.optionalFilename && lhs.requiredName == rhs.requiredName && lhs.enabled == rhs.enabled && lhs.force == rhs.force && lhs.glob == rhs.glob && lhs.arguments == rhs.arguments
}

extension TestOptions: CustomStringConvertible {
var description: String {
return "{ intValue: \(intValue), stringValue: \(stringValue), optionalFilename: \(optionalFilename), requiredName: \(requiredName), enabled: \(enabled), force: \(force), glob: \(glob), arguments: \(arguments) }"
return "{ intValue: \(intValue), stringValue: \(stringValue), optionalStringValue: \(optionalStringValue), optionalFilename: \(optionalFilename), requiredName: \(requiredName), enabled: \(enabled), force: \(force), glob: \(glob), arguments: \(arguments) }"
}
}