class RSpec::Support::ObjectFormatter

rubocop:disable Metrics/ClassLength
@api private
printing Time, DateTime, or BigDecimal
Provide additional output details beyond what ‘inspect` provides when

def self.default_instance

For example, calling ObjectFormatter.format is still possible
Methods are deferred to a default instance of the class to maintain the interface
def self.default_instance
  @default_instance ||= new
end

def self.format(object)

def self.format(object)
  default_instance.format(object)
end

def self.prepare_for_inspection(object)

def self.prepare_for_inspection(object)
  default_instance.prepare_for_inspection(object)
end

def format(object)

def format(object)
  if max_formatted_output_length.nil?
    prepare_for_inspection(object).inspect
  else
    formatted_object = prepare_for_inspection(object).inspect
    if formatted_object.length < max_formatted_output_length
      formatted_object
    else
      beginning = truncate_string formatted_object, 0, max_formatted_output_length / 2
      ending = truncate_string formatted_object, -max_formatted_output_length / 2, -1
      beginning + ELLIPSIS + ending
    end
  end
end

def initialize(max_formatted_output_length=200)

def initialize(max_formatted_output_length=200)
  @max_formatted_output_length = max_formatted_output_length
  @current_structure_stack = []
end

def prepare_array(array)

def prepare_array(array)
  with_entering_structure(array) do
    array.map { |element| prepare_element(element) }
  end
end

def prepare_element(element)

def prepare_element(element)
  if recursive_structure?(element)
    case element
    when Array then InspectableItem.new('[...]')
    when Hash then InspectableItem.new('{...}')
    else raise # This won't happen
    end
  else
    prepare_for_inspection(element)
  end
end

def prepare_for_inspection(object)

format the entire thing.
for that item. Then we can just use `Array#inspect` or `Hash#inspect` to
with custom items that have `inspect` defined to return the desired output
at any level of nesting, simply by walking that structure and replacing items
This allows us to apply the desired formatting to hash/array data structures

desired output.
in something that, when `inspect` is called on it, will produce the
Prepares the provided object to be formatted by wrapping it as needed
def prepare_for_inspection(object)
  case object
  when Array
    prepare_array(object)
  when Hash
    prepare_hash(object)
  else
    inspector_class = INSPECTOR_CLASSES.find { |inspector| inspector.can_inspect?(object) }
    inspector_class.new(object, self)
  end
end

def prepare_hash(input_hash)

def prepare_hash(input_hash)
  with_entering_structure(input_hash) do
    sort_hash_keys(input_hash).inject({}) do |output_hash, key_and_value|
      key, value = key_and_value.map { |element| prepare_element(element) }
      output_hash[key] = value
      output_hash
    end
  end
end

def recursive_structure?(object)

def recursive_structure?(object)
  @current_structure_stack.any? { |seen_structure| seen_structure.equal?(object) }
end

def sort_hash_keys(input_hash)

def sort_hash_keys(input_hash)
  if input_hash.keys.all? { |k| k.is_a?(String) || k.is_a?(Symbol) }
    Hash[input_hash.sort_by { |k, _v| k.to_s }]
  else
    input_hash
  end
end

def truncate_string(str, start_index, end_index)

codes to the terminal can lead to corruption
will be removed as printing partial ANSI
If the string ends with a partial ANSI code code then that
Returns the substring defined by the start_index and end_index
def truncate_string(str, start_index, end_index)
  cut_str = str[start_index..end_index]
  # ANSI color codes are like: \e[33m so anything with \e[ and a
  # number without a 'm' is an incomplete color code
  cut_str.sub(/\e\[\d+$/, '')
end

def with_entering_structure(structure)

def with_entering_structure(structure)
  @current_structure_stack.push(structure)
  return_value = yield
  @current_structure_stack.pop
  return_value
end