class GarlandRails::Base

Public Class Methods

any?(belongs_to = nil) click to toggle source
# File lib/garland_rails/utils.rb, line 55
def self.any?(belongs_to = nil)
  self.thread(belongs_to).any?
end
belongs_to(name, scope = nil, options = {}, &extension) click to toggle source

everything seems to work without `polymorphic: true`, but we will set it just for accordance to docs

Calls superclass method
# File lib/garland_rails/base.rb, line 24
def self.belongs_to(name, scope = nil, options = {}, &extension)
  if scope.class == Hash
    options = scope
    scope = nil
  end
  options = options.merge(foreign_key: "belongs_to_id")

  super(name, scope, options, &extension)
end
continuous?(belongs_to = nil) click to toggle source
# File lib/garland_rails/utils.rb, line 30
def self.continuous?(belongs_to = nil)
  tail = self.tail(belongs_to)
  head = self.head(belongs_to)
  return false unless tail && head

  current_bulb = tail
  current_hash = tail.safe_eval_entity
  items_counted = 1
  while current_bulb.next_id do
    items_counted += 1
    current_bulb = current_bulb.next
    if current_bulb.entity_type == DIFF
      current_hash = HashDiffSym.patch!(current_hash, current_bulb.safe_eval_entity)
    else
      break
    end
  end

  items_counted == self.thread(belongs_to).size && current_hash == head.safe_eval_entity
end
head(belongs_to = nil) click to toggle source
# File lib/garland_rails/utils.rb, line 19
def self.head(belongs_to = nil)
  self.thread(belongs_to).find_by(next_id: nil)
end
init(hash, belongs_to = nil) click to toggle source
# File lib/garland_rails/push.rb, line 33
def self.init(hash, belongs_to = nil)
  common_props = self._split_belongs_to(belongs_to)

  tail_props = common_props.merge(entity: {}.to_s, entity_type: SNAPSHOT)
  brand_new_tail = self.new(tail_props)

  diff = HashDiffSym.diff({}, hash)
  first_diff_props = common_props.merge(entity: diff.to_s, entity_type: DIFF)
  first_diff = self.new(first_diff_props)

  head_props = common_props.merge(entity: hash.to_s, entity_type: SNAPSHOT)
  brand_new_head = self.new(head_props)

  self.transaction do
     ActiveRecord::Base.connection.create_savepoint("savepoint_before_init")

     # first id: tail ({})
     # second id: head (latest snapshot)
     # third+: diffs
     unless brand_new_tail.save
       Rails.logger.error("Unable to create new tail with props '#{tail_props}'")
       ActiveRecord::Base.connection.release_savepoint("savepoint_before_init")
       return nil
     end

     # belongs_to validations were in `brand_new_tail.save`
     # here and below validations may be skipped as long as we check for continuity later
     brand_new_head.save(validate: false)
     first_diff.save(validate: false)
     brand_new_tail.update_attribute(:next_id, first_diff.id)
     first_diff.update_attribute(:previous_id, brand_new_tail.id)
     first_diff.update_attribute(:next_id, brand_new_head.id)
     brand_new_head.update_attribute(:previous_id, first_diff.id)

     unless self.continuous?(belongs_to)
       Rails.logger.error("Initialized garland is not continuous")
       ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_init")
       ActiveRecord::Base.connection.release_savepoint("savepoint_before_init")
       return nil
     end
  end

  first_diff
end
insert_diff(hash, belongs_to = nil) click to toggle source
# File lib/garland_rails/push.rb, line 78
def self.insert_diff(hash, belongs_to = nil)
  head = self.head(belongs_to)
  last_diff = self.find_by(id: head.previous_id)
  common_props = self._split_belongs_to(belongs_to)

  diff = HashDiffSym.diff(head.safe_eval_entity, hash)
  return unless diff.any?

  new_diff_props = common_props.merge(
    entity: diff.to_s,
    entity_type: DIFF,
    previous_id: last_diff.id,
    next_id: head.id,
  )
  new_diff = self.new(new_diff_props)

  self.transaction do
     ActiveRecord::Base.connection.create_savepoint("savepoint_before_insert_diff")

     # insert_diff should not use skipping valudatuons methods
     # because we don't want to check for continuity on every push
     unless new_diff.save
       Rails.logger.error("Unable to create new_diff with props '#{new_diff_props}'")
       ActiveRecord::Base.connection.release_savepoint("savepoint_before_init")
       return nil
     end

     last_diff.next_id = new_diff.id
     unless last_diff.save
       Rails.logger.error("Unable to save last_diff with 'next_id' = '#{new_diff.id}'")
       ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_insert_diff")
       ActiveRecord::Base.connection.release_savepoint("savepoint_before_init")
       return nil
     end

     head.previous_id = new_diff.id
     unless head.save
       Rails.logger.error("Unable to save head with 'previous_id' = '#{new_diff.id}'")
       ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_insert_diff")
       ActiveRecord::Base.connection.release_savepoint("savepoint_before_init")
       return nil
     end

     head.entity = hash.to_s
     unless head.save
       Rails.logger.error("Unable to save head with 'entity' = '#{hash}'")
       ActiveRecord::Base.connection.exec_rollback_to_savepoint("savepoint_before_insert_diff")
       ActiveRecord::Base.connection.release_savepoint("savepoint_before_init")
       return nil
     end
  end

  new_diff
