Skip to content

Commit 3866cad

Browse files
committed
Merge pull request #23 from Carthage/custom-client-error-type
Parameterize CommandantError by a ClientError type
2 parents b470b6e + be65dcc commit 3866cad

File tree

7 files changed

+93
-54
lines changed

7 files changed

+93
-54
lines changed

Commandant/ArgumentParser.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public final class ArgumentParser {
111111
///
112112
/// If a value is found, the key and the value are both removed from the
113113
/// list of arguments remaining to be parsed.
114-
internal func consumeValueForKey(key: String) -> Result<String?, CommandantError> {
114+
internal func consumeValueForKey(key: String) -> Result<String?, CommandantError<NoError>> {
115115
let oldArguments = rawArguments
116116
rawArguments.removeAll()
117117

Commandant/Command.swift

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import LlamaKit
1111

1212
/// Represents a subcommand that can be executed with its own set of arguments.
1313
public protocol CommandType {
14+
typealias ClientError
15+
1416
/// The action that users should specify to use this subcommand (e.g.,
1517
/// `help`).
1618
var verb: String { get }
@@ -20,7 +22,25 @@ public protocol CommandType {
2022
var function: String { get }
2123

2224
/// Runs this subcommand in the given mode.
23-
func run(mode: CommandMode) -> Result<(), CommandantError>
25+
func run(mode: CommandMode) -> Result<(), CommandantError<ClientError>>
26+
}
27+
28+
/// A type-erased CommandType.
29+
public struct CommandOf<ClientError>: CommandType {
30+
public let verb: String
31+
public let function: String
32+
private let runClosure: CommandMode -> Result<(), CommandantError<ClientError>>
33+
34+
/// Creates a command that wraps another.
35+
public init<C: CommandType where C.ClientError == ClientError>(_ command: C) {
36+
verb = command.verb
37+
function = command.function
38+
runClosure = { mode in command.run(mode) }
39+
}
40+
41+
public func run(mode: CommandMode) -> Result<(), CommandantError<ClientError>> {
42+
return runClosure(mode)
43+
}
2444
}
2545

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

3656
/// Maintains the list of commands available to run.
37-
public final class CommandRegistry {
38-
private var commandsByVerb: [String: CommandType] = [:]
57+
public final class CommandRegistry<ClientError> {
58+
private var commandsByVerb: [String: CommandOf<ClientError>] = [:]
3959

4060
/// All available commands.
41-
public var commands: [CommandType] {
61+
public var commands: [CommandOf<ClientError>] {
4262
return sorted(commandsByVerb.values) { return $0.verb < $1.verb }
4363
}
4464

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

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

6383
/// Returns the command matching the given verb, or nil if no such command
6484
/// is registered.
65-
public subscript(verb: String) -> CommandType? {
85+
public subscript(verb: String) -> CommandOf<ClientError>? {
6686
return commandsByVerb[verb]
6787
}
6888
}
@@ -78,18 +98,18 @@ extension CommandRegistry {
7898
/// If the chosen command fails, the provided error handler will be invoked,
7999
/// then the process will exit with a failure exit code.
80100
///
81-
/// If a matching command could not be found, a helpful error message will
82-
/// be written to `stderr`, then the process will exit with a failure error
83-
/// code.
84-
@noreturn public func main(#defaultCommand: CommandType, errorHandler: CommandantError -> ()) {
101+
/// If a matching command could not be found or a usage error occurred,
102+
/// a helpful error message will be written to `stderr`, then the process
103+
/// will exit with a failure error code.
104+
@noreturn public func main(#defaultVerb: String, errorHandler: ClientError -> ()) {
85105
var arguments = Process.arguments
86106
assert(arguments.count >= 1)
87107

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

92-
let verb = arguments.first ?? defaultCommand.verb
112+
let verb = arguments.first ?? defaultVerb
93113
if arguments.count > 0 {
94114
// Remove the command name.
95115
arguments.removeAtIndex(0)
@@ -100,7 +120,14 @@ extension CommandRegistry {
100120
exit(EXIT_SUCCESS)
101121

102122
case let .Some(.Failure(error)):
103-
errorHandler(error.unbox)
123+
switch error.unbox {
124+
case let .UsageError(description):
125+
fputs(description + "\n", stderr)
126+
127+
case let .CommandError(error):
128+
errorHandler(error.unbox)
129+
}
130+
104131
exit(EXIT_FAILURE)
105132

106133
case .None:

Commandant/Errors.swift

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,42 +7,45 @@
77
//
88

99
import Foundation
10+
import LlamaKit
1011

1112
/// Possible errors that can originate from Commandant.
12-
public enum CommandantError {
13+
///
14+
/// `ClientError` should be the type of error (if any) that can occur when
15+
/// running commands.
16+
public enum CommandantError<ClientError> {
1317
/// An option was used incorrectly.
1418
case UsageError(description: String)
1519

16-
/// Creates an NSError that represents the receiver.
17-
public func toNSError() -> NSError {
18-
let domain = "org.carthage.Commandant"
19-
20-
switch self {
21-
case let .UsageError(description):
22-
return NSError(domain: domain, code: 0, userInfo: [ NSLocalizedDescriptionKey: description ])
23-
}
24-
}
20+
/// An error occurred while running a command.
21+
case CommandError(Box<ClientError>)
2522
}
2623

2724
extension CommandantError: Printable {
2825
public var description: String {
2926
switch self {
3027
case let .UsageError(description):
3128
return description
29+
30+
case let .CommandError(error):
31+
return toString(error)
3232
}
3333
}
3434
}
3535

36+
/// Used to represent that a ClientError will never occur.
37+
internal enum NoError {}
38+
3639
/// Constructs an `InvalidArgument` error that indicates a missing value for
3740
/// the argument by the given name.
38-
internal func missingArgumentError(argumentName: String) -> CommandantError {
41+
internal func missingArgumentError<ClientError>(argumentName: String) -> CommandantError<ClientError> {
3942
let description = "Missing argument for \(argumentName)"
40-
return CommandantError.UsageError(description: description)
43+
return .UsageError(description: description)
4144
}
4245

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

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

5356
/// Constructs an error that describes how to use the option, with the given
5457
/// example of key (and value, if applicable) usage.
55-
internal func informativeUsageError<T>(keyValueExample: String, option: Option<T>) -> CommandantError {
58+
internal func informativeUsageError<T, ClientError>(keyValueExample: String, option: Option<T>) -> CommandantError<ClientError> {
5659
if option.defaultValue != nil {
5760
return informativeUsageError("[\(keyValueExample)]", option.usage)
5861
} else {
@@ -61,7 +64,7 @@ internal func informativeUsageError<T>(keyValueExample: String, option: Option<T
6164
}
6265

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

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

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

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

98101
/// Combines the text of the two errors, if they're both `UsageError`s.
99102
/// Otherwise, uses whichever one is not (biased toward the left).
100-
internal func combineUsageErrors(lhs: CommandantError, rhs: CommandantError) -> CommandantError {
103+
internal func combineUsageErrors<ClientError>(lhs: CommandantError<ClientError>, rhs: CommandantError<ClientError>) -> CommandantError<ClientError> {
101104
switch (lhs, rhs) {
102105
case let (.UsageError(left), .UsageError(right)):
103106
let combinedDescription = "\(left)\n\n\(right)"
104-
return CommandantError.UsageError(description: combinedDescription)
107+
return .UsageError(description: combinedDescription)
105108

106109
case (.UsageError, _):
107110
return rhs

Commandant/HelpCommand.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,23 @@ import LlamaKit
1515
/// If you want to use this command, initialize it with the registry, then add
1616
/// it to that same registry:
1717
///
18-
/// let commands: CommandRegistry = …
18+
/// let commands: CommandRegistry<MyErrorType> = …
1919
/// let helpCommand = HelpCommand(registry: commands)
2020
/// commands.register(helpCommand)
21-
public struct HelpCommand: CommandType {
21+
public struct HelpCommand<ClientError>: CommandType {
2222
public let verb = "help"
2323
public let function = "Display general or command-specific help"
2424

25-
private let registry: CommandRegistry
25+
private let registry: CommandRegistry<ClientError>
2626

2727
/// Initializes the command to provide help from the given registry of
2828
/// commands.
29-
public init(registry: CommandRegistry) {
29+
public init(registry: CommandRegistry<ClientError>) {
3030
self.registry = registry
3131
}
3232

33-
public func run(mode: CommandMode) -> Result<(), CommandantError> {
34-
return HelpOptions.evaluate(mode)
33+
public func run(mode: CommandMode) -> Result<(), CommandantError<ClientError>> {
34+
return HelpOptions<ClientError>.evaluate(mode)
3535
.flatMap { options in
3636
if let verb = options.verb {
3737
if let command = self.registry[verb] {
@@ -61,7 +61,7 @@ public struct HelpCommand: CommandType {
6161
}
6262
}
6363

64-
private struct HelpOptions: OptionsType {
64+
private struct HelpOptions<ClientError>: OptionsType {
6565
let verb: String?
6666

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

75-
static func evaluate(m: CommandMode) -> Result<HelpOptions, CommandantError> {
75+
static func evaluate(m: CommandMode) -> Result<HelpOptions, CommandantError<ClientError>> {
7676
return create
7777
<*> m <| Option(defaultValue: "", usage: "the command to display help for")
7878
}

Commandant/Option.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,12 @@ import LlamaKit
3535
/// }
3636
/// }
3737
public protocol OptionsType {
38+
typealias ClientError
39+
3840
/// Evaluates this set of options in the given mode.
3941
///
40-
/// Returns the parsed options, or an `InvalidArgument` error containing
41-
/// usage information.
42-
static func evaluate(m: CommandMode) -> Result<Self, CommandantError>
42+
/// Returns the parsed options or a `UsageError`.
43+
static func evaluate(m: CommandMode) -> Result<Self, CommandantError<ClientError>>
4344
}
4445

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

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

@@ -155,15 +156,15 @@ infix operator <| {
155156
///
156157
/// In the context of command-line option parsing, this is used to chain
157158
/// together the parsing of multiple arguments. See OptionsType for an example.
158-
public func <*><T, U>(f: T -> U, value: Result<T, CommandantError>) -> Result<U, CommandantError> {
159+
public func <*> <T, U, ClientError>(f: T -> U, value: Result<T, CommandantError<ClientError>>) -> Result<U, CommandantError<ClientError>> {
159160
return value.map(f)
160161
}
161162

162163
/// Applies the function in `f` to the value in the given result.
163164
///
164165
/// In the context of command-line option parsing, this is used to chain
165166
/// together the parsing of multiple arguments. See OptionsType for an example.
166-
public func <*><T, U>(f: Result<(T -> U), CommandantError>, value: Result<T, CommandantError>) -> Result<U, CommandantError> {
167+
public func <*> <T, U, ClientError>(f: Result<(T -> U), CommandantError<ClientError>>, value: Result<T, CommandantError<ClientError>>) -> Result<U, CommandantError<ClientError>> {
167168
switch (f, value) {
168169
case let (.Failure(left), .Failure(right)):
169170
return failure(combineUsageErrors(left.unbox, right.unbox))
@@ -184,7 +185,7 @@ public func <*><T, U>(f: Result<(T -> U), CommandantError>, value: Result<T, Com
184185
///
185186
/// If parsing command line arguments, and no value was specified on the command
186187
/// line, the option's `defaultValue` is used.
187-
public func <|<T: ArgumentType>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError> {
188+
public func <| <T: ArgumentType, ClientError>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError<ClientError>> {
188189
switch mode {
189190
case let .Arguments(arguments):
190191
var stringValue: String?
@@ -194,7 +195,13 @@ public func <|<T: ArgumentType>(mode: CommandMode, option: Option<T>) -> Result<
194195
stringValue = value.unbox
195196

196197
case let .Failure(error):
197-
return failure(error.unbox)
198+
switch error.unbox {
199+
case let .UsageError(description):
200+
return failure(.UsageError(description: description))
201+
202+
case .CommandError:
203+
fatalError("CommandError should be impossible when parameterized over NoError")
204+
}
198205
}
199206
} else {
200207
stringValue = arguments.consumePositionalArgument()
@@ -221,7 +228,7 @@ public func <|<T: ArgumentType>(mode: CommandMode, option: Option<T>) -> Result<
221228
///
222229
/// If parsing command line arguments, and no value was specified on the command
223230
/// line, the option's `defaultValue` is used.
224-
public func <|(mode: CommandMode, option: Option<Bool>) -> Result<Bool, CommandantError> {
231+
public func <| <ClientError>(mode: CommandMode, option: Option<Bool>) -> Result<Bool, CommandantError<ClientError>> {
225232
precondition(option.key != nil)
226233

227234
switch mode {

Commandant/Switch.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ extension Switch: Printable {
4949
///
5050
/// If parsing command line arguments, and no value was specified on the command
5151
/// line, the option's `defaultValue` is used.
52-
public func <|(mode: CommandMode, option: Switch) -> Result<Bool, CommandantError> {
52+
public func <| <ClientError> (mode: CommandMode, option: Switch) -> Result<Bool, CommandantError<ClientError>> {
5353
switch mode {
5454
case let .Arguments(arguments):
5555
var enabled = arguments.consumeKey(option.key)

CommandantTests/OptionSpec.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import LlamaKit
1212
import Nimble
1313
import Quick
1414

15+
enum NoError {}
16+
1517
class OptionsTypeSpec: QuickSpec {
1618
override func spec() {
1719
describe("CommandMode.Arguments") {
18-
func tryArguments(arguments: String...) -> Result<TestOptions, CommandantError> {
20+
func tryArguments(arguments: String...) -> Result<TestOptions, CommandantError<NoError>> {
1921
return TestOptions.evaluate(.Arguments(ArgumentParser(arguments)))
2022
}
2123

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

98-
static func evaluate(m: CommandMode) -> Result<TestOptions, CommandantError> {
100+
static func evaluate(m: CommandMode) -> Result<TestOptions, CommandantError<NoError>> {
99101
return create
100102
<*> m <| Option(key: "intValue", defaultValue: 42, usage: "Some integer value")
101103
<*> m <| Option(key: "stringValue", defaultValue: "foobar", usage: "Some string value")

0 commit comments

Comments
 (0)