Getting a Handle on Swift Errors
And Guard, and (still) Optionals, but the Pun Gets the Title
Because the iTunes search command was done fast, the first attempt wasn't the cleanest code I've ever written. A lot of that is because I'm learning swift, some of it also was because I'm not worried about refactoring a 200 line project so I didn't hold myself up to my usual standards.
This of course means, I now have a playground full of opportunities for improvement. The first case in the project is my search query initializer that creates a search query from the command line input, parses the arguments and prepares to run. The first version had some very ugly parsing, used an optional initializer and just wrote errors to the console inline in the method.
init?(commandArguments: [String]){
guard let cleanResults = ItunesSearchQuery.stripInvocation(commandArguments) else{
print("This script requires arguments to do anything useful")
return nil
}
guard let (type, searchArguments) = ItunesSearchQuery.parseCommand(cleanResults) else{
print("The script requires '-tv' or '-movie' as the first argument")
return nil
}
self.type = type
guard let (search, fileArguments) = ItunesSearchQuery.parseSearch(searchArguments) else{
print("The script requires '-s \"search string\" as the second argument")
return nil
}
self.search = search
guard let file = ItunesSearchQuery.parseFileArgument(fileArguments) else{
print("The script requires '-o \"outfile.jpg\" as the final argument")
return nil
}
self.file = file
}
The run loop of the app then just had to execute:
ItunesSearchQuery(commandArguments: Process.arguments)?.performQuery()
Using guard let
to return nil early avoided the pyramid of doom, and the code is readable, but it's still ugly code that I wouldn't ship for a real project. I decided to make a non-optional initializer that throws an error on bad arguments instead, so the console output is now separated out into the error handling code, but the initializer got better:
init(commandArguments: [String]) throws {
do {
let typeArguments = try ItunesSearchQuery.stripInvocation(commandArguments)
let (type, searchArguments) = try ItunesSearchQuery.parseCommand(typeArguments)
let (search, fileArguments) = try ItunesSearchQuery.parseSearch(searchArguments)
let file = try ItunesSearchQuery.parseFileArgument(fileArguments)
self.type = type
self.search = search
self.file = file
}catch{ throw error }
}
I wasn't happy with the shadowing of variables (bad form and all) and the semi-redundant let statements. I decided to simplify further by using a var
to store the command arguments, which let me make the function look nicer:
init(commandArguments: [String]) throws {
do {
var commandBuffer = try ItunesSearchQuery.stripInvocation(commandArguments)
(type, commandBuffer) = try ItunesSearchQuery.parseCommand(commandBuffer)
(search, commandBuffer) = try ItunesSearchQuery.parseSearch(commandBuffer)
file = try ItunesSearchQuery.parseFileArgument(commandBuffer)
}catch{ throw error }
}
This did displace some complexity into the main class, as the invocation now looks like:
extension ItunesSearchQueryError{
var consoleError: String{
switch (self){
case noArguments: return "arguments to do anything useful"
case badTypeQuery: return "'-tv' or '-movie' as the first argument"
case badSearchTerm: return "'-s \"search string\" as the second argument"
case badFileArgment: return "'-o \"outfile.jpg\" as the final argument"
}
}
}
do {
try ItunesSearchQuery(commandArguments: Process.arguments).performQuery()
}catch{
if let itunesError = error as? ItunesSearchQueryError{
print("This script requires \(itunesError.consoleError())");
}
else{
print("Unhandled other error")
}
}
While it spreads the error handling out, I like that the error messages are all in one place, and that when I add more error states to the search query I'll be able to extend the console error output in one place.
So far, I like errors in Swift substantially more than the conventional error handling pattern in Objective-C. Passing in an empty pointer and then inspecting it to see if anything came out.
For anyone intersted, the full source for the project is (mostly) up to date on Github.