end
last_diff(belongs_to = nil) click to toggle source
# File lib/garland_rails/utils.rb, line 23
def self.last_diff(belongs_to = nil)
  head = self.head(belongs_to)
  return nil unless head

  head.previous
end
push(args) click to toggle source
# File lib/garland_rails/push.rb, line 3
def self.push(args)
  return nil unless args.class == Hash

  if args[:hash]
    hash = args[:hash]
    return nil unless hash.class == Hash

    belongs_to = args[:belongs_to]
    if belongs_to
      belongs_to_params = self._split_belongs_to(belongs_to)
      belongs_to_id, belongs_to_type =
        belongs_to_params.values_at(:belongs_to_id, :belongs_to_type)
    end
  else
    hash = args
    belongs_to = nil
    belongs_to_id = nil
    belongs_to_type = nil
  end

  head = self.head(belongs_to)
  if head
    diff = self.insert_diff(hash, belongs_to)
  else
    diff = self.init(hash, belongs_to)
  end

  diff
end
table_type(record) click to toggle source
# File lib/garland_rails/utils.rb, line 51
def self.table_type(record)
  record.class.name
end
tail(belongs_to = nil) click to toggle source
# File lib/garland_rails/utils.rb, line 15
def self.tail(belongs_to = nil)
  self.thread(belongs_to).find_by(previous_id: nil)
end
thread(belongs_to = nil) click to toggle source

REVIEW: shouldn't thread(), tail() and head() have the same args as push()?

# File lib/garland_rails/utils.rb, line 4
def self.thread(belongs_to = nil)
  if belongs_to
    return self.where(
      "belongs_to_id = ? AND belongs_to_type = ?",
      belongs_to.id, table_type(belongs_to)
    )
  else
    return self.where("belongs_to_id is null AND belongs_to_type is null")
  end
end

Private Class Methods

_split_belongs_to(belongs_to) click to toggle source
# File lib/garland_rails/utils.rb, line 74
def self._split_belongs_to(belongs_to)
  if belongs_to
    belongs_to_id = belongs_to.id
    belongs_to_type = table_type(belongs_to)
  else
    belongs_to_id = nil
    belongs_to_type = nil
  end

  { belongs_to_id: belongs_to_id, belongs_to_type: belongs_to_type }
end

Public Instance Methods

==(other) click to toggle source

Minitest raises “No visible difference in the *#inspect output” error while comparing Garland objects without this.

# File lib/garland_rails/base.rb, line 18
def ==(other)
  self.attributes == other.attributes
end
next() click to toggle source
# File lib/garland_rails/utils.rb, line 59
def next
  Garland.find(self.next_id) if self.next_id
end
previous() click to toggle source
# File lib/garland_rails/utils.rb, line 63
def previous
  Garland.find(self.previous_id) if self.previous_id
end
safe_eval_entity() click to toggle source
# File lib/garland_rails/utils.rb, line 67
def safe_eval_entity
  return nil unless self.entity =~ /\[.*\]/ || self.entity =~ /{.*}/

  eval(self.entity)
end
snapshot(patch_direction = :backward) click to toggle source
# File lib/garland_rails/snapshot.rb, line 3
def snapshot(patch_direction = :backward)
  case patch_direction
  when :forward
    approach_nearest_snapshot_method = :previous
    back_method = :next
    patch_method = :patch!
  when :backward
    approach_nearest_snapshot_method = :next
    back_method = :previous
    patch_method = :unpatch!
  end

  if self.entity_type == SNAPSHOT
    snapshot = self.safe_eval_entity
  else
    nearest_snapshot = self
    while nearest_snapshot.entity_type != SNAPSHOT do
      nearest_snapshot = nearest_snapshot.send(approach_nearest_snapshot_method)
    end

    snapshot = nearest_snapshot.safe_eval_entity
    current_bulb = nearest_snapshot.send(back_method)
    loop do
      break if current_bulb == self
      HashDiffSym.send(patch_method, snapshot, current_bulb.safe_eval_entity)
      current_bulb = current_bulb.send(back_method)
    end

    # this comes from asymmetry of Garland model
    #
    # entity => producing snapshot:
    #
    # tail  => {}
    # diff1 => hash1
    # diff2 => hash2
    # diff3 => hash3
    # head  => hash3
    if patch_direction == :forward
      HashDiffSym.send(patch_method, snapshot, current_bulb.safe_eval_entity)
    end
  end

  Garland.new(
    entity_type: SNAPSHOT,
    entity: snapshot.to_s,
    created_at: self.created_at,
    type: self.type,
  )
end