Module: Onceler::Recordable

Defined in:
lib/onceler/recordable.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.extended(instance) ⇒ Object



5
6
7
8
9
10
11
# File 'lib/onceler/recordable.rb', line 5

def self.extended(instance)
  instance.instance_eval do
    @__retvals = {}
    @__inherited_retvals = {}
    @__ignore_ivars = instance_variables
  end
end

Instance Method Details

#__associations_equal?(obj1, obj2) ⇒ Boolean

if a nested once block updates an inherited object’s associations, we want to know about it

Returns:

  • (Boolean)


74
75
76
77
78
79
# File 'lib/onceler/recordable.rb', line 74

def __associations_equal?(obj1, obj2)
  cache1 = obj1.instance_variable_get(:@association_cache)
  cache2 = obj2.instance_variable_get(:@association_cache)
  cache1.size == cache2.size &&
  cache1.all? { |k, v| cache2.key?(k) && __values_equal?(v.target, cache2[k].target) }
end

#__data(inherit = false) ⇒ Object



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/onceler/recordable.rb', line 81

def __data(inherit = false)
  @__data ||= {}
  @__data[inherit] ||= begin
    @__comparison_cache = {}
    data = [__ivars(inherit), __retvals(inherit)]
    begin
      data = Marshal.dump(data)
    rescue TypeError
      data.each do |hash|
        hash.each do |key, val|
          find_dump_error(key, val)
        end
      end
      raise # find_dump_error should have re-raised, but just in case...
    ensure
      __visited_dump_vars.clear
    end
    @__comparison_cache = nil
    data
  end
end

#__ivars(inherit = false) ⇒ Object



36
37
38
39
40
41
42
43
44
45
# File 'lib/onceler/recordable.rb', line 36

def __ivars(inherit = false)
  ivars = instance_variables - @__ignore_ivars
  ivars.inject({}) do |hash, key|
    if key.to_s !~ /\A@__/
      val = instance_variable_get(key)
      hash[key] = val if __mutated?(key, val) || inherit
    end
    hash
  end
end

#__mutated?(key, val) ⇒ Boolean

we don’t include inherited stuff in __data, because we might need to interleave things from an intermediate before(:each) at run time

Returns:

  • (Boolean)


49
50
51
52
53
54
55
56
57
# File 'lib/onceler/recordable.rb', line 49

def __mutated?(key, val)
  # top-level recorders don't inherit anything, so we always want to return true
  return true unless @__inherited_cache
  # need to do both types of comparison, i.e. it's the same object in
  # memory (not reassigned), and nothing about it has been changed
  return true unless @__inherited_values[key].equal?(val)
  return true unless __values_equal?(@__inherited_cache[key], val)
  false
end

#__prepare_recording(recording) ⇒ Object



13
14
15
16
17
18
19
20
21
22
# File 'lib/onceler/recordable.rb', line 13

def __prepare_recording(recording)
  method = recording.name
  define_singleton_method(method) do
    if @__retvals.key?(method)
      @__retvals[method]
    else
      @__retvals[method] = __record(recording)
    end
  end
end

#__record(recording) ⇒ Object



24
25
26
# File 'lib/onceler/recordable.rb', line 24

def __record(recording)
  instance_eval(&recording.block)
end

#__retvals(inherit = false) ⇒ Object



28
29
30
31
32
33
34
# File 'lib/onceler/recordable.rb', line 28

def __retvals(inherit = false)
  retvals = @__inherited_retvals.merge(@__retvals)
  retvals.inject({}) do |hash, (key, val)|
    hash[key] = val if __mutated?(key, val) || inherit
    hash
  end
end

#__values_equal?(obj1, obj2) ⇒ Boolean

Returns:

  • (Boolean)


59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/onceler/recordable.rb', line 59

def __values_equal?(obj1, obj2)
  if ActiveRecord::Base === obj1 && ActiveRecord::Base === obj2
    cache_key = [obj1, obj2]
    return @__comparison_cache[cache_key] if @__comparison_cache.key?(cache_key)
    # so as to avoid cycles while traversing AR associations
    @__comparison_cache[cache_key] = true
    @__comparison_cache[cache_key] = obj1.attributes == obj2.attributes &&
                                     __associations_equal?(obj1, obj2)
  else
    obj1 == obj2
  end
end

#__visited_dump_varsObject



103
104
105
# File 'lib/onceler/recordable.rb', line 103

def __visited_dump_vars
  @__visited_dump_vars ||= Set.new
end

#copy_from(other) ⇒ Object



142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/onceler/recordable.rb', line 142

def copy_from(other)
  # need two copies of things for __mutated? checks (see above)
  @__inherited_cache = Marshal.load(other.__data(:inherit)).inject(&:merge)
  ivars, retvals = Marshal.load(other.__data(:inherit))
  @__inherited_retvals = retvals
  @__inherited_values = ivars.merge(retvals)
  ivars.each do |key, value|
    instance_variable_set(key, value)
  end
  retvals.each do |key, value|
    define_singleton_method(key) { value }
  end
end

#find_dump_error(key, val, prefix = "") ⇒ Object



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
# File 'lib/onceler/recordable.rb', line 107

def find_dump_error(key, val, prefix = "")
  return if __visited_dump_vars.include?(val)
  __visited_dump_vars << val

  Marshal.dump(val)
rescue TypeError

  # see if anything inside val can't be dumped...
  sub_prefix = "#{prefix}#{key} (#<#{val.class}>) => "

  if val.respond_to?(:marshal_dump)
    find_dump_error("marshal_dump", val.marshal_dump, sub_prefix)
  else
    # instance var?
    val.instance_variables.each do |k|
      v = val.instance_variable_get(k)
      find_dump_error(k, v, sub_prefix)
    end

    # hash key/value?
    val.each_pair do |k, v|
      find_dump_error("hash key #{k}", k, sub_prefix)
      find_dump_error("[#{k.inspect}]", v, sub_prefix)
    end if val.respond_to?(:each_pair)

    # array element?
    val.each_with_index do |v, i|
      find_dump_error("[#{i}]", v, sub_prefix)
    end if val.respond_to?(:each_with_index)
  end

  # guess it's val proper
  raise TypeError.new("Unable to dump #{prefix}#{key} (#<#{val.class}>) in #{self.class.[:location]}: #{$!}")
end