Class: TreeDiff

Inherits:
Object
  • Object
show all
Defined in:
lib/tree_diff.rb

Defined Under Namespace

Classes: Change

Constant Summary collapse

Mold =

A container for each copied attribute from the source object by each path.

Class.new

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(original_object) ⇒ TreeDiff

Prepares a new comparision. Creates a mold/mock of the object being diffed as the 'old' state according to the relationship tree defined via .observe. Each attribute's value is copied via serializing and deserializing via Marshal.

Parameters:

  • original_object

    The object to be compared, before you mutate any of its attributes.



99
100
101
102
103
104
105
# File 'lib/tree_diff.rb', line 99

def initialize(original_object)
  check_observations

  self.class.class_variable_set(:@@conditions, [])
  @old_object_as_mold = create_mold Mold.new, original_object, self.class.observations
  @current_object = original_object
end

Class Method Details

.attribute_pathsArray

All paths to be walked expressed as arrays, ending in each observed attribute. These are generated upon calling .observe.

Returns:

  • (Array)


64
65
66
67
# File 'lib/tree_diff.rb', line 64

def self.attribute_paths
  class_variable_set(:@@attribute_paths, nil) unless class_variable_defined?(:@@attribute_paths)
  class_variable_get(:@@attribute_paths)
end

.condition(path) {|original_object| ... } ⇒ void

This method returns an undefined value.

Adds a condition to an attribute that dictates whether an attribute is compared. The given block is invoked just before trying to call the attribute to get a comparable value. Called twice per attribute, once for the old value and once for the new.

class MyDiffClass < TreeDiff
  observe details: :order_status

  condition [:details, :order_status] do |order|
    order.order_status == 'delivered'
  end
end

Yield Parameters:

  • original_object

    The original object being compared.

Yield Returns:

  • (Boolean)

    Whether this attribute should be compared.



49
50
51
52
# File 'lib/tree_diff.rb', line 49

def self.condition(path, &condition)
  class_variable_set(:@@conditions, []) unless conditions
  conditions << [path, condition]
end

.conditionsArray

All conditions defined via .condition.

Returns:

  • (Array)


71
72
73
74
# File 'lib/tree_diff.rb', line 71

def self.conditions
  class_variable_set(:@@conditions, nil) unless class_variable_defined?(:@@conditions)
  class_variable_get(:@@conditions)
end

.observationsArray

Holds the original object tree definitions passed to .observe.

Returns:

  • (Array)


56
57
58
59
# File 'lib/tree_diff.rb', line 56

def self.observations
  class_variable_set(:@@observations, nil) unless class_variable_defined?(:@@observations)
  class_variable_get(:@@observations)
end

.observe(*defs) ⇒ void

This method returns an undefined value.

Defines the full tree of relationships and attributes this TreeDiff class observes. Pass a structure of arrays and hashes, similar to a strong_params #permit definition.

class MyDiffClass < TreeDiff
  observe :order_number, :available_at, line_items: [:description, :price, tags: [:name]]
end

Parameters:

  • defs (Array)

    Observation attribute definitions. Anything not in this list will be skipped by the diff.



26
27
28
29
30
# File 'lib/tree_diff.rb', line 26

def self.observe(*defs)
  class_variable_set(:@@observations, defs)
  class_variable_set(:@@attribute_paths, [])
  observe_chain [], defs
end

Instance Method Details

#changed?(path) ⇒ Boolean

Compare all observed paths, find the given path, and check if its value has changed.

Returns:

  • (Boolean)

    Whether the path is changed.



131
132
133
# File 'lib/tree_diff.rb', line 131

def changed?(path)
  changes_at(path).present?
end

#changed_pathsArray of Arrays

Get a collection of changed paths only.

Returns:

  • (Array of Arrays)


152
153
154
# File 'lib/tree_diff.rb', line 152

def changed_paths
  changes_as_objects.map(&:path)
end

#changesArray

Walk all observed paths and compare each resulting value. Returns a structure like:

  [{path: [:line_items, :description], old: ["thing", "other thing"], new: ["other thing", "new thing"]},
   {path: [:line_items, :price_usd_cents], old: [1234, 5678], new: [5678, 1357]},
   {path: [:line_items, :item_categories, :description], old: ['foo', 'bar'], new: ['foo']}]

Returns:

  • (Array)

    A list of each attribute that has changed. Each element is a hash with keys :path, :old, and :new.



124
125
126
# File 'lib/tree_diff.rb', line 124

def changes
  iterate_observations(@old_object_as_mold, @current_object)
end

#changes_as_objectsArray of TreeDiff::Change

Walk all observed paths and compare each resulting value. Returns a structure like:

Examples:

Return all "old" values

diff = MyDiffClass.new(my_object)
# Mutate the object...
changes = diff.changes_as_objects

changes.map(&:old)

Returns:

  • (Array of TreeDiff::Change)

    A list of each attribute that has changed. Each element is an ojbect with methods :path, :old, and :new.



146
147
148
# File 'lib/tree_diff.rb', line 146

def changes_as_objects
  changes.map { |c| Change.new(c.fetch(:path), c.fetch(:old), c.fetch(:new)) }
end

#changes_at(path) ⇒ TreeDiff::Change

Find a change by its path.

Returns:



159
160
161
162
# File 'lib/tree_diff.rb', line 159

def changes_at(path)
  arrayed_path = Array(path)
  changes_as_objects.detect { |c| c.path == arrayed_path }
end

#saw_any_change?Boolean

Check if there is any change at all, otherwise each observed attribute is considered equal before and after the change.

Returns:

  • (Boolean)

    Whether there was a change



110
111
112
# File 'lib/tree_diff.rb', line 110

def saw_any_change?
  changes.present?
end