Tree Diff

Compare large object trees. Like a generic and standalone ActiveModel::Dirty, but completely ORM agnostic.

The gem is still new and I'm still adding features, but it's stable and released.

Installation

gem install tree_diff

Getting Started

  1. Define a diff class that inherits from TreeDiff. Pass an array of hashes and arrays to define your object tree to observe.

    This format is just like how sets of nested attributes are passed to strong params.

    class MyDiffClass < TreeDiff
      observe :status, :created_at, :user_id
        items: [:status, :description, :cost],
        starting_location: [:latitude, :longitude, :updated_at,
                            address: [:city, :state, :country]
    end
    
  2. Instantiate your diff class and pass it the object you want to compare just before mutating it. For example, controller usage:

    class ThingsController
      def update
        thing = Thing.find(params[:id])
        my_diff = MyDiffClass.new(thing)
    
        if thing.update(thing_params)
          handle_stuff if my_diff.saw_any_change?
          redirect_to thing, notice: 'Updated thing.'
        else
          # ...
        end
      end
    end
    
  3. handle_stuff will only be executed if any of the observed attributes changed -- my_diff.saw_any_change?. To see what the actual changes were, use my_diff.changes.

Defining conditions

Sometimes you don't want to make attribute comparisons if the object receiving the method is in a certain state:

class MyDiffClass < TreeDiff
  observe :reference_code

  condition [:reference_code] do |thing|
    thing.status == 'enabled'
  end
end

This will only call and compare :reference_code if Thing's status is "enabled". This works as expected for associations:

class MyDiffClass < TreeDiff
  observe items: [:id, tags: [{categories: [:name, :is_active]}]]

  condition [:tags, :categories, :name] do |category|
    category.is_active
  end
end

The object passed to the block is always the receiver of the attribute method. It's an instance of the second to the last segment of the call chain.

Bear in mind the condition is called on both the mold of the object in its "old" state and the current full object. Because TreeDiff works off of call chains only, the "old" object is nothing but a mock, meaning you can only use other attributes you've observed in your conditions. Otherwise, you can add a virtual attribute.

Virtual Attributes

NOTE: Not implemented yet, work in progress.

If you want to create conditions using attributes that are not observed, you can add them as virtual attributes. That will define the method on the Mold (mock) object so it can be safely called.

class MyDiffClass < TreeDiff
  observe items: [:id, tags: [{categories: [:name]}]]

  virtual_attributes [:tags, :categories, :is_active],
                     [:tags, :categories, :created_at]


  condition [:tags, :categories, :name] do |category|
    category.is_active && category.created_at > 1.year.ago
  end
end

Documentation / Reference

RubyDoc