Class: PaperTrailDiff::CollectionComparator

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

Overview

Compares collection snapshots with a low-allocation path for stable membership.

Instance Method Summary collapse

Constructor Details

#initialize(from_association, to_association, record_comparer:) ⇒ CollectionComparator

: (AssociationSnapshot, AssociationSnapshot, record_comparer: untyped) -> void

Parameters:



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

def initialize(from_association, to_association, record_comparer:)
  @from = from_association
  @to = to_association
  @record_comparer = record_comparer
end

Instance Method Details

#aligned_changed_pairsArray[[ RecordSnapshot, RecordSnapshot ]]?

: () -> Array[[RecordSnapshot, RecordSnapshot]]?

Returns:



63
64
65
66
67
68
69
# File 'lib/paper_trail_diff/collection_comparator.rb', line 63

def aligned_changed_pairs
  from_records = @from.records
  to_records = @to.records
  return unless from_records.length == to_records.length

  collect_aligned_pairs(from_records, to_records)
end

#aligned_changesArray[RecordChange]?

: () -> Array?

Returns:



53
54
55
56
57
58
59
60
# File 'lib/paper_trail_diff/collection_comparator.rb', line 53

def aligned_changes
  pairs = aligned_changed_pairs
  return unless pairs

  pairs.sort_by { |_from, to| sortable_identity(to.identity) }.filter_map do |pair|
    @record_comparer.call(pair.fetch(0), pair.fetch(1))
  end
end

#callCollectionAssociationDiff

: () -> CollectionAssociationDiff



15
16
17
18
19
20
21
22
23
# File 'lib/paper_trail_diff/collection_comparator.rb', line 15

def call
  transition = @to.transition_from(@from)
  return transition_difference(transition) if transition

  aligned = aligned_changes
  return difference(added: [], removed: [], changed: aligned) if aligned

  indexed_difference
end

#changed_records(from_records, to_records) ⇒ Array[RecordChange]

: (Hash[identity, RecordSnapshot], Hash[identity, RecordSnapshot]) -> Array

Parameters:

Returns:



118
119
120
121
122
123
124
125
126
# File 'lib/paper_trail_diff/collection_comparator.rb', line 118

def changed_records(from_records, to_records)
  sorted_identities(from_records.keys & to_records.keys).filter_map do |identity|
    from_record = from_records.fetch(identity)
    to_record = to_records.fetch(identity)
    next if from_record.equal?(to_record)

    @record_comparer.call(from_record, to_record)
  end
end

#collect_aligned_pairs(from_records, to_records) ⇒ Array[[ RecordSnapshot, RecordSnapshot ]]?

: (Array, Array) -> Array[[RecordSnapshot, RecordSnapshot]]?

Parameters:

Returns:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/paper_trail_diff/collection_comparator.rb', line 72

def collect_aligned_pairs(from_records, to_records)
  seen = {} #: Hash[identity, bool]
  changed = [] #: Array[[RecordSnapshot, RecordSnapshot]]
  index = 0
  while index < from_records.length
    from_record = from_records.fetch(index)
    to_record = to_records.fetch(index)
    identity = from_record.identity
    raise_duplicate_identity!(identity) if seen.key?(identity)
    return unless identity == to_record.identity

    seen[identity] = true
    changed << [from_record, to_record] unless from_record.equal?(to_record)
    index += 1
  end
  changed
end

#compare_transition_records(before, after) ⇒ RecordChange?

: (RecordSnapshot?, RecordSnapshot?) -> RecordChange?

Parameters:

Returns:



46
47
48
49
50
# File 'lib/paper_trail_diff/collection_comparator.rb', line 46

def compare_transition_records(before, after)
  return unless before && after && before.identity == after.identity

  @record_comparer.call(before, after)
end

#difference(added:, removed:, changed:) ⇒ CollectionAssociationDiff

: (added: Array, removed: Array, changed: Array) -> CollectionAssociationDiff

Parameters:

Returns:



