# frozen_string_literal: true require_relative '../value_equality' module RubyTerraform module Models # rubocop:disable Metrics/ClassLength class Path class << self def empty new([]) end end extend Forwardable include Comparable include ValueEquality def_delegators(:@elements, :first, :last, :length, :empty?) attr_reader(:elements) def initialize(elements) @elements = elements.compact end def references_any_lists? elements.any? { |e| e.is_a?(Numeric) } end def references_any_maps? elements.any? { |e| e.is_a?(Symbol) } end def same_parent_collection?(other) return true if self == other left, right = diff(other) left.length == 1 && right.length == 1 end def list_indices elements.each_with_index.inject([]) do |acc, element_index| element, index = element_index element.is_a?(Numeric) ? acc + [[index, element]] : acc end end def to_location(index) return self.class.new([]) if index.negative? self.class.new(elements[0..index]) end def before_location(index) return self.class.new([]) if index.negative? self.class.new(elements[0...index]) end def append(element) self.class.new(elements + [element]) end def drop(count = 1) self.class.new(elements.drop(count)) end def diff(other) left, right = match_lengths(elements, other.elements) pairwise = left.zip(right) difference = pairwise.drop_while { |e| e[0] == e[1] } difference = difference.empty? ? [[], []] : difference.transpose difference.map { |e| self.class.new(e) } end def traverse(initial, &block) initial_context = initial_traversal_context(initial) final_context = elements.inject(initial_context) do |context, element| state = block.call(context[:state], context[:step]) next_traversal_context(state, context[:step], element) end final_context[:state] end def read(object, default: nil) return default if empty? result = object.dig(*elements) result.nil? ? default : result rescue NoMethodError, TypeError default end def <=>(other) return 0 if self == other left, right = diff(other) return -1 if left.empty? return 1 if right.empty? compare_numbers_before_symbols(left.first, right.first) end def state [elements] end private class TraversalStep attr_reader(:seen, :element, :remaining) def initialize(seen, element, remaining) @seen = seen @element = element @remaining = remaining end end def initial_traversal_context(state) { state:, step: TraversalStep.new(self.class.empty, first, drop(1)) } end def next_traversal_context(state, position, step) { state:, step: TraversalStep.new(position.seen.append(step), position.remaining.first, position.remaining.drop(1)) } end def compare_numbers_before_symbols(left, right) return -1 if left.is_a?(Numeric) && right.is_a?(Symbol) return 1 if left.is_a?(Symbol) && right.is_a?(Numeric) left <=> right end def match_lengths(left, right) max_length = [left.count, right.count].max [ pad_to_length(left, max_length), pad_to_length(right, max_length) ] end def pad_to_length(array, target_length) array.clone.fill(nil, array.count, target_length - array.count) end end # rubocop:enable Metrics/ClassLength end end