Module: ForestAdminAgent::AuditTrail::Diff

Defined in:
lib/forest_admin_agent/audit_trail/diff.rb

Overview

Minimal structural diff. Nested hashes and arrays of hashes are recursed into, so only the keys/indexes whose leaf value actually changed are kept — a single sub-field change does not store the whole object/array. Scalars, primitive arrays, dates and other values are compared and kept as a whole.

Ruby's == already performs deep, key-order-independent equality on Hash and (ordered) equality on Array, so it is used directly as the equality primitive.

Constant Summary collapse

ABSENT =

A key that does not exist on one side of the diff. It is never stored: the key is simply left out of that side's hash, so {"flag" => nil} (a key holding nil) and {} (no key) stay tellable apart in the database — which a revert needs and a string sentinel would only fake.

Object.new.freeze

Class Method Summary collapse

Class Method Details

.changed_values(before, patch, columns) ⇒ Hash{Symbol=>Hash}

Build the previous/new value hashes for the writable columns that actually changed.

Parameters:

  • before (Hash)

    snapshot of the record before the change (string keys)

  • patch (Hash)

    the values being written (string keys); only present keys are considered

  • columns (Array<String>)

    writable column names to inspect

Returns:

  • (Hash{Symbol=>Hash})

    { previous_values:, new_values: }



35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/forest_admin_agent/audit_trail/diff.rb', line 35

def changed_values(before, patch, columns)
  previous_values = {}
  new_values = {}

  columns.each do |column|
    delta = patch.key?(column) ? diff(before[column], patch[column]) : nil
    next unless delta

    previous_values[column] = delta[:previous]
    new_values[column] = delta[:next]
  end

  { previous_values: previous_values, new_values: new_values }
end

.diff(before, after) ⇒ Hash{Symbol=>Object}?

Returns { previous:, next: } of the changed leaves, or nil when equal.

Returns:

  • (Hash{Symbol=>Object}, nil)

    { previous:, next: } of the changed leaves, or nil when equal.



19
20
21
22
23
24
25
26
27
# File 'lib/forest_admin_agent/audit_trail/diff.rb', line 19

def diff(before, after)
  return nil if before == after

  return diff_hashes(before, after) if before.is_a?(Hash) && after.is_a?(Hash)

  return diff_object_arrays(before, after) if object_array?(before) && object_array?(after)

  { previous: before.nil? ? nil : before, next: after.nil? ? nil : after }
end

.object_array?(value) ⇒ Boolean

Arrays whose every element is a hash (record-like collections, e.g. a workflow history).

Returns:

  • (Boolean)


51
52
53
# File 'lib/forest_admin_agent/audit_trail/diff.rb', line 51

def object_array?(value)
  value.is_a?(Array) && !value.empty? && value.all?(Hash)
end

.revert(current, previous, changed) ⇒ Object

Undo one recorded change: given the value as it stands now and the two sides of the diff that produced it, return the value as it was before. Nested hashes are walked so untouched keys keep their current value; a key missing from previous was added by the change, so it goes away.

Parameters:

  • current (Object)

    the value as it stands now (may be nil when the record is gone)

  • previous (Object)

    the previous_values side of the recorded diff

  • changed (Object)

    the new_values side of the recorded diff



105
106
107
108
109
110
# File 'lib/forest_admin_agent/audit_trail/diff.rb', line 105

def revert(current, previous, changed)
  return revert_array(current, previous, changed) if current.is_a?(Array) && partial?(previous, changed)
  return revert_hash(current, previous, changed) if current.is_a?(Hash) && partial?(previous, changed)

  previous
end