102
103
104
105
106
107
108
109
# File 'lib/paper_trail_diff/collection_comparator.rb', line 102

def difference(added:, removed:, changed:)
  CollectionAssociationDiff.new(
    kind: @from.kind,
    added: added,
    removed: removed,
    changed: changed
  )
end

#index_records(records) ⇒ Hash[identity, RecordSnapshot]

: (Array) -> Hash[identity, RecordSnapshot]

Parameters:

Returns:



129
130
131
132
133
134
135
136
137
138
# File 'lib/paper_trail_diff/collection_comparator.rb', line 129

def index_records(records)
  index = {} #: Hash[identity, RecordSnapshot]
  records.each do |record|
    identity = record.identity
    raise_duplicate_identity!(identity) if index.key?(identity)

    index[identity] = record
  end
  index
end

#indexed_differenceCollectionAssociationDiff

: () -> CollectionAssociationDiff



91
92
93
94
95
96
97
98
99
# File 'lib/paper_trail_diff/collection_comparator.rb', line 91

def indexed_difference
  from_records = index_records(@from.records)
  to_records = index_records(@to.records)
  difference(
    added: records_missing_from(to_records, from_records),
    removed: records_missing_from(from_records, to_records),
    changed: changed_records(from_records, to_records)
  )
end

#raise_duplicate_identity!(identity) ⇒ void

This method returns an undefined value.

: (identity) -> void

Parameters:

  • (identity)


164
165
166
# File 'lib/paper_trail_diff/collection_comparator.rb', line 164

def raise_duplicate_identity!(identity)
  raise ArgumentError, "duplicate record identity: #{identity.inspect}"
end

#records_missing_from(records, other_records) ⇒ Array[RecordSnapshot]

: (Hash[identity, RecordSnapshot], Hash[identity, RecordSnapshot]) -> Array

Parameters:

Returns:



112
113
114
115
# File 'lib/paper_trail_diff/collection_comparator.rb', line 112

def records_missing_from(records, other_records)
  identities = sorted_identities(records.keys - other_records.keys)
  identities.map { |identity| records.fetch(identity) }
end

#sortable_id(id) ⇒ Array[untyped]

: (untyped) -> Array

Parameters:

  • (Object)

Returns:

  • (Array[untyped])


155
156
157
158
159
160
161
# File 'lib/paper_trail_diff/collection_comparator.rb', line 155

def sortable_id(id)
  case id
  when Numeric then [0, id, '']
  when String, Symbol then [1, 0, id.to_s]
  else [2, 0, id.inspect]
  end
end

#sortable_identity(identity) ⇒ Array[untyped]

Identities sort by type first so that mixed id types stay comparable, then naturally within one type. Ordering by the printed form instead would put id 10 before id 2, which is deterministic but reads as unsorted wherever a result is rendered. : (identity) -> Array

Parameters:

  • (identity)

Returns:

  • (Array[untyped])


150
151
152
# File 'lib/paper_trail_diff/collection_comparator.rb', line 150

def sortable_identity(identity)
  [identity.fetch(0), *sortable_id(identity.fetch(1))]
end

#sorted_identities(identities) ⇒ Array[identity]

: (Array) -> Array

Parameters:

  • (Array[identity])

Returns:

  • (Array[identity])


141
142
143
# File 'lib/paper_trail_diff/collection_comparator.rb', line 141

def sorted_identities(identities)
  identities.sort_by { |identity| sortable_identity(identity) }
end

#transition_difference(transition) ⇒ CollectionAssociationDiff

: (CollectionTransition) -> CollectionAssociationDiff



32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/paper_trail_diff/collection_comparator.rb', line 32

def transition_difference(transition)
  @from.validate_unique_identities!
  @to.validate_unique_identities!
  before = transition.before
  after = transition.after
  changed = compare_transition_records(before, after)
  difference(
    added: before.nil? && after ? [after] : [],
    removed: after.nil? && before ? [before] : [],
    changed: changed ? [changed] : []
  )
end