devedbox/Commander

View on GitHub
Sources/Commander/Commander.swift

Summary

Maintainability
A
0 mins
Test Coverage
//
//  Commander.swift
//  Commander
//
//  Created by devedbox on 2018/7/4.
//
//  Copyright (c) 2018 devedbox
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//

import Foundation
import Utility

/// The logger to log the output message to standard output.
public private(set) var logger: TextOutputStream!

// MARK: - CommanderRepresentable.

public protocol CommanderRepresentable: CommandDescribable, TextOutputStream {
  /// The associated type of `Options`.
  associatedtype Options: OptionsRepresentable = DefaultOptions.None
  /// A closure of `(Error) -> Void` to handle the stderror.
  static var errorHandler: ((Swift.Error) throws -> Swift.Void)? { get }
  /// A closure of `(String) -> Void` to handle the stdout.
  static var outputHandler: ((String) -> Void)? { get }
  /// The registered available commands of the commander.
  static var commands: [CommandDispatchable.Type] { get }
  
  /// Decoding the given command line argumants as the current command's options type and disatch the
  /// command with the decided options.
  func dispatch(with commandLineArgs: [String]) throws
}

// MARK: - Dispatch.

extension CommanderRepresentable {
  /// Returns the options type of the command.
  public static var optionsDescriber: OptionsDescribable.Type { return Options.self }
  /// Returns the children of the insrance of `CommandDescribable`.
  public static var childrenDescribers: [CommandDescribable.Type] { return allCommands }
  /// The command symbol also name of the command.
  public static var symbol: String { return "" }
  /// The command level.
  public static var level: CommandLevel { return .commander }
  /// Appends the given string to the stream.
  public mutating func write(_ string: String) {
    (type(of: self).outputHandler?(string) == nil).true {
      var stdout = FileHandle.standardOutput; print(string, terminator: "", to: &stdout)
    }
  }
  /// Returns all commands of commander with registered commands along with built-in commands.
  internal static var allCommands: [CommandDispatchable.Type] {
    return [BuiltIn.help] + commands
  }
  /// Decoding the current command line arguments of `CommandLine.arguments` as the current command's
  /// options type and dispatch the command with the decoded options.
#if DEBUG
  @discardableResult
  public func dispatch() -> Result {
    do {
      try dispatch(with: Swift.CommandLine.arguments)
    } catch {
      do {
        (try type(of: self).errorHandler?(error) == nil).true { describe(error) }
      } catch InternalError.needsHelp(path: let commandPath) { // Catch InternalError.help() only.
        try? Help.with([commandPath])
      } catch {
        describe(error)
      }
      
      return dispatchFailure()
    }
    return dispatchSuccess()
  }
#else
  public func dispatch() -> Result {
    do {
      try dispatch(with: Swift.CommandLine.arguments)
    } catch {
      do {
        (try type(of: self).errorHandler?(error) == nil).true { describe(error) }
      } catch InternalError.needsHelp(path: let commandPath) { // Catch InternalError.help() only.
        try? Help.with([commandPath])
      } catch {
        describe(error)
      }
      
      return dispatchFailure()
    }
    return dispatchSuccess()
  }
#endif
  /// Decoding the given command line argumants as the current command's options type and disatch the
  /// command with the decided options.
  public func dispatch(with commandLineArgs: [String]) throws {
    defer {
      CommandPath.running.commander = nil // Clear the running commander.
      CommandPath.running.commanderPath = nil // Clear the running path of commander.
      CommandPath.running.sharedOptions = nil // Clear the running global options.
      CommandPath.running.commanderUsage = nil // Clear the runnung commander usage.
      CommandPath.running.commands = [] // Clear the running commands.
      logger = nil // Reset the logger.
      _ArgumentsStorage = [:] // Reset the storage of arguments.
    }
    
    let runningPath = commandLineArgs.first!
    
    logger = self
    CommandPath.running.commander = type(of: self)
    CommandPath.running.commanderPath = runningPath
    CommandPath.running.commanderUsage = type(of: self).usage
    CommandPath.running.commands = type(of: self).allCommands
    
    var commands = commandLineArgs.dropFirst()
    let symbol = commands.popFirst()
    let allCommands = type(of: self).allCommands + BuiltIn.commands
    
    let commandPath = allCommands.first { $0.symbol == symbol }.map {
      CommandPath(
        running: $0,
        at: runningPath.split(separator: "/").last!.string
      )
    }
    
    do {
      if try commandPath?.run(with: Array(commands)) == nil {
        guard let symbol = symbol else {
          throw Error.emptyCommand
        }
        
        if OptionsDecoder.optionsFormat.validate(symbol) {
          try Help.with([symbol], path: nil, commandLineArgs: commandLineArgs)
        } else {
          throw Error.invalidCommand(command: symbol)
        }
      }
    } catch let dispatcher as CommandPath.Dispatcher {
      guard Options.self != DefaultOptions.None.self else {
        try Help.with(dispatcher.options, path: dispatcher.path, commandLineArgs: commandLineArgs)
        return
      }
      
      let unrecognizedOptions = dispatcher.options.filter { Options.OptionKeys(rawValue: $0) == nil }
      guard unrecognizedOptions.isEmpty else {
        throw Error.unrecognizedOptions(
          unrecognizedOptions,
          path: dispatcher.path,
          underlyingError: nil
        )
      }
      
      CommandPath.running.sharedOptions = try Options(from: dispatcher.decoder)
      try dispatcher.path.command.dispatch(with: dispatcher.decoded)
      
    } catch Error.unrecognizedOptions(let options, path: let path, underlyingError: let error) {
      try Set(Options.codingKeys).isSuperset(of: Set(options)).true {
        try error.map { throw $0 }
      }
      
      try Help.with(options, path: path, commandLineArgs: commandLineArgs)
    } catch InternalError.needsHelp(path: let commandPath) {
      try Help.with([commandPath])
    } catch {
      throw error
    }
  }
  
  /// Help with the error describing.
  private func describe(_ error: Swift.Error) {
    var stderr = FileHandle.standardError
    print(String(describing: error), to: &stderr)
  }
}