diff --git a/.gitmodules b/.gitmodules index 8b5e765..7603397 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "Carthage.checkout/LlamaKit"] path = Carthage/Checkouts/LlamaKit - url = https://github.com/Carthage/LlamaKit.git + url = https://github.com/LlamaKit/LlamaKit.git [submodule "Carthage.checkout/Nimble"] path = Carthage/Checkouts/Nimble url = https://github.com/Quick/Nimble.git diff --git a/Cartfile b/Cartfile index a6379f1..938a548 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1 @@ -github "Carthage/LlamaKit" == 0.1.1 +github "LlamaKit/LlamaKit" == 0.5 diff --git a/Cartfile.private b/Cartfile.private index 03c8b50..c6bd3e5 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,3 +1,3 @@ -github "Quick/Quick" == 0.2.0 -github "Quick/Nimble" +github "Quick/Quick" ~> 0.2 +github "Quick/Nimble" ~> 0.3 github "jspahrsummers/xcconfigs" >= 0.6 diff --git a/Cartfile.resolved b/Cartfile.resolved index 82c826b..fff0ccc 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -github "Carthage/LlamaKit" "carthage-0.1.1" -github "Quick/Nimble" "v0.2.0" -github "Quick/Quick" "v0.2.0" -github "jspahrsummers/xcconfigs" "0.7" +github "LlamaKit/LlamaKit" "v0.5.0" +github "Quick/Nimble" "v0.3.0" +github "Quick/Quick" "v0.2.2" +github "jspahrsummers/xcconfigs" "0.7.2" diff --git a/Carthage/Checkouts/LlamaKit b/Carthage/Checkouts/LlamaKit index ac2c2d3..e37b966 160000 --- a/Carthage/Checkouts/LlamaKit +++ b/Carthage/Checkouts/LlamaKit @@ -1 +1 @@ -Subproject commit ac2c2d36bf14bdddd0c1c489df67ad9a7ad04eb4 +Subproject commit e37b966998df6ca05445c0b5d9c6c9560f1e7b61 diff --git a/Carthage/Checkouts/Nimble b/Carthage/Checkouts/Nimble index 81a2d8a..aeb5da7 160000 --- a/Carthage/Checkouts/Nimble +++ b/Carthage/Checkouts/Nimble @@ -1 +1 @@ -Subproject commit 81a2d8a63083ae6512d40f7a02d5e075e57be317 +Subproject commit aeb5da7bed72483d22ccfef5310ccdd3dd39a06f diff --git a/Carthage/Checkouts/Quick b/Carthage/Checkouts/Quick index 315ae2a..b0e9828 160000 --- a/Carthage/Checkouts/Quick +++ b/Carthage/Checkouts/Quick @@ -1 +1 @@ -Subproject commit 315ae2a4a630156d5ac094ae1f107af60edce21d +Subproject commit b0e98286db7e9d1f1e73cd4e69eac2aae2a1e3f8 diff --git a/Carthage/Checkouts/xcconfigs b/Carthage/Checkouts/xcconfigs index 6c7a02a..2e77204 160000 --- a/Carthage/Checkouts/xcconfigs +++ b/Carthage/Checkouts/xcconfigs @@ -1 +1 @@ -Subproject commit 6c7a02a43954c74d2be1d2fcf7bfd65a263e7c52 +Subproject commit 2e77204b59c3d97c24e5dd34966fb32c231194f0 diff --git a/Commandant/ArgumentParser.swift b/Commandant/ArgumentParser.swift index df4f9c3..73fb870 100644 --- a/Commandant/ArgumentParser.swift +++ b/Commandant/ArgumentParser.swift @@ -100,7 +100,7 @@ public final class ArgumentParser { /// /// If a value is found, the key and the value are both removed from the /// list of arguments remaining to be parsed. - internal func consumeValueForKey(key: String) -> Result { + internal func consumeValueForKey(key: String) -> Result { let oldArguments = rawArguments rawArguments.removeAll() diff --git a/Commandant/Command.swift b/Commandant/Command.swift index 41e99e6..03cfc98 100644 --- a/Commandant/Command.swift +++ b/Commandant/Command.swift @@ -20,7 +20,7 @@ public protocol CommandType { var function: String { get } /// Runs this subcommand in the given mode. - func run(mode: CommandMode) -> Result<()> + func run(mode: CommandMode) -> Result<(), CommandantError> } /// Describes the "mode" in which a command should run. @@ -56,7 +56,7 @@ public final class CommandRegistry { /// arguments. /// /// Returns the results of the execution, or nil if no such command exists. - public func runCommand(verb: String, arguments: [String]) -> Result<()>? { + public func runCommand(verb: String, arguments: [String]) -> Result<(), CommandantError>? { return self[verb]?.run(.Arguments(ArgumentParser(arguments))) } @@ -81,7 +81,7 @@ extension CommandRegistry { /// If a matching command could not be found, a helpful error message will /// be written to `stderr`, then the process will exit with a failure error /// code. - @noreturn public func main(#defaultCommand: CommandType, errorHandler: NSError -> ()) { + @noreturn public func main(#defaultCommand: CommandType, errorHandler: CommandantError -> ()) { var arguments = Process.arguments assert(arguments.count >= 1) @@ -100,7 +100,7 @@ extension CommandRegistry { exit(EXIT_SUCCESS) case let .Some(.Failure(error)): - errorHandler(error) + errorHandler(error.unbox) exit(EXIT_FAILURE) case .None: diff --git a/Commandant/Errors.swift b/Commandant/Errors.swift index 7db7adb..78c5dc6 100644 --- a/Commandant/Errors.swift +++ b/Commandant/Errors.swift @@ -8,25 +8,41 @@ import Foundation -/// The domain for all errors originating within Commandant. -public let CommandantErrorDomain: NSString = "org.carthage.Commandant" +/// Possible errors that can originate from Commandant. +public enum CommandantError { + /// An option was used incorrectly. + case UsageError(description: String) + + /// Creates an NSError that represents the receiver. + public func toNSError() -> NSError { + let domain = "org.carthage.Commandant" + + switch self { + case let .UsageError(description): + return NSError(domain: domain, code: 0, userInfo: [ NSLocalizedDescriptionKey: description ]) + } + } +} -/// Possible error codes within `CommandantErrorDomain`. -public enum CommandantError: Int { - /// One or more arguments was invalid. - case InvalidArgument +extension CommandantError: Printable { + public var description: String { + switch self { + case let .UsageError(description): + return description + } + } } /// Constructs an `InvalidArgument` error that indicates a missing value for /// the argument by the given name. -internal func missingArgumentError(argumentName: String) -> NSError { +internal func missingArgumentError(argumentName: String) -> CommandantError { let description = "Missing argument for \(argumentName)" - return NSError(domain: CommandantErrorDomain, code: CommandantError.InvalidArgument.rawValue, userInfo: [ NSLocalizedDescriptionKey: description ]) + return CommandantError.UsageError(description: description) } -/// Constructs an `InvalidArgument` error that describes how to use the -/// option, with the given example of key (and value, if applicable) usage. -internal func informativeUsageError(keyValueExample: String, option: Option) -> NSError { +/// Constructs an error that describes how to use the option, with the given +/// example of key (and value, if applicable) usage. +internal func informativeUsageError(keyValueExample: String, option: Option) -> CommandantError { var description = "" if option.defaultValue != nil { @@ -43,13 +59,12 @@ internal func informativeUsageError(keyValueExample: String, option: Option(option: Option) -> NSError { +/// Constructs an error that describes how to use the option. +internal func informativeUsageError(option: Option) -> CommandantError { var example = "" if let key = option.key { @@ -70,9 +85,8 @@ internal func informativeUsageError(option: Option) -> NSErr return informativeUsageError(example, option) } -/// Constructs an `InvalidArgument` error that describes how to use the -/// given boolean option. -internal func informativeUsageError(option: Option) -> NSError { +/// Constructs an error that describes how to use the given boolean option. +internal func informativeUsageError(option: Option) -> CommandantError { precondition(option.key != nil) let key = option.key! @@ -84,23 +98,18 @@ internal func informativeUsageError(option: Option) -> NSError { } } -/// Combines the text of the two errors, if they're both `InvalidArgument` -/// errors. Otherwise, uses whichever one is not (biased toward the left). -internal func combineUsageErrors(left: NSError, right: NSError) -> NSError { - let combinedDescription = "\(left.localizedDescription)\n\n\(right.localizedDescription)" - let combinedError = NSError(domain: CommandantErrorDomain, code: CommandantError.InvalidArgument.rawValue, userInfo: [ NSLocalizedDescriptionKey: combinedDescription ]) - - func isUsageError(error: NSError) -> Bool { - return error.domain == combinedError.domain && error.code == combinedError.code - } - - if isUsageError(left) { - if isUsageError(right) { - return combinedError - } else { - return right - } - } else { - return left +/// Combines the text of the two errors, if they're both `UsageError`s. +/// Otherwise, uses whichever one is not (biased toward the left). +internal func combineUsageErrors(lhs: CommandantError, rhs: CommandantError) -> CommandantError { + switch (lhs, rhs) { + case let (.UsageError(left), .UsageError(right)): + let combinedDescription = "\(left)\n\n\(right)" + return CommandantError.UsageError(description: combinedDescription) + + case (.UsageError, _): + return rhs + + case (_, .UsageError), (_, _): + return lhs } } diff --git a/Commandant/HelpCommand.swift b/Commandant/HelpCommand.swift index 66a53ac..065b2b9 100644 --- a/Commandant/HelpCommand.swift +++ b/Commandant/HelpCommand.swift @@ -30,7 +30,7 @@ public struct HelpCommand: CommandType { self.registry = registry } - public func run(mode: CommandMode) -> Result<()> { + public func run(mode: CommandMode) -> Result<(), CommandantError> { return HelpOptions.evaluate(mode) .flatMap { options in if let verb = options.verb { @@ -68,7 +68,7 @@ private struct HelpOptions: OptionsType { return self(verb: (verb == "" ? nil : verb)) } - static func evaluate(m: CommandMode) -> Result { + static func evaluate(m: CommandMode) -> Result { return create <*> m <| Option(defaultValue: "", usage: "the command to display help for") } diff --git a/Commandant/Option.swift b/Commandant/Option.swift index 47d1467..de70a70 100644 --- a/Commandant/Option.swift +++ b/Commandant/Option.swift @@ -38,7 +38,7 @@ public protocol OptionsType { /// /// Returns the parsed options, or an `InvalidArgument` error containing /// usage information. - class func evaluate(m: CommandMode) -> Result + class func evaluate(m: CommandMode) -> Result } /// Describes an option that can be provided on the command line. @@ -74,9 +74,9 @@ public struct Option { /// 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(value: String) -> NSError { + private func invalidUsageError(value: String) -> CommandantError { let description = "Invalid value for '\(self)': \(value)" - return NSError(domain: CommandantErrorDomain, code: CommandantError.InvalidArgument.rawValue, userInfo: [ NSLocalizedDescriptionKey: description ]) + return CommandantError.UsageError(description: description) } } @@ -154,7 +154,7 @@ infix operator <| { /// /// In the context of command-line option parsing, this is used to chain /// together the parsing of multiple arguments. See OptionsType for an example. -public func <*>(f: T -> U, value: Result) -> Result { +public func <*>(f: T -> U, value: Result) -> Result { return value.map(f) } @@ -162,16 +162,16 @@ public func <*>(f: T -> U, value: Result) -> Result { /// /// In the context of command-line option parsing, this is used to chain /// together the parsing of multiple arguments. See OptionsType for an example. -public func <*>(f: Result<(T -> U)>, value: Result) -> Result { +public func <*>(f: Result<(T -> U), CommandantError>, value: Result) -> Result { switch (f, value) { case let (.Failure(left), .Failure(right)): - return failure(combineUsageErrors(left, right)) + return failure(combineUsageErrors(left.unbox, right.unbox)) case let (.Failure(left), .Success): - return failure(left) + return failure(left.unbox) case let (.Success, .Failure(right)): - return failure(right) + return failure(right.unbox) case let (.Success(f), .Success(value)): let newValue = f.unbox(value.unbox) @@ -183,7 +183,7 @@ public func <*>(f: Result<(T -> U)>, value: Result) -> Result { /// /// If parsing command line arguments, and no value was specified on the command /// line, the option's `defaultValue` is used. -public func <|(mode: CommandMode, option: Option) -> Result { +public func <|(mode: CommandMode, option: Option) -> Result { switch mode { case let .Arguments(arguments): var stringValue: String? @@ -193,7 +193,7 @@ public func <|(mode: CommandMode, option: Option) -> Result< stringValue = value.unbox case let .Failure(error): - return failure(error) + return failure(error.unbox) } } else { stringValue = arguments.consumePositionalArgument() @@ -220,7 +220,7 @@ public func <|(mode: CommandMode, option: Option) -> Result< /// /// If parsing command line arguments, and no value was specified on the command /// line, the option's `defaultValue` is used. -public func <|(mode: CommandMode, option: Option) -> Result { +public func <|(mode: CommandMode, option: Option) -> Result { precondition(option.key != nil) switch mode { diff --git a/CommandantTests/OptionSpec.swift b/CommandantTests/OptionSpec.swift index b2ac76f..4657a99 100644 --- a/CommandantTests/OptionSpec.swift +++ b/CommandantTests/OptionSpec.swift @@ -15,50 +15,50 @@ import Quick class OptionsTypeSpec: QuickSpec { override func spec() { describe("CommandMode.Arguments") { - func tryArguments(arguments: String...) -> Result { + func tryArguments(arguments: String...) -> Result { return TestOptions.evaluate(.Arguments(ArgumentParser(arguments))) } it("should fail if a required argument is missing") { - expect(tryArguments().isSuccess()).to(beFalsy()) + expect(tryArguments().isSuccess).to(beFalsy()) } it("should fail if an option is missing a value") { - expect(tryArguments("required", "--intValue").isSuccess()).to(beFalsy()) + expect(tryArguments("required", "--intValue").isSuccess).to(beFalsy()) } it("should succeed without optional arguments") { - let value = tryArguments("required").value() + let value = tryArguments("required").value let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalFilename: "filename", requiredName: "required", enabled: false) expect(value).to(equal(expected)) } it("should succeed with some optional arguments") { - let value = tryArguments("required", "--intValue", "3", "fuzzbuzz").value() + let value = tryArguments("required", "--intValue", "3", "fuzzbuzz").value let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: false) 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 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) expect(value).to(equal(expected)) } it("should enable a boolean flag") { - let value = tryArguments("required", "--enabled", "--intValue", "3", "fuzzbuzz").value() + let value = tryArguments("required", "--enabled", "--intValue", "3", "fuzzbuzz").value let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: true) expect(value).to(equal(expected)) } it("should re-disable a boolean flag") { - let value = tryArguments("required", "--enabled", "--no-enabled", "--intValue", "3", "fuzzbuzz").value() + let value = tryArguments("required", "--enabled", "--no-enabled", "--intValue", "3", "fuzzbuzz").value let expected = TestOptions(intValue: 3, stringValue: "foobar", optionalFilename: "fuzzbuzz", requiredName: "required", enabled: false) expect(value).to(equal(expected)) } it("should treat -- as the end of valued options") { - let value = tryArguments("--", "--intValue").value() + let value = tryArguments("--", "--intValue").value let expected = TestOptions(intValue: 42, stringValue: "foobar", optionalFilename: "filename", requiredName: "--intValue", enabled: false) expect(value).to(equal(expected)) } @@ -66,11 +66,11 @@ class OptionsTypeSpec: QuickSpec { describe("CommandMode.Usage") { it("should return an error containing usage information") { - let error = TestOptions.evaluate(.Usage).error()! - expect(error.localizedDescription).to(contain("intValue")) - expect(error.localizedDescription).to(contain("stringValue")) - expect(error.localizedDescription).to(contain("name you're required to")) - expect(error.localizedDescription).to(contain("optionally specify")) + let error = TestOptions.evaluate(.Usage).error + expect(error?.description).to(contain("intValue")) + expect(error?.description).to(contain("stringValue")) + expect(error?.description).to(contain("name you're required to")) + expect(error?.description).to(contain("optionally specify")) } } } @@ -87,7 +87,7 @@ struct TestOptions: OptionsType, Equatable { return self(intValue: a, stringValue: b, optionalFilename: d, requiredName: c, enabled: e) } - static func evaluate(m: CommandMode) -> Result { + static func evaluate(m: CommandMode) -> Result { return create <*> m <| Option(key: "intValue", defaultValue: 42, usage: "Some integer value") <*> m <| Option(key: "stringValue", defaultValue: "foobar", usage: "Some string value")