AtelierClockwork

Trying Out Protocols

August 7, 2015

And Going Generic While Doing It

So last time, we left my initializer code here:

init(commandArguments: [String]) throws {
  var commandBuffer = try ItunesSearchQuery.stripInvocation(commandArguments)
  (type, commandBuffer) = try ItunesSearchQuery.parseCommand(commandBuffer)
  (search, commandBuffer) = try ItunesSearchQuery.parseSearch(commandBuffer)
  file = try ItunesSearchQuery.parseFileArgument(commandBuffer)
}

That’s a big improvement over where I started, but requiring arguments in a strict order and just throwing an error on bad input isn’t an ideal experience. To take this on, I decided to make a protocol and start trying out protocol extensions. After taking on several options, I ended up going with a recursive parser that consumes an input array of commands. The protocol itself is only a pair of typeAliases required to make the extension work, but the extension handles stripping out the first item in the command line arguments (the application itself), then generates an array of command objects.

protocol CommandParsable{
  typealias Command
  typealias Argument
}

extension CommandParsable{
  typealias Parser = ([Argument]) throws -> (Command, [Argument])

  static func parseCommands(input: [Argument], parser: Parser) throws -> [Command] {
      guard let (_, main) = input.decompose else {
        throw CommandParserError<Command, Argument>.noArguments
      }
      return try consumeInput(main, parser:parser)
  }

  private static func consumeInput(input: [Argument], parser: Parser) throws -> [Command] {
      let (cmd, tail) = try parser(input)
      return tail.count > 0 ? try [cmd] + consumeInput(tail, parser:parser) : [cmd]
  }
}

The closure that goes into that initializer is:

enum SearchCommands {
  case type(SearchType), search(String), file(String)
}

static func parseArument(arguments: [Argument]) throws -> (Command, [Argument]) {
    guard let (argument, tail) = arguments.decompose else { throw CommandError.noArguments }
    switch argument.lowercaseString {
    case "-tv", "-t": return (SearchCommands.type(SearchType.tv), tail)
    case "-movie", "-m": return (SearchCommands.type(SearchType.movie), tail)
    case "-search", "-s":
      guard let (search, innerTail) = tail.decompose else { throw CommandError.missingArgument }
      return (SearchCommands.search(search), innerTail)
    case "-file", "-f", "-o", "-outfile":
      guard let (file, innerTail) = tail.decompose else { throw CommandError.missingArgument }
      return (SearchCommands.file(file), innerTail)
    default: throw CommandError.badCommand(argument)
    }
}

It’s longer than the initializer, but probably not the initializer and all of the per-argument error checking code. I also now have the command parseable protocol implementation in the main file of the project rather than embedded directly in the ItunesSearchQuery class.

So far, I’ve been making good progress on separating code by functional unit rather than by class, and in managing to keep values immutable wherever possible. I’m also happier with the expressiveness of the code, and how minor bits of control flow code like guard both express and enforce intent. As always, anyone who wants to take a look at the new project code, it’s on github.