Module: Platanus::StackedAttr::ClassMethods

Defined in:
lib/platanus/stacked.rb

Instance Method Summary collapse

Instance Method Details

#has_stacked(_name, _options = {}) ⇒ Object

Adds an stacked attribute to the model.

Raises:



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/platanus/stacked.rb', line 22

def has_stacked(_name, _options={})

  # check option support
  raise NotSupportedError.new('Only autosave mode is supported') if _options[:autosave] == false
  raise NotSupportedError.new('has_many_through is not supported yet') if _options.has_key? :through

  # prepare names
  tname = _name.to_s
  tname_single = tname.singularize
  tname_class = _options.fetch(:class_name, tname_single.camelize)

  # generate top_value property
  top_value_prop = "top_#{tname_single}"
  if _options.has_key? :top_value_key
    belongs_to top_value_prop.to_sym, class_name: tname_class, foreign_key: _options.delete(:top_value_key)
  elsif self.column_names.include? "#{top_value_prop}_id"
    belongs_to top_value_prop.to_sym, class_name: tname_class
  else
    instance_var = "@_last_#{tname_single}".to_sym
    send :define_method, top_value_prop do
      # Storing the last stacked value will not prevent race conditions
      # when simultaneous updates occur.
      last = instance_variable_get instance_var
      return last unless last.nil?
      instance_variable_set(instance_var, self.send(tname).first)
    end
    send :define_method, "#{top_value_prop}=" do |_top|
      instance_variable_set(instance_var, _top)
    end
  end
  send :private, "#{top_value_prop}="

  # prepare cached attributes
  to_cache = _options.delete(:cached)
  to_cache_prf = if _options[:cache_prf].nil? then 'last_' else _options.delete(:cache_prf) end # TODO: deprecate

  unless to_cache.nil?
    to_cache = to_cache.map do |cache_attr|
      unless cache_attr.is_a? Hash
        name = cache_attr.to_s
        # attr_protected(to_cache_prf + name)
        send :define_method, name do self.send(to_cache_prf + name) end # generate read-only aliases without prefix. TODO: deprecate
        { to: to_cache_prf + name, from: name }
      else
        # TODO: Test whether options are valid.
        cache_attr
      end
    end
  end

  # callbacks
  on_stack = _options.delete(:on_stack)

  # limits and ordering
  # TODO: Support other kind of ordering, this would require to reevaluate top on every push
  _options[:order] = 'created_at DESC, id DESC'
  _options[:limit] = 10 if _options[:limit].nil?

  # setup main association
  has_many _name, _options

  cache_step = ->(_ctx, _top, _top_is_new) {
    # cache required fields
    return if to_cache.nil?
    to_cache.each do |cache_attr|
      value = if cache_attr.has_key? :from
        _top.nil? ? _top : _top.send(cache_attr[:from])
      else
        _ctx.send(cache_attr[:virtual], _top, _top_is_new)
      end
      _ctx.send(cache_attr[:to].to_s + '=', value)
    end
  }

  after_step = ->(_ctx, _top) {
    # update top value property
    _ctx.send("#{top_value_prop}=", _top)

    # execute after callback
    _ctx.send(on_stack, _top) unless on_stack.nil?
  }

  send :define_method, "push_#{tname_single}!" do |obj|
    self.class.transaction do

      # cache, then save if new, then push and finally process state
      cache_step.call(self, obj, true)
      self.save! if self.new_record? # make sure there is an id BEFORE pushing
      raise ActiveRecord::RecordInvalid.new(obj) unless send(tname).send('<<',obj)
      after_step.call(self, obj)

      self.save! if self.changed? # Must save again, no other way...
    end
  end

  send :define_method, "push_#{tname_single}" do |obj|
    begin
      return send("push_#{tname_single}!", obj)
    rescue ActiveRecord::RecordInvalid
      return false
    end
  end

  send :define_method, "restore_#{tname}!" do
    self.class.transaction do

        # find current top, then restore stack state
        top = self.send(_name).first
        cache_step.call(self, top, false)
        after_step.call(self, top)

        self.save! if self.changed?
    end
  end

  send :define_method, "restore_#{tname}" do
    begin
      return send("restore_#{tname}!")
    rescue ActiveRecord::RecordInvalid
      return false
    end
  end
end