Class: PaperTrailDiff::SnapshotNormalizer

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

Overview

Converts an ActiveRecord graph into immutable, persistence-independent snapshots.

Instance Method Summary collapse

Constructor Details

#initialize(tree:, ignore_policy:, traversal:, pool: SnapshotPool.new) ⇒ SnapshotNormalizer

: (tree: AssociationTree, ignore_policy: IgnorePolicy, traversal: AssociationTraversal, ?pool: SnapshotPool) -> void

Parameters:



50
51
52
53
54
55
56
57
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 50

def initialize(tree:, ignore_policy:, traversal:, pool: SnapshotPool.new)
  @tree = tree
  @ignore_policy = ignore_policy
  @traversal = traversal
  @pool = pool
  @structural_columns = {} #: Hash[String, Array[String]]
  @excluded_attributes = {} #: Hash[Array[untyped], Array[String]]
end

Instance Method Details

#association_snapshot(record, reflection, subtree, path, reifier) ⇒ AssociationSnapshot

: (untyped, untyped, AssociationTree, String, untyped) -> AssociationSnapshot

Parameters:

Returns:



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 154

def association_snapshot( # rubocop:disable Metrics/AbcSize
  record,
  reflection,
  subtree,
  path,
  reifier
)
  associated = record.public_send(reflection.name)
  records = if AssociationSnapshot.collection_kind?(reflection.macro)
              associated.to_a
            else
              Array(associated).compact
            end
  child_path = Support.association_path(path, reflection.name.to_s)
  @structural_columns[child_path] ||= @traversal.incoming_relationship_columns(reflection)
  snapshot = AssociationSnapshot.new(
    kind: reflection.macro,
    records: normalize_children(records, subtree, child_path, reifier)
  )
  @pool.association(child_path, snapshot)
end

#attributes_for(record, tree:, path:) ⇒ snapshot_attributes

Normalizes scalar attributes without traversing or loading associations. : (untyped, tree: AssociationTree, path: String) -> snapshot_attributes

Parameters:

Returns:

  • (snapshot_attributes)


78
79
80
81
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 78

def attributes_for(record, tree:, path:)
  reflections = reflections_for(record, tree, path)
  normalized_attributes(record, reflections, path)
end

#build_excluded_attributes(model_class, reflections, path) ⇒ Array[String]

: (untyped, Array, String) -> Array

Parameters:

  • (Object)
  • (Array[untyped])
  • (String)

Returns:

  • (Array[String])


123
124
125
126
127
128
129
130
131
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 123

def build_excluded_attributes(model_class, reflections, path)
  primary_key = model_class.primary_key #: untyped
  primary_keys = primary_key.is_a?(Array) ? primary_key : [primary_key]
  # @type var primary_keys: Array[untyped]
  primary_keys = primary_keys.map { |key| key.to_s } # rubocop:disable Style/SymbolProc
  ignored = @ignore_policy.attributes_for(path)
  structural = @structural_columns.fetch(path, [])
  (primary_keys + ignored + relationship_columns(reflections) + structural).uniq.freeze
end

#call(record, reifier:) ⇒ RecordSnapshot?

: (untyped, reifier: untyped) -> RecordSnapshot?

Parameters:

  • (Object)
  • reifier: (Object)

Returns:



60
61
62
63
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 60

def call(record, reifier:)
  reflections = reflections_for(record, @tree, '')
  normalize_record(record, @tree, '', reifier, reflections)
end

#call_child(record, tree:, path:, incoming:, reifier:) ⇒ RecordSnapshot?

Normalizes one member of an already selected association branch. : (untyped, tree: AssociationTree, path: String, incoming: untyped, reifier: untyped) -> RecordSnapshot?

Parameters:

  • (Object)
  • tree: (AssociationTree)
  • path: (String)
  • incoming: (Object)
  • reifier: (Object)

Returns:



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

