Class: PaperTrailDiff::AssociationTraversal

Inherits:
Object
  • Object
show all
Defined in:
lib/paper_trail_diff/association_traversal.rb,
sig/generated/paper_trail_diff/association_traversal.rbs

Overview

Resolves and validates ActiveRecord reflections for a bounded association tree.

Instance Method Summary collapse

Constructor Details

#initialize(tree) ⇒ AssociationTraversal

: (AssociationTree) -> void

Parameters:



8
9
10
11
# File 'lib/paper_trail_diff/association_traversal.rb', line 8

def initialize(tree)
  @tree = tree
  @reflection_cache = {}
end

Instance Method Details

#collect_reflections(model_class, tree, path:) ⇒ Array[Array[untyped]]

: (untyped, AssociationTree, path: String) -> Array[Array[untyped]]

Parameters:

Returns:

  • (Array[Array[untyped]])


66
67
68
69
70
71
72
73
74
75
# File 'lib/paper_trail_diff/association_traversal.rb', line 66

def collect_reflections(model_class, tree, path:)
  reflections_for(model_class, tree, path: path).flat_map do |reflection|
    child_path = join_path(path, reflection.name.to_s)
    result = [[child_path, reflection]]
    subtree = tree.child(reflection.name)
    next result unless subtree && !subtree.empty? && !reflection.polymorphic?

    result + collect_reflections(reflection.klass, subtree, path: child_path)
  end
end

#ensure_habtm_history!(model_class, version) ⇒ void

This method returns an undefined value.

: (untyped, untyped) -> void

Parameters:

  • (Object)
  • (Object)


24
25
26
27
28
29
30
31
# File 'lib/paper_trail_diff/association_traversal.rb', line 24

def ensure_habtm_history!(model_class, version)
  paths = habtm_paths(model_class)
  return if paths.empty?
  return if version.respond_to?(:transaction_id) && version.transaction_id

  message = "HABTM history is incomplete at version #{version.id}: #{paths.join(', ')}"
  raise IncompleteAssociationHistoryError, message
end

#habtm_paths(model_class) ⇒ Array[String]

: (untyped) -> Array

Parameters:

  • (Object)

Returns:

  • (Array[String])


34
35
36
37
38
# File 'lib/paper_trail_diff/association_traversal.rb', line 34

def habtm_paths(model_class)
  selected_reflections(model_class).filter_map do |path, reflection|
    path if habtm_reflection?(reflection)
  end.freeze
end

#habtm_reflection?(reflection) ⇒ Boolean

: (untyped) -> bool

Parameters:

  • (Object)

Returns:

  • (Boolean)


119
120
121
122
123
124
# File 'lib/paper_trail_diff/association_traversal.rb', line 119

def habtm_reflection?(reflection)
  return true if reflection.macro == :has_and_belongs_to_many
  return false unless reflection.options[:through]

  habtm_reflection?(reflection.through_reflection)
end

#incoming_relationship_columns(reflection) ⇒ Array[String]

: (untyped) -> Array

Parameters:

  • (Object)

Returns:

  • (Array[String])


47
48
49
50
51
52
53
54
55
56
57
58
# File 'lib/paper_trail_diff/association_traversal.rb', line 47

def incoming_relationship_columns(reflection)
  direct = %i[has_one has_many].include?(reflection.macro) && !reflection.options[:through]
  return [] unless direct

  foreign_keys = Array(reflection.foreign_key)
  # @type var foreign_keys: Array[untyped]
  columns = foreign_keys.map do |column| # rubocop:disable Style/SymbolProc
    column.to_s
  end
  columns << reflection.type.to_s if reflection.options[:as]
  columns
end

#join_path(parent, name) ⇒ String

: (String, String) -> String

Parameters:

  • (String)
  • (String)

Returns:

  • (String)


127
128
129
# File 'lib/paper_trail_diff/association_traversal.rb', line 127

def join_path(parent, name)
  parent.empty? ? name : "#{parent}.#{name}"
end

#paper_trail_versions?(model_class, reflection) ⇒ Boolean

: (untyped, untyped) -> bool

Parameters:

  • (Object)
  • (Object)

Returns:

  • (Boolean)


113
114
115
116
# File 'lib/paper_trail_diff/association_traversal.rb', line 113

def paper_trail_versions?(model_class, reflection)
  model_class.respond_to?(:versions_association_name) &&
    reflection.name.to_s == model_class.versions_association_name.to_s
end

#reflections_for(model_class, tree, path:) ⇒ Array[untyped]

: (untyped, AssociationTree, path: String) -> Array

Parameters:

Returns:

  • (Array[untyped])


41
42
43
44
# File 'lib/paper_trail_diff/association_traversal.rb', line 41

def reflections_for(model_class, tree, path:)
  key = [model_class.name.to_s, path, tree.object_id]
  @reflection_cache[key] ||= requested_reflections(model_class, tree, path)
end

#requested_reflections(model_class, tree, path) ⇒ Array[untyped]

: (untyped, AssociationTree, String) -> Array

Parameters:

Returns:

  • (Array[untyped])


89
90
91
92
93
# File 'lib/paper_trail_diff/association_traversal.rb', line 89

def requested_reflections(model_class, tree, path)
  tree.children.map do |name, _subtree|
    resolve_reflection(model_class, name, join_path(path, name))
  end.freeze
end

#resolve_reflection(model_class, name, full_path) ⇒ Object

: (untyped, String, String) -> untyped

Parameters:

  • (Object)
  • (String)
  • (String)

Returns:

  • (Object)


96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/paper_trail_diff/association_traversal.rb', line 96

def resolve_reflection(model_class, name, full_path)
  reflection = model_class.reflect_on_association(name.to_sym)
  raise UnknownAssociationError, "unknown association: #{full_path}" unless reflection

  if paper_trail_versions?(model_class, reflection)
    message = "unsupported association #{full_path}: PaperTrail versions"
    raise UnsupportedAssociationError, message
  end
  unless SUPPORTED_ASSOCIATION_MACROS.include?(reflection.macro)
    raise UnsupportedAssociationError,
          "unsupported association #{full_path}: #{reflection.macro}"
  end

  reflection
end

#selected_reflections(model_class) ⇒ Array[Array[untyped]]

: (untyped) -> Array[Array[untyped]]

Parameters:

  • (Object)

Returns:

  • (Array[Array[untyped]])


19
20
21
# File 'lib/paper_trail_diff/association_traversal.rb', line 19

def selected_reflections(model_class)
  collect_reflections(model_class, @tree, path: '').freeze
end

#validate!(model_class) ⇒ void

This method returns an undefined value.

: (untyped) -> void

Parameters:

  • (Object)


14
15
16
# File 'lib/paper_trail_diff/association_traversal.rb', line 14

def validate!(model_class)
  validate_tree!(model_class, @tree, path: '')
end

#validate_tree!(model_class, tree, path:) ⇒ void

This method returns an undefined value.

: (untyped, AssociationTree, path: String) -> void

Parameters:



78
79
80
81
82
83
84
85
86
# File 'lib/paper_trail_diff/association_traversal.rb', line 78

def validate_tree!(model_class, tree, path:)
  reflections_for(model_class, tree, path: path).each do |reflection|
    subtree = tree.child(reflection.name)
    next unless subtree && !subtree.empty? && !reflection.polymorphic?

    child_path = join_path(path, reflection.name.to_s)
    validate_tree!(reflection.klass, subtree, path: child_path)
  end
end