brewster1134/cli_miami

View on GitHub
lib/cli_miami/ask.rb

Summary

Maintainability
D
1 day
Test Coverage
#
# class CliMiami::A
#
class CliMiami::A
  attr_reader :value

  # A.sk API method
  #
  # See documentation for CliMiami::S.ay
  # The same options are accepted, with the addition of
  #   :readline   - uses Readline instead of standard `gets`
  #   :type       - symbol specifying what type of data is requested from the user
  #   :validate   - hash of validation options
  #
  def self.sk question, options = {}, &block
    new question, options, &block
  end

private

  def initialize question, options
    options = CliMiami.get_options options

    # display question
    CliMiami::S.ay question, options.merge(preset: :cli_miami_instruction)

    # display description
    CliMiami::S.ay options[:description], preset: :cli_miami_instruction_sub, indent: 4 if options[:description]

    # request given type to user
    @value = request_type options

    # return response if no block is passed
    # rubocop:disable Style/GuardClause
    if block_given?
      yield @value
    else
      return @value
    end
    # rubocop:enable Style/GuardClause
  end

  # determine the expecting type, and request input form user
  #
  def request_type options
    send("request_#{options[:type]}", options)
  rescue
    CliMiami::S.ay I18n.t('cli_miami.errors.type', type: options[:type]), options.merge(preset: :cli_miami_fail)
  end

  # for most types, a simple validation check is all that is needed
  # if validation fails, we request the user to try again
  #
  # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
  def request_until_valid options, allow_empty_string = false
    response = nil

    while response.nil?
      # get user input based on given file type
      response = if options[:type] == :file
        Readline.readline.chomp '/'
      else
        $stdin.gets.chomp
      end

      # for multiple entry type objects, we allow the user to justs press enter to finish adding entries
      break if allow_empty_string && response == ''

      # otherwise validate the user's input
      validation = CliMiami::Validation.new response, options
      if validation.valid?
        response = validation.value
      else
        response = nil
        CliMiami::S.ay validation.error, :cli_miami_fail
      end
    end

    response
  end
  # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
  alias_method :request_boolean, :request_until_valid
  alias_method :request_file, :request_until_valid
  alias_method :request_float, :request_until_valid
  alias_method :request_fixnum, :request_until_valid
  alias_method :request_string, :request_until_valid
  alias_method :request_symbol, :request_until_valid

  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
  def request_array options
    array = []
    value_options = CliMiami.get_options(options[:value_options] || {})

    # build the array by prompting the user
    # until the array length is an acceptable length, keep prompting user for values
    while array.length < options[:max]
      response = request_until_valid value_options, true

      # user attempting to finish entering items
      if response.empty?
        break if CliMiami::Validation.new(array, options).valid?
        redo

      # remove item if user enters it twice
      elsif array.include? response
        array.delete response

      # add response to the list
      else
        array << response
      end

      # update user
      CliMiami::S.ay array.to_sentence, :cli_miami_update
    end

    array
  end
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength

  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  def request_multiple_choice options
    selected_choices = nil
    selected_choice_indexes = []

    while selected_choice_indexes.length < options[:max]
      # quit if all the options have been selected
      break if selected_choice_indexes.length == options[:choices].length

      # display options
      CliMiami::S.ay
      CliMiami::S.ay I18n.t('cli_miami.core.multiple_choice.prompt'), :cli_miami_instruction
      options[:choices].to_a.each_with_index do |li, i|
        li = li[1] if li.is_a? Array

        # show already selected choices with a different color
        choice_text = "#{(i + 1).to_s.rjust(3)}: #{li}"
        choice_text_color = selected_choice_indexes.include?(i) ? :black : :cyan
        CliMiami::S.ay choice_text, style: :bright, color: choice_text_color
      end

      # request a response from the user (return fixnum)
      response = request_until_valid({
        type: :fixnum,
        min: 1,
        max: options[:choices].length
      }, true)

      if response == ''
        break if CliMiami::Validation.new(selected_choice_indexes, options).valid?
        redo
      else
        # convert human readable response to array index
        response_index = response - 1

        # add choice to array
        if selected_choice_indexes.include? response_index
          selected_choice_indexes.delete response_index
        else
          selected_choice_indexes << response_index
        end
      end

      # get selected choice values
      selected_choices = selected_choice_indexes.map do |i|
        options[:choices].to_a[i]
      end

      # convert associate array back to hash
      if options[:choices].is_a? Hash
        selected_choices = Hash[*selected_choices.flatten]
      end

      # show user their selected choices
      # update user
      CliMiami::S.ay
      CliMiami::S.ay I18n.t('cli_miami.core.multiple_choice.selected_choices'), :cli_miami_instruction_sub
      selected_choices.each do |sc|
        sc = sc[1] if sc.is_a? Array
        CliMiami::S.ay "• #{sc}", preset: :cli_miami_update, indent: 3
      end
    end

    selected_choices
  end
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

  # rubocop:disable Metrics/AbcSize, Metrics/BlockNesting, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  def request_hash options
    hash = {}
    options[:keys] ||= []
    value_options = CliMiami.get_options(options[:value_options] || {})
    required_keys_set = false

    # build the hash by prompting the user
    # until the hash length of keys is an acceptable length, keep prompting user for values
    while hash.keys.length < options[:max]
      # if keys options is set, prompt for those values first
      if required_keys_set == false
        options[:keys].each do |key|
          hash[key.to_sym] = request_until_valid value_options

          # update user
          CliMiami::S.ay hash.to_cli_miami_string, :cli_miami_update
        end

        # set boolean so we know all required keys are set
        required_keys_set = true

        # end this loop to re-check the while condition to make sure the max wasn't reached from required keys
        #   e.g. setting { max: 2, keys: [:foo, :bar] }
        #   this prevents users from entering user-defined keys since the max will already be met
        next

      # then start prompting for keys and values
      else
        # request key
        user_key = request_until_valid value_options.merge(type: :symbol), true

        if user_key.empty?
          break if CliMiami::Validation.new(hash, options).valid?
          redo
        else
          CliMiami::S.ay I18n.t('cli_miami.core.enter_value_for', key: user_key), :cli_miami_instruction

          # request value
          user_value = request_until_valid value_options, true

          if user_value.empty?
            if options[:keys].include? user_key
              # prevent deleting required keys
              CliMiami::S.ay I18n.t('cli_miami.errors.multiple_choice.delete_required_key', key: user_key), :cli_miami_fail
              redo
            else
              # delete user-specified key
              hash.delete user_key
            end
          else
            # set user-defined key
            hash[user_key] = user_value
          end
        end
      end

      CliMiami::S.ay hash.to_cli_miami_string, :cli_miami_update
    end

    hash
  end
  # rubocop:enable Metrics/AbcSize, Metrics/BlockNesting, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
  def request_range options
    start_value = nil
    end_value = nil
    range_value_options = CliMiami.get_options type: :float

    # get start value
    until (Float(start_value) rescue nil)
      CliMiami::S.ay I18n.t('cli_miami.core.enter_start_value'), preset: :cli_miami_instruction, newline: false
      start_value = request_until_valid range_value_options
    end

    # get end value
    until (Float(end_value) rescue nil)
      CliMiami::S.ay I18n.t('cli_miami.core.enter_end_value'), preset: :cli_miami_instruction, newline: false
      end_value = request_until_valid range_value_options
    end

    # swap values if entered in reverse
    start_value, end_value = end_value, start_value if start_value > end_value

    # build range object
    range = Range.new start_value, end_value

    # if range is invalid, start over and request it again
    if CliMiami::Validation.new(range, options).valid?
      range
    else
      request_range options
    end
  end
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
end