Skip to content

Commit

Permalink
Merge pull request #23 from Carthage/custom-client-error-type
Browse files Browse the repository at this point in the history
Parameterize CommandantError by a ClientError type
  • Loading branch information
jspahrsummers committed Apr 14, 2015
2 parents b470b6e + be65dcc commit 3866cad
Show file tree
Hide file tree
Showing 7 changed files with 93 additions and 54 deletions.
2 changes: 1 addition & 1 deletion Commandant/ArgumentParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,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<String?, CommandantError> {
internal func consumeValueForKey(key: String) -> Result<String?, CommandantError<NoError>> {
let oldArguments = rawArguments
rawArguments.removeAll()

Expand Down
55 changes: 41 additions & 14 deletions Commandant/Command.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import LlamaKit

/// Represents a subcommand that can be executed with its own set of arguments.
public protocol CommandType {
typealias ClientError

/// The action that users should specify to use this subcommand (e.g.,
/// `help`).
var verb: String { get }
Expand All @@ -20,7 +22,25 @@ public protocol CommandType {
var function: String { get }

/// Runs this subcommand in the given mode.
func run(mode: CommandMode) -> Result<(), CommandantError>
func run(mode: CommandMode) -> Result<(), CommandantError<ClientError>>
}

/// A type-erased CommandType.
public struct CommandOf<ClientError>: CommandType {
public let verb: String
public let function: String
private let runClosure: CommandMode -> Result<(), CommandantError<ClientError>>

/// Creates a command that wraps another.
public init<C: CommandType where C.ClientError == ClientError>(_ command: C) {
verb = command.verb
function = command.function
runClosure = { mode in command.run(mode) }
}

public func run(mode: CommandMode) -> Result<(), CommandantError<ClientError>> {
return runClosure(mode)
}
}

/// Describes the "mode" in which a command should run.
Expand All @@ -34,11 +54,11 @@ public enum CommandMode {
}

/// Maintains the list of commands available to run.
public final class CommandRegistry {
private var commandsByVerb: [String: CommandType] = [:]
public final class CommandRegistry<ClientError> {
private var commandsByVerb: [String: CommandOf<ClientError>] = [:]

/// All available commands.
public var commands: [CommandType] {
public var commands: [CommandOf<ClientError>] {
return sorted(commandsByVerb.values) { return $0.verb < $1.verb }
}

Expand All @@ -48,21 +68,21 @@ public final class CommandRegistry {
///
/// If another command was already registered with the same `verb`, it will
/// be overwritten.
public func register(command: CommandType) {
commandsByVerb[command.verb] = command
public func register<C: CommandType where C.ClientError == ClientError>(command: C) {
commandsByVerb[command.verb] = CommandOf(command)
}

/// Runs the command corresponding to the given verb, passing it the given
/// arguments.
///
/// Returns the results of the execution, or nil if no such command exists.
public func runCommand(verb: String, arguments: [String]) -> Result<(), CommandantError>? {
public func runCommand(verb: String, arguments: [String]) -> Result<(), CommandantError<ClientError>>? {
return self[verb]?.run(.Arguments(ArgumentParser(arguments)))
}

/// Returns the command matching the given verb, or nil if no such command
/// is registered.
public subscript(verb: String) -> CommandType? {
public subscript(verb: String) -> CommandOf<ClientError>? {
return commandsByVerb[verb]
}
}
Expand All @@ -78,18 +98,18 @@ extension CommandRegistry {
/// If the chosen command fails, the provided error handler will be invoked,
/// then the process will exit with a failure exit code.
///
/// 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: CommandantError -> ()) {
/// If a matching command could not be found or a usage error occurred,
/// a helpful error message will be written to `stderr`, then the process
/// will exit with a failure error code.
@noreturn public func main(#defaultVerb: String, errorHandler: ClientError -> ()) {
var arguments = Process.arguments
assert(arguments.count >= 1)

// Extract the executable name.
let executableName = arguments.first!
arguments.removeAtIndex(0)

let verb = arguments.first ?? defaultCommand.verb
let verb = arguments.first ?? defaultVerb
if arguments.count > 0 {
// Remove the command name.
arguments.removeAtIndex(0)
Expand All @@ -100,7 +120,14 @@ extension CommandRegistry {
exit(EXIT_SUCCESS)

case let .Some(.Failure(error)):
errorHandler(error.unbox)
switch error.unbox {
case let .UsageError(description):
fputs(description + "\n", stderr)

case let .CommandError(error):
errorHandler(error.unbox)
}

exit(EXIT_FAILURE)

case .None:
Expand Down
39 changes: 21 additions & 18 deletions Commandant/Errors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,45 @@
//

import Foundation
import LlamaKit

/// Possible errors that can originate from Commandant.
public enum CommandantError {
///
/// `ClientError` should be the type of error (if any) that can occur when
/// running commands.
public enum CommandantError<ClientError> {
/// 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 ])
}
}
/// An error occurred while running a command.
case CommandError(Box<ClientError>)
}

extension CommandantError: Printable {
public var description: String {
switch self {
case let .UsageError(description):
return description

case let .CommandError(error):
return toString(error)
}
}
}

/// Used to represent that a ClientError will never occur.
internal enum NoError {}

/// Constructs an `InvalidArgument` error that indicates a missing value for
/// the argument by the given name.
internal func missingArgumentError(argumentName: String) -> CommandantError {
internal func missingArgumentError<ClientError>(argumentName: String) -> CommandantError<ClientError> {
let description = "Missing argument for \(argumentName)"
return CommandantError.UsageError(description: description)
return .UsageError(description: description)
}

/// Constructs an error by combining the example of key (and value, if applicable)
/// with the usage description.
internal func informativeUsageError(keyValueExample: String, usage: String) -> CommandantError {
internal func informativeUsageError<ClientError>(keyValueExample: String, usage: String) -> CommandantError<ClientError> {
let lines = usage.componentsSeparatedByCharactersInSet(NSCharacterSet.newlineCharacterSet())

return .UsageError(description: reduce(lines, keyValueExample) { previous, value in
Expand All @@ -52,7 +55,7 @@ internal func informativeUsageError(keyValueExample: String, usage: String) -> C

/// 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>(keyValueExample: String, option: Option<T>) -> CommandantError {
internal func informativeUsageError<T, ClientError>(keyValueExample: String, option: Option<T>) -> CommandantError<ClientError> {
if option.defaultValue != nil {
return informativeUsageError("[\(keyValueExample)]", option.usage)
} else {
Expand All @@ -61,7 +64,7 @@ internal func informativeUsageError<T>(keyValueExample: String, option: Option<T
}

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

if let key = option.key {
Expand All @@ -83,7 +86,7 @@ internal func informativeUsageError<T: ArgumentType>(option: Option<T>) -> Comma
}

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

let key = option.key!
Expand All @@ -97,11 +100,11 @@ internal func informativeUsageError(option: Option<Bool>) -> CommandantError {

/// 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 {
internal func combineUsageErrors<ClientError>(lhs: CommandantError<ClientError>, rhs: CommandantError<ClientError>) -> CommandantError<ClientError> {
switch (lhs, rhs) {
case let (.UsageError(left), .UsageError(right)):
let combinedDescription = "\(left)\n\n\(right)"
return CommandantError.UsageError(description: combinedDescription)
return .UsageError(description: combinedDescription)

case (.UsageError, _):
return rhs
Expand Down
16 changes: 8 additions & 8 deletions Commandant/HelpCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,23 @@ import LlamaKit
/// If you want to use this command, initialize it with the registry, then add
/// it to that same registry:
///
/// let commands: CommandRegistry = …
/// let commands: CommandRegistry<MyErrorType> = …
/// let helpCommand = HelpCommand(registry: commands)
/// commands.register(helpCommand)
public struct HelpCommand: CommandType {
public struct HelpCommand<ClientError>: CommandType {
public let verb = "help"
public let function = "Display general or command-specific help"

private let registry: CommandRegistry
private let registry: CommandRegistry<ClientError>

/// Initializes the command to provide help from the given registry of
/// commands.
public init(registry: CommandRegistry) {
public init(registry: CommandRegistry<ClientError>) {
self.registry = registry
}

public func run(mode: CommandMode) -> Result<(), CommandantError> {
return HelpOptions.evaluate(mode)
public func run(mode: CommandMode) -> Result<(), CommandantError<ClientError>> {
return HelpOptions<ClientError>.evaluate(mode)
.flatMap { options in
if let verb = options.verb {
if let command = self.registry[verb] {
Expand Down Expand Up @@ -61,7 +61,7 @@ public struct HelpCommand: CommandType {
}
}

private struct HelpOptions: OptionsType {
private struct HelpOptions<ClientError>: OptionsType {
let verb: String?

init(verb: String?) {
Expand All @@ -72,7 +72,7 @@ private struct HelpOptions: OptionsType {
return self(verb: (verb == "" ? nil : verb))
}

static func evaluate(m: CommandMode) -> Result<HelpOptions, CommandantError> {
static func evaluate(m: CommandMode) -> Result<HelpOptions, CommandantError<ClientError>> {
return create
<*> m <| Option(defaultValue: "", usage: "the command to display help for")
}
Expand Down
27 changes: 17 additions & 10 deletions Commandant/Option.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ import LlamaKit
/// }
/// }
public protocol OptionsType {
typealias ClientError

/// Evaluates this set of options in the given mode.
///
/// Returns the parsed options, or an `InvalidArgument` error containing
/// usage information.
static func evaluate(m: CommandMode) -> Result<Self, CommandantError>
/// Returns the parsed options or a `UsageError`.
static func evaluate(m: CommandMode) -> Result<Self, CommandantError<ClientError>>
}

/// Describes an option that can be provided on the command line.
Expand Down Expand Up @@ -75,9 +76,9 @@ public struct Option<T> {

/// 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) -> CommandantError {
private func invalidUsageError<ClientError>(value: String) -> CommandantError<ClientError> {
let description = "Invalid value for '\(self)': \(value)"
return CommandantError.UsageError(description: description)
return .UsageError(description: description)
}
}

Expand Down Expand Up @@ -155,15 +156,15 @@ 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 <*><T, U>(f: T -> U, value: Result<T, CommandantError>) -> Result<U, CommandantError> {
public func <*> <T, U, ClientError>(f: T -> U, value: Result<T, CommandantError<ClientError>>) -> Result<U, CommandantError<ClientError>> {
return value.map(f)
}

/// Applies the function in `f` to the value in the given 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 <*><T, U>(f: Result<(T -> U), CommandantError>, value: Result<T, CommandantError>) -> Result<U, CommandantError> {
public func <*> <T, U, ClientError>(f: Result<(T -> U), CommandantError<ClientError>>, value: Result<T, CommandantError<ClientError>>) -> Result<U, CommandantError<ClientError>> {
switch (f, value) {
case let (.Failure(left), .Failure(right)):
return failure(combineUsageErrors(left.unbox, right.unbox))
Expand All @@ -184,7 +185,7 @@ public func <*><T, U>(f: Result<(T -> U), CommandantError>, value: Result<T, Com
///
/// If parsing command line arguments, and no value was specified on the command
/// line, the option's `defaultValue` is used.
public func <|<T: ArgumentType>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError> {
public func <| <T: ArgumentType, ClientError>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError<ClientError>> {
switch mode {
case let .Arguments(arguments):
var stringValue: String?
Expand All @@ -194,7 +195,13 @@ public func <|<T: ArgumentType>(mode: CommandMode, option: Option<T>) -> Result<
stringValue = value.unbox

case let .Failure(error):
return failure(error.unbox)
switch error.unbox {
case let .UsageError(description):
return failure(.UsageError(description: description))

case .CommandError:
fatalError("CommandError should be impossible when parameterized over NoError")
}
}
} else {
stringValue = arguments.consumePositionalArgument()
Expand All @@ -221,7 +228,7 @@ public func <|<T: ArgumentType>(mode: CommandMode, option: Option<T>) -> 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<Bool>) -> Result<Bool, CommandantError> {
public func <| <ClientError>(mode: CommandMode, option: Option<Bool>) -> Result<Bool, CommandantError<ClientError>> {
precondition(option.key != nil)

switch mode {
Expand Down
2 changes: 1 addition & 1 deletion Commandant/Switch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ extension Switch: Printable {
///
/// 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: Switch) -> Result<Bool, CommandantError> {
public func <| <ClientError> (mode: CommandMode, option: Switch) -> Result<Bool, CommandantError<ClientError>> {
switch mode {
case let .Arguments(arguments):
var enabled = arguments.consumeKey(option.key)
Expand Down
6 changes: 4 additions & 2 deletions CommandantTests/OptionSpec.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ import LlamaKit
import Nimble
import Quick

enum NoError {}

class OptionsTypeSpec: QuickSpec {
override func spec() {
describe("CommandMode.Arguments") {
func tryArguments(arguments: String...) -> Result<TestOptions, CommandantError> {
func tryArguments(arguments: String...) -> Result<TestOptions, CommandantError<NoError>> {
return TestOptions.evaluate(.Arguments(ArgumentParser(arguments)))
}

Expand Down Expand Up @@ -95,7 +97,7 @@ struct TestOptions: OptionsType, Equatable {
return self(intValue: a, stringValue: b, optionalFilename: d, requiredName: c, enabled: e, force: f, glob: g)
}

static func evaluate(m: CommandMode) -> Result<TestOptions, CommandantError> {
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")
Expand Down

0 comments on commit 3866cad

Please sign in to comment.