devedbox/Commander

View on GitHub
Sources/Commander/Commands/CommandPath.swift

Summary

Maintainability
A
0 mins
Test Coverage
//
//  CommandPath.swift
//  Commander
//
//  Created by devedbox on 2018/10/11.
//
//  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.
//

// MARK: - CommandPath.

/// A type represents the running paths of the specific command of `AnyCommandRepresetnable.Type`.
public struct CommandPath {
  /// The error info of the command path to redispatch with the.
  internal struct Dispatcher: Swift.Error {
    /// Running command path.
    internal let path: CommandPath
    /// The unrecognized options keys.
    internal let options: [String]
    /// The decoded options.
    internal let decoded: Decodable
    /// The decoder to decode the options.
    internal let decoder: Decoder
  }
  
  /// The running context type of the command path.
  internal struct RunningContext {
    /// The running commander.
    internal var commander: CommandDescribable.Type!
    /// The running command path.
    internal var commandPath: CommandPath!
    /// The command path with exceptions.
    internal var exceptionCommandPath: CommandPath!
    /// The running commander path of the commander.
    internal var commanderPath: String!
    /// The running commander's usage.
    internal var commanderUsage: String!
    /// The running global options of the commander.
    internal var sharedOptions: OptionsDescribable?
    /// The running commander's available commands.
    internal var commands: [CommandDispatchable.Type] = []
  }
  
  /// The running context of the command path.
  internal static var running: RunningContext = RunningContext()
  
  /// The running paths of the ass
  public private(set) var paths: [String]
  /// The exact running command of the command path.
  public private(set) var command: CommandDispatchable.Type
  /// Creates an instance of `CommandPath` with the given command and running path.
  ///
  /// - Parameter command: The command to run at the path.
  /// - Parameter path: The path of the command to run at.
  public init(
    running command: CommandDispatchable.Type,
    at path: String)
  {
    self.init(running: command, at: path.split(separator: " ").map { String($0) })
  }
  /// Creates an instance of `CommandPath` with the given command and running paths.
  ///
  /// - Parameter command: The command to run at the path.
  /// - Parameter paths: The separated paths of the command to run at.
  public init(
    running command: CommandDispatchable.Type,
    at paths: [String])
  {
    self.paths = paths
    self.command = command
  }
  /// Returns the all matches available command paths  and unrecognized symbols of the given command symbols.
  ///
  /// - Parameter symbols: The command symbols to be evaluated.
  /// - Returns: The tuple of command paths and unrecognized symbols.
  public static func maxMatches(_ symbols: [String]) throws -> [CommandPath] {
    var index = symbols.startIndex
    var unrecognizedIndices: [Array<String>.Index] = []
    var commandGroups: [[CommandDescribable.Type]] = []
    
    /// Matches the commands in the given symbols as long as possiable.
    func match(
      from index: inout Array<String>.Index,
      in command: CommandDescribable.Type? = nil) -> [CommandDescribable.Type]
    {
      guard index < symbols.endIndex else { return [] }
      
      var commands: [CommandDescribable.Type] = []
      let symbol = symbols[index]
      let matchingCommands = command?.childrenDescribers ?? self.running.commands
      
      if let matchingCommand = matchingCommands.first(where: { $0.symbol == symbol }) {
        symbols.formIndex(after: &index)
        commands += [matchingCommand]
        commands += match(from: &index, in: matchingCommand)
      }
      
      return commands
    }
    
    while index < symbols.endIndex {
      switch match(from: &index) {
      case let group where group.isEmpty:
        unrecognizedIndices.append(index)
        symbols.formIndex(after: &index)
      case let group where group.isEmpty == false:
        commandGroups.append(group)
      default: break
      }
    }
    
    switch unrecognizedIndices {
    case let t where t.isEmpty == false:
      throw Error.unrecognizedCommands(commands: unrecognizedIndices.map { symbols[$0] })
    default:
      return commandGroups.compactMap {
        CommandPath(
          running: $0.last! as! CommandDispatchable.Type,
          at: [self.running.commanderPath.split(delimiter: "/").last!] + $0.dropLast().map { $0.symbol }
        )
      }
    }
  }
  /// Returns the first mached command path for the given command symbol.
  ///
  /// - Parameter symbol: The command symbols to be evaluated.
  /// - Returns: The matched command path of the symbol.
  public static func of(_ symbol: String) throws -> CommandPath {
    func recursiveBuildPaths(
      _ input: inout [String],
      in commands: [CommandDispatchable.Type]) -> CommandDispatchable.Type?
    {
      guard commands.isEmpty == false else { return nil }
      if let command = commands.first(where: { $0.symbol == symbol }) { return command }
      
      var iterator = commands.makeIterator()
      while let command = iterator.next() {
        input.append(command.symbol)
        if let target = recursiveBuildPaths(&input, in: command.children) { return target }
        input.removeLast()
      }
      
      return nil
    }
    
    var paths: [String] = [self.running.commanderPath.split(delimiter: "/").last!]
    if let command = recursiveBuildPaths(&paths, in: self.running.commands) {
      return CommandPath(running: command, at: paths.joined(separator: " "))
    }
    
    throw Error.unrecognizedCommands(commands: [symbol])
  }
  
  /// Run the command with specific command line arguments and returns the exact running command path
  /// of `CommandPath`.
  ///
  /// The command path of `CommandPath` will run the command recursively if the given command line
  /// arguments matchs the paths of subcommands of the command.
  ///
  /// - Parameter commandLineArgs: The command line arguments.
  /// - Parameter ignoresExecution: Should ignore the execution of the command path.
  /// - Returns: Returns the exact running path.
  @discardableResult
  internal func run(with commandLineArgs: [String], skipping: Bool = false) throws -> CommandPath {
    if
      let first = commandLineArgs.first,
      OptionsDecoder.optionsFormat.index(of: first) == nil,
      let subcommand = command.children.filter({ $0.symbol == first }).first
    { // Consider a subcommand.
      return try CommandPath(
        running: subcommand,
        at: "\(paths.joined(separator: " ")) \(command.symbol)"
      ).run(
        with: Array(commandLineArgs.dropFirst()),
        skipping: skipping
      )
    }
    
    guard skipping == false else {
      return self
    }
    
    // Set the running command path before run the command path.
    // Defer setting nil of the running command path.
    type(of: self).running.commandPath = self; defer { type(of: self).running.commandPath = nil }
    type(of: self).running.exceptionCommandPath = self
    
    do {
      try command.dispatch(with: commandLineArgs)
    } catch OptionsDecoder.Error.unrecognizedOptions(let options, decoded: nil, decoder: _, decodeError: let error) {
      throw Error.unrecognizedOptions(options, path: self, underlyingError: error)
    } catch OptionsDecoder.Error.unrecognizedOptions(let options, decoded: let decoded?, decoder: let decoder?, decodeError: _) {
      throw Dispatcher(path: self, options: options, decoded: decoded, decoder: decoder)
    } catch Signal.return {
      return self
    } catch {
      throw error
    }
    
    return self
  }
}