def call_child(record, tree:, path:, incoming:, reifier:)
  return unless record

  @structural_columns[path] ||= @traversal.incoming_relationship_columns(incoming)
  reflections = reflections_for(record, tree, path)
  reifier.reify(record, reflections) unless reflections.empty?
  normalize_record(record, tree, path, reifier, reflections)
end

#excluded_attributes(record, reflections, path) ⇒ Array[String]

Every input is fixed for one model class at one selected path, and the path's structural columns are recorded before its records normalize, so this is resolved once instead of per record per boundary. : (untyped, Array, String) -> Array

Parameters:

  • (Object)
  • (Array[untyped])
  • (String)

Returns:

  • (Array[String])


116
117
118
119
120
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 116

def excluded_attributes(record, reflections, path)
  model_class = record.class
  @excluded_attributes[[model_class, path]] ||=
    build_excluded_attributes(model_class, reflections, path)
end

#normalize_associations(record, tree, path, reifier, reflections) ⇒ Hash[String, AssociationSnapshot]

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

Parameters:

Returns:



143
144
145
146
147
148
149
150
151
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 143

def normalize_associations(record, tree, path, reifier, reflections)
  reflections.to_h do |reflection|
    name = reflection.name.to_s
    subtree = tree.child(name)
    raise ConfigurationError, "missing traversal subtree: #{name}" unless subtree

    [name, association_snapshot(record, reflection, subtree, path, reifier)]
  end
end

#normalize_children(records, tree, path, reifier) ⇒ Array[RecordSnapshot]

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

Parameters:

Returns:



177
178
179
180
181
182
183
184
185
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 177

def normalize_children(records, tree, path, reifier)
  reflections_by_class = {} #: Hash[String, Array[untyped]]
  records.filter_map do |child|
    type = child.class.name
    reflections = reflections_by_class[type] ||= reflections_for(child, tree, path)
    reifier.reify(child, reflections) unless reflections.empty?
    normalize_record(child, tree, path, reifier, reflections)
  end
end

#normalize_record(record, tree, path, reifier, reflections) ⇒ RecordSnapshot?

: (untyped, AssociationTree, String, untyped, Array) -> RecordSnapshot?

Parameters:

Returns:



93
94
95
96
97
98
99
100
101
102
103
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 93

def normalize_record(record, tree, path, reifier, reflections)
  return unless record

  snapshot = RecordSnapshot.new(
    type: record.class.name,
    id: record.id,
    attributes: normalized_attributes(record, reflections, path),
    associations: normalize_associations(record, tree, path, reifier, reflections)
  )
  path.empty? ? snapshot : @pool.record(path, snapshot)
end

#normalized_attributes(record, reflections, path) ⇒ snapshot_attributes

: (untyped, Array, String) -> snapshot_attributes

Parameters:

  • (Object)
  • (Array[untyped])
  • (String)

Returns:

  • (snapshot_attributes)


106
107
108
109
110
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 106

def normalized_attributes(record, reflections, path)
  attributes = record.attributes.dup
  excluded_attributes(record, reflections, path).each { |name| attributes.delete(name) }
  attributes
end

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

: (untyped, AssociationTree, String) -> Array

Parameters:

Returns:

  • (Array[untyped])


188
189
190
191
192
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 188

def reflections_for(record, tree, path)
  return [] unless record

  @traversal.reflections_for(record.class, tree, path: path)
end

#relationship_columns(reflections) ⇒ Array[String]

: (Array) -> Array

Parameters:

  • (Array[untyped])

Returns:

  • (Array[String])


134
135
136
137
138
139
140
# File 'lib/paper_trail_diff/snapshot_normalizer.rb', line 134

def relationship_columns(reflections)
  reflections.select { |reflection| reflection.macro == :belongs_to }.flat_map do |reflection|
    columns = [reflection.foreign_key.to_s]
    columns << reflection.foreign_type.to_s if reflection.polymorphic?
    columns
  end
end