graph_mediator 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +4 -0
- data/.gitignore +26 -0
- data/LICENSE +20 -0
- data/README.rdoc +136 -0
- data/Rakefile +32 -0
- data/graph_mediator.gemspec +31 -0
- data/lib/graph_mediator.rb +509 -0
- data/lib/graph_mediator/locking.rb +50 -0
- data/lib/graph_mediator/mediator.rb +260 -0
- data/lib/graph_mediator/version.rb +3 -0
- data/spec/database.rb +12 -0
- data/spec/examples/course_example_spec.rb +91 -0
- data/spec/examples/dingo_pen_example_spec.rb +288 -0
- data/spec/graph_mediator_spec.rb +500 -0
- data/spec/integration/changes_spec.rb +159 -0
- data/spec/integration/locking_tests_spec.rb +214 -0
- data/spec/integration/nesting_spec.rb +113 -0
- data/spec/integration/threads_spec.rb +59 -0
- data/spec/integration/validation_spec.rb +19 -0
- data/spec/investigation/alias_method_chain_spec.rb +170 -0
- data/spec/investigation/insert_subclass_spec.rb +122 -0
- data/spec/investigation/insert_superclass_spec.rb +131 -0
- data/spec/investigation/module_super_spec.rb +88 -0
- data/spec/investigation/self_decorating.rb +55 -0
- data/spec/mediator_spec.rb +201 -0
- data/spec/reservations/lodging.rb +4 -0
- data/spec/reservations/party.rb +4 -0
- data/spec/reservations/party_lodging.rb +4 -0
- data/spec/reservations/reservation.rb +18 -0
- data/spec/reservations/schema.rb +33 -0
- data/spec/spec.opts +3 -0
- data/spec/spec_helper.rb +65 -0
- metadata +173 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
module GraphMediator
|
2
|
+
# Overrides to ActiveRecord::Optimistic::Locking to ensure that lock_column is updated
|
3
|
+
# during the +versioning+ phase of a mediated transaction.
|
4
|
+
module Locking
|
5
|
+
|
6
|
+
def self.included(base)
|
7
|
+
base.extend(ClassMethods)
|
8
|
+
base.send(:alias_method, :locking_enabled_without_mediation?, :locking_enabled?)
|
9
|
+
base.send(:include, InstanceMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
# Overrides ActiveRecord::Base.update_counters to skip locking if currently mediating
|
14
|
+
# the passed id.
|
15
|
+
def update_counters(ids, counters)
|
16
|
+
# id may be an array of ids...
|
17
|
+
unless currently_mediating?(ids)
|
18
|
+
# if none are being mediated can proceed as normal
|
19
|
+
super
|
20
|
+
else
|
21
|
+
# we have to go one by one unfortunately
|
22
|
+
Array(ids).each do |id|
|
23
|
+
currently_mediating?(id) ?
|
24
|
+
update_counters_without_lock(id, counters) :
|
25
|
+
super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
module InstanceMethods
|
32
|
+
# Overrides ActiveRecord::Locking::Optimistic#locking_enabled?
|
33
|
+
#
|
34
|
+
# * True if we are not in a mediated_transaction and lock_enabled? is true
|
35
|
+
# per ActiveRecord (lock_column exists and lock_optimistically? true)
|
36
|
+
# * True if we are in a mediated_transaction and lock_enabled? is true per
|
37
|
+
# ActiveRecord and we are in the midst of the version bumping phase of the transaction.
|
38
|
+
#
|
39
|
+
# Effectively this ensures that an optimistic lock check and version bump
|
40
|
+
# occurs as usual outside of mediation but only at the end of the
|
41
|
+
# transaction within mediation.
|
42
|
+
def locking_enabled?
|
43
|
+
locking_enabled = locking_enabled_without_mediation?
|
44
|
+
locking_enabled &&= current_mediation_phase == :versioning if mediation_enabled? && currently_mediating?
|
45
|
+
return locking_enabled
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,260 @@
|
|
1
|
+
require 'aasm'
|
2
|
+
|
3
|
+
module GraphMediator
|
4
|
+
# Instances of this class perform the actual mediation work on behalf of a
|
5
|
+
# Proxy#mediated_transaction.
|
6
|
+
class Mediator
|
7
|
+
include AASM
|
8
|
+
|
9
|
+
class IndexedHash < Hash
|
10
|
+
attr_reader :index
|
11
|
+
attr_reader :klass
|
12
|
+
|
13
|
+
def initialize(*args, &block)
|
14
|
+
@index = {}
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(ar_instance, klass, changes)
|
19
|
+
add_to_index(changes)
|
20
|
+
case
|
21
|
+
when ar_instance.new_record? then
|
22
|
+
created_array = self[:_created] ||= []
|
23
|
+
created_array << changes
|
24
|
+
when ar_instance.destroyed? then
|
25
|
+
destroyed_array = self[:_destroyed] ||= []
|
26
|
+
destroyed_array << ar_instance.id
|
27
|
+
else self[ar_instance.id] = changes
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def add_to_index(changes)
|
32
|
+
index.merge!(changes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
class ChangesHash < IndexedHash
|
37
|
+
|
38
|
+
def <<(ar_instance)
|
39
|
+
raise(ArgumentError, "Expected an ActiveRecord::Dirty instance: #{ar_instance}") unless ar_instance.respond_to?(:changed?)
|
40
|
+
klass = ar_instance.class.base_class
|
41
|
+
changes = ar_instance.changes
|
42
|
+
add_to_index(changes)
|
43
|
+
klass_hash = self[klass] ||= IndexedHash.new
|
44
|
+
klass_hash.<<(ar_instance, klass, changes)
|
45
|
+
return self
|
46
|
+
end
|
47
|
+
|
48
|
+
# True if the given attribute was changed in root or a dependent.
|
49
|
+
#
|
50
|
+
# * attribute - symbol or string for attribute to lookup
|
51
|
+
# * klass - optionally, restrict lookup to changes for a particular class.
|
52
|
+
#
|
53
|
+
# Shortcut:
|
54
|
+
# changed_#{attribute}?
|
55
|
+
# #{my_class}_changed_#{attribute}?
|
56
|
+
#
|
57
|
+
def attribute_changed?(attribute, klass = nil)
|
58
|
+
(klass ? _class_hash(klass) : self).index.key?(attribute.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
# True if all the passed attributes were changed in root or a dependent.
|
62
|
+
def all_changed?(*attributes)
|
63
|
+
attributes.all? { |a| attribute_changed?(a) }
|
64
|
+
end
|
65
|
+
|
66
|
+
# True if any of the passed attributes were changed in root or a dependent.
|
67
|
+
def any_changed?(*attributes)
|
68
|
+
attributes.any? { |a| attribute_changed?(a) }
|
69
|
+
end
|
70
|
+
|
71
|
+
# True if a dependent of the given class was added.
|
72
|
+
def added_dependent?(klass)
|
73
|
+
_class_hash(klass).key?(:_created)
|
74
|
+
end
|
75
|
+
|
76
|
+
# True if a dependent of the given class was destroyed.
|
77
|
+
def destroyed_dependent?(klass)
|
78
|
+
_class_hash(klass).key?(:_destroyed)
|
79
|
+
end
|
80
|
+
|
81
|
+
# True if an existing dependent of the given class was updated.
|
82
|
+
def altered_dependent?(klass)
|
83
|
+
!_class_hash(klass).reject { |k,v| k == :_created || k == :_destroyed }.empty?
|
84
|
+
end
|
85
|
+
|
86
|
+
# True only if a dependent of the given class was added or destroyed.
|
87
|
+
def added_or_destroyed_dependent?(klass)
|
88
|
+
added_dependent?(klass) || destroyed_dependent?(klass)
|
89
|
+
end
|
90
|
+
|
91
|
+
# TODO this and altered_dependent may give false positive for empty change hashes (if a record is saved with no changes, there will be a +record_id+ => {} entry)
|
92
|
+
# True if a dependent of the given class as added, destroyed or updated.
|
93
|
+
def touched_any_dependent?(klass)
|
94
|
+
!_class_hash(klass).empty?
|
95
|
+
end
|
96
|
+
|
97
|
+
# TODO raise an error if class does not respond to method? but what about general changed_foo? calls? The point is that this syntax can give you false negatives, because changed_foo? will be false even foo isn't even an attribute -- misspelling an attribute can lead to difficult bugs. This helper may not be a good idea...
|
98
|
+
def method_missing(method)
|
99
|
+
case method.to_s
|
100
|
+
when /(?:(.*)_)?changed_(.*)\?/
|
101
|
+
then
|
102
|
+
klass = $1
|
103
|
+
klass = klass.classify.constantize if klass
|
104
|
+
attribute = $2
|
105
|
+
# XXX Don't define a method here, or you run into issues with Rails class reloading.
|
106
|
+
# After the first call, you hold a reference to an old Class which will no longer work as
|
107
|
+
# a key in a new changes hash.
|
108
|
+
# self.class.__send__(:define_method, method) do
|
109
|
+
return attribute_changed?(attribute, klass)
|
110
|
+
# end
|
111
|
+
# return send(method)
|
112
|
+
else super
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def _class_hash(klass)
|
119
|
+
self.fetch(klass.base_class, nil) || IndexedHash.new
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# An instance of the root ActiveRecord object currently under mediation.
|
124
|
+
attr_accessor :mediated_instance
|
125
|
+
|
126
|
+
# Changes made to mediated_instance or dependents during a transaction.
|
127
|
+
attr_accessor :changes
|
128
|
+
|
129
|
+
# Tracks nested transactions
|
130
|
+
attr_accessor :stack
|
131
|
+
|
132
|
+
aasm_initial_state :idle
|
133
|
+
aasm_state :idle
|
134
|
+
aasm_state :mediating
|
135
|
+
aasm_state :versioning
|
136
|
+
aasm_state :disabled
|
137
|
+
|
138
|
+
aasm_event :start do
|
139
|
+
transitions :from => :idle, :to => :mediating
|
140
|
+
end
|
141
|
+
aasm_event :bump do
|
142
|
+
transitions :from => :mediating, :to => :versioning
|
143
|
+
end
|
144
|
+
aasm_event :disable do
|
145
|
+
transitions :from => :idle, :to => :disabled
|
146
|
+
end
|
147
|
+
aasm_event :done do
|
148
|
+
transitions :from => [:idle, :mediating, :versioning, :disabled], :to => :idle
|
149
|
+
end
|
150
|
+
|
151
|
+
def initialize(instance)
|
152
|
+
raise(ArgumentError, "Given instance has not been initialized for mediation: #{instance}") unless instance.kind_of?(GraphMediator)
|
153
|
+
self.mediated_instance = instance
|
154
|
+
self.changes = ChangesHash.new
|
155
|
+
self.stack = []
|
156
|
+
end
|
157
|
+
|
158
|
+
# Mediation may be disabled at either the Class or instance level.
|
159
|
+
# TODO - global module setting?
|
160
|
+
def mediation_enabled?
|
161
|
+
mediated_instance.mediation_enabled?
|
162
|
+
end
|
163
|
+
|
164
|
+
# The id of the instance we are mediating.
|
165
|
+
def mediated_id
|
166
|
+
mediated_instance.try(:id)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Record the ActiveRecord changes state of the current object. This allows
|
170
|
+
# us to make decisions in after_mediation callbacks based on changed state.
|
171
|
+
def track_changes_for(ar_instance)
|
172
|
+
changes << ar_instance
|
173
|
+
end
|
174
|
+
|
175
|
+
# True if we are currently in a nested mediated transaction call.
|
176
|
+
def nested?
|
177
|
+
stack.size > 1
|
178
|
+
end
|
179
|
+
|
180
|
+
def mediate(&block)
|
181
|
+
debug("mediate called")
|
182
|
+
stack.push(self)
|
183
|
+
result = if idle?
|
184
|
+
begin_transaction &block
|
185
|
+
else
|
186
|
+
debug("nested transaction; mediate yielding instead")
|
187
|
+
yield self
|
188
|
+
end
|
189
|
+
debug("mediate finished successfully")
|
190
|
+
return result
|
191
|
+
|
192
|
+
ensure
|
193
|
+
done! unless nested? # very important, so that calling methods can ensure cleanup
|
194
|
+
stack.pop
|
195
|
+
end
|
196
|
+
|
197
|
+
[:debug, :info, :warn, :error, :fatal].each do |level|
|
198
|
+
define_method(level) do |message|
|
199
|
+
mediated_instance.send("m_#{level}", "\e[4;32;1m#{self} - #{aasm_current_state} :\e[0m #{message}")
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Reload them mediated instance. Throws an ActiveRecord::StaleObjectError
|
204
|
+
# if lock_column has been updated outside of transaction.
|
205
|
+
def refresh_mediated_instance
|
206
|
+
debug "refresh_mediated_instance called"
|
207
|
+
unless mediated_instance.new_record?
|
208
|
+
if mediated_instance.locking_enabled_without_mediation?
|
209
|
+
locking_column = mediated_instance.class.locking_column
|
210
|
+
current_lock_version = mediated_instance.send(locking_column) if locking_column
|
211
|
+
end
|
212
|
+
debug("reloading")
|
213
|
+
mediated_instance.reload
|
214
|
+
raise(ActiveRecord::StaleObjectError) if current_lock_version && current_lock_version != mediated_instance.send(locking_column)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def begin_transaction(&block)
|
221
|
+
debug("begin_transaction called")
|
222
|
+
result = if mediation_enabled?
|
223
|
+
start!
|
224
|
+
_wrap_in_callbacks &block
|
225
|
+
else
|
226
|
+
disable!
|
227
|
+
debug("mediation disabled; begin_transaction yielding instead")
|
228
|
+
yield self
|
229
|
+
end
|
230
|
+
debug("begin_transaction finished successfully")
|
231
|
+
return result
|
232
|
+
end
|
233
|
+
|
234
|
+
def _wrap_in_callbacks
|
235
|
+
debug("_wrap_in_callbacks called")
|
236
|
+
debug("_wrap_in_callbacks before_mediation")
|
237
|
+
mediated_instance.run_callbacks(:before_mediation)
|
238
|
+
debug("_wrap_in_callbacks before_mediation completed")
|
239
|
+
debug("_wrap_in_callbacks yielding")
|
240
|
+
result = yield self
|
241
|
+
# skip after_mediation if failed validation
|
242
|
+
unless !result.nil? && result == false
|
243
|
+
debug("_wrap_in_callbacks yielding completed")
|
244
|
+
debug("_wrap_in_callbacks mediate_reconciles")
|
245
|
+
mediated_instance.run_callbacks(:mediate_reconciles)
|
246
|
+
refresh_mediated_instance # after having reconciled
|
247
|
+
debug("_wrap_in_callbacks mediate_reconciles completed")
|
248
|
+
debug("_wrap_in_callbacks mediate_caches")
|
249
|
+
mediated_instance.run_callbacks(:mediate_caches)
|
250
|
+
debug("_wrap_in_callbacks mediate_caches completed")
|
251
|
+
debug("_wrap_in_callbacks bumping")
|
252
|
+
bump!
|
253
|
+
mediated_instance.touch if mediated_instance.class.locking_enabled?
|
254
|
+
debug("_wrap_in_callbacks bumping done")
|
255
|
+
refresh_mediated_instance # after having cached and versioned
|
256
|
+
end
|
257
|
+
return result
|
258
|
+
end
|
259
|
+
end
|
260
|
+
end
|
data/spec/database.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
gem 'activerecord', '>=2.3.5'
|
4
|
+
require 'active_record'
|
5
|
+
|
6
|
+
ActiveRecord::Base.establish_connection({'adapter' => 'sqlite3', 'database' => ':memory:'})
|
7
|
+
ActiveRecord::Base.logger = Logger.new("#{File.dirname(__FILE__)}/active_record.log")
|
8
|
+
|
9
|
+
def create_schema(&block)
|
10
|
+
connection = ActiveRecord::Base.connection
|
11
|
+
yield connection if block_given?
|
12
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
|
2
|
+
|
3
|
+
create_schema do |conn|
|
4
|
+
conn.create_table(:people, :force => true) do |t|
|
5
|
+
t.string :name
|
6
|
+
t.string :type
|
7
|
+
end
|
8
|
+
|
9
|
+
conn.create_table(:rooms, :force => true) do |t|
|
10
|
+
t.string :building
|
11
|
+
t.string :number
|
12
|
+
end
|
13
|
+
|
14
|
+
conn.create_table(:courses, :force => true) do |t|
|
15
|
+
t.string :name
|
16
|
+
t.string :term
|
17
|
+
t.integer :year
|
18
|
+
t.belongs_to :people
|
19
|
+
t.belongs_to :room
|
20
|
+
t.integer :session_max
|
21
|
+
t.integer :course_version
|
22
|
+
end
|
23
|
+
|
24
|
+
conn.create_table(:schedules, :force => true) do |t|
|
25
|
+
t.belongs_to :room
|
26
|
+
t.belongs_to :course
|
27
|
+
t.belongs_to :session
|
28
|
+
t.string :day_of_the_week
|
29
|
+
t.time :start_time
|
30
|
+
t.time :end_time
|
31
|
+
end
|
32
|
+
|
33
|
+
conn.create_table(:assistants, :force => true) do |t|
|
34
|
+
t.belongs_to :course
|
35
|
+
t.belongs_to :people
|
36
|
+
end
|
37
|
+
|
38
|
+
conn.create_table(:sessions, :force => true) do |t|
|
39
|
+
t.belongs_to :assistant
|
40
|
+
t.belongs_to :room
|
41
|
+
end
|
42
|
+
|
43
|
+
conn.create_table(:students, :force => true) do |t|
|
44
|
+
t.belongs_to :people
|
45
|
+
t.belongs_to :course
|
46
|
+
t.string :grade
|
47
|
+
end
|
48
|
+
|
49
|
+
conn.create_table(:session_students, :force => true) do |t|
|
50
|
+
t.belongs_to :session
|
51
|
+
t.belongs_to :student
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class Person < ActiveRecord::Base; end
|
56
|
+
class Lecturer < Person; end
|
57
|
+
class Student < Person; end
|
58
|
+
class GraduateStudent < Student; end
|
59
|
+
|
60
|
+
class Room < ActiveRecord::Base; end
|
61
|
+
|
62
|
+
class Course < ActiveRecord::Base
|
63
|
+
|
64
|
+
belongs_to :lecturer
|
65
|
+
belongs_to :room
|
66
|
+
has_many :schedules
|
67
|
+
has_many :assistants
|
68
|
+
has_many :sessions, :through => :assistants
|
69
|
+
has_many :students
|
70
|
+
|
71
|
+
# mediate :reconciliation => :adjust_bars, :bumping => :meeting_version
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class Schedule < ActiveRecord::Base
|
76
|
+
belongs_to :course
|
77
|
+
belongs_to :room
|
78
|
+
end
|
79
|
+
|
80
|
+
class Assistant < ActiveRecord::Base
|
81
|
+
belongs_to :course
|
82
|
+
belongs_to :grad_student
|
83
|
+
has_many :sessions
|
84
|
+
has_many :schedule, :through => :session
|
85
|
+
end
|
86
|
+
|
87
|
+
class Session < ActiveRecord::Base
|
88
|
+
belongs_to :assistant
|
89
|
+
belongs_to :room
|
90
|
+
# has_many :schedule, :foreign_key =>
|
91
|
+
end
|
@@ -0,0 +1,288 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))
|
2
|
+
require 'aasm'
|
3
|
+
|
4
|
+
# Okay I lied. This example has dingos.
|
5
|
+
|
6
|
+
create_schema do |conn|
|
7
|
+
|
8
|
+
conn.create_table(:dingo_pens, :force => true) do |t|
|
9
|
+
t.integer :pen_number
|
10
|
+
t.integer :dingos_count
|
11
|
+
t.integer :feed_rate
|
12
|
+
t.integer :biscuit_minimum
|
13
|
+
t.integer :biscuit_maximum
|
14
|
+
t.float :total_biscuits
|
15
|
+
t.float :total_biscuit_weight
|
16
|
+
t.integer :lock_version, :default => 0
|
17
|
+
t.timestamps
|
18
|
+
end
|
19
|
+
|
20
|
+
conn.create_table(:dingos, :force => true) do |t|
|
21
|
+
t.belongs_to :dingo_pen
|
22
|
+
t.string :name
|
23
|
+
t.string :breed
|
24
|
+
t.integer :voracity
|
25
|
+
t.integer :belly
|
26
|
+
t.string :aasm_state
|
27
|
+
t.integer :lock_version, :default => 0
|
28
|
+
t.timestamps
|
29
|
+
end
|
30
|
+
|
31
|
+
conn.create_table(:biscuits, :force => true) do |t|
|
32
|
+
t.belongs_to :dingo_pen
|
33
|
+
t.string :type
|
34
|
+
t.float :weight
|
35
|
+
t.integer :amount
|
36
|
+
t.integer :lock_version, :default => 0
|
37
|
+
t.timestamps
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
# A dingo.
|
43
|
+
class Dingo < ActiveRecord::Base
|
44
|
+
belongs_to :dingo_pen, :counter_cache => true
|
45
|
+
include AASM
|
46
|
+
aasm_initial_state :hungry
|
47
|
+
aasm_state :hungry, :exit => :eat_biscuits!
|
48
|
+
aasm_state :satiated, :exit => :burn_biscuits!
|
49
|
+
aasm_event :eat do
|
50
|
+
transitions :from => :hungry, :to => :satiated, :guard => :full?
|
51
|
+
end
|
52
|
+
aasm_event :run do
|
53
|
+
transitions :from => :satiated, :to => :hungry
|
54
|
+
end
|
55
|
+
|
56
|
+
def eat_biscuits!
|
57
|
+
# puts "#{self}.eat_biscuits"
|
58
|
+
update_attributes(:belly => (belly || 0) + dingo_pen.eat_biscuits(voracity))
|
59
|
+
end
|
60
|
+
|
61
|
+
def burn_biscuits!
|
62
|
+
# puts "#{self}.burn_biscuits"
|
63
|
+
update_attributes(:belly => 0)
|
64
|
+
end
|
65
|
+
|
66
|
+
def full?
|
67
|
+
# puts "#{self}.full? #{belly}, #{voracity}"
|
68
|
+
belly >= voracity
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# A bunch of biscuits.
|
73
|
+
class Biscuit < ActiveRecord::Base
|
74
|
+
belongs_to :dingo_pen
|
75
|
+
|
76
|
+
def consume_weight!(weight_to_consume)
|
77
|
+
amount_to_consume = (weight_to_consume/weight).round
|
78
|
+
amount_consumed = nil
|
79
|
+
if amount >= amount_to_consume
|
80
|
+
self.amount -= amount_to_consume
|
81
|
+
amount_consumed = amount_to_consume
|
82
|
+
else
|
83
|
+
amount_consumed = amount
|
84
|
+
self.amount = 0
|
85
|
+
end
|
86
|
+
save!
|
87
|
+
return amount_consumed * weight
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class BigBiscuit < Biscuit; end
|
92
|
+
class LittleBiscuit < Biscuit; end
|
93
|
+
|
94
|
+
class DingoPen < ActiveRecord::Base
|
95
|
+
has_many :dingos
|
96
|
+
has_many :biscuits
|
97
|
+
|
98
|
+
include GraphMediator
|
99
|
+
|
100
|
+
def purchase_biscuits
|
101
|
+
puts :purchase_biscuits
|
102
|
+
end
|
103
|
+
|
104
|
+
mediate :purchase_biscuits,
|
105
|
+
:dependencies => [Dingo, Biscuit],
|
106
|
+
:when_reconciling => [:adjust_biscuit_supply, :feed_dingos],
|
107
|
+
:when_cacheing => :calculate_biscuit_totals
|
108
|
+
|
109
|
+
def adjust_biscuit_supply
|
110
|
+
#puts "\n* adjusting_biscuit_supply"
|
111
|
+
biscuits.each do |b|
|
112
|
+
b.update_attributes(:amount => DingoPen.shovel_biscuits((biscuit_minimum + biscuit_maximum)/2)) if b.amount < biscuit_minimum
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def feed_dingos
|
117
|
+
#puts "** feed_dingos #{dingos.inspect}"
|
118
|
+
dingos.each { |d| d.eat! if d.hungry? }
|
119
|
+
end
|
120
|
+
|
121
|
+
def eat_biscuits(weight_desired)
|
122
|
+
#puts "** eat_biscuits #{weight_desired}"
|
123
|
+
total_weight_consumed = 0
|
124
|
+
weight_left_to_consume = weight_desired
|
125
|
+
biscuits.each do |b|
|
126
|
+
weight_consumed_from_bin = b.consume_weight!(weight_left_to_consume)
|
127
|
+
#puts "weight_consumed_from_bin #{b.inspect}: #{weight_consumed_from_bin}"
|
128
|
+
total_weight_consumed += weight_consumed_from_bin
|
129
|
+
weight_left_to_consume -= weight_consumed_from_bin
|
130
|
+
break if weight_left_to_consume <= 0
|
131
|
+
end
|
132
|
+
#puts "total_weight_consumed: #{total_weight_consumed}"
|
133
|
+
return total_weight_consumed
|
134
|
+
end
|
135
|
+
|
136
|
+
def calculate_biscuit_totals
|
137
|
+
#puts "** calculate_biscuit_totals #{biscuits.inspect}"
|
138
|
+
update_attributes(
|
139
|
+
:total_biscuits => biscuits.sum('amount'),
|
140
|
+
:total_biscuit_weight => biscuits.sum('weight * amount')
|
141
|
+
)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Class methods
|
145
|
+
class << self
|
146
|
+
# simulates the shoveling of biscuits into DingoPen feeders from the
|
147
|
+
# theoretically BiscuitStore
|
148
|
+
def shovel_biscuits(amount)
|
149
|
+
return amount
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
describe "DingoPen" do
|
155
|
+
|
156
|
+
before(:each) do
|
157
|
+
@dingo_pen_attributes = {
|
158
|
+
:pen_number => 42,
|
159
|
+
:feed_rate => 10,
|
160
|
+
:biscuit_minimum => 50,
|
161
|
+
:biscuit_maximum => 100,
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
it "should initialize" do
|
166
|
+
dp = DingoPen.new
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should create" do
|
170
|
+
dp = DingoPen.create!(@dingo_pen_attributes)
|
171
|
+
dp.lock_version.should == 1
|
172
|
+
end
|
173
|
+
|
174
|
+
it "should create with dingos and biscuits" do
|
175
|
+
dp = DingoPen.new(@dingo_pen_attributes)
|
176
|
+
dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
|
177
|
+
dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
|
178
|
+
dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
|
179
|
+
dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
|
180
|
+
dp.save!
|
181
|
+
dp.lock_version.should == 1
|
182
|
+
end
|
183
|
+
|
184
|
+
context "on create" do
|
185
|
+
|
186
|
+
it "should succeed" do
|
187
|
+
dp = DingoPen.new(@dingo_pen_attributes)
|
188
|
+
dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
|
189
|
+
dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
|
190
|
+
dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
|
191
|
+
dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
|
192
|
+
dp.save!
|
193
|
+
dp.reload
|
194
|
+
dp.dingos_count.should == 2
|
195
|
+
dp.dingos[0].belly.should == 10
|
196
|
+
dp.dingos[1].belly.should == 6
|
197
|
+
# biscuit amounts adjusted and dingos ate
|
198
|
+
dp.total_biscuits.should == 142
|
199
|
+
dp.total_biscuit_weight.should == 67 * 2 + 75 * 0.5
|
200
|
+
dp.lock_version.should == 1
|
201
|
+
end
|
202
|
+
|
203
|
+
it "should succed without mediation" do
|
204
|
+
begin
|
205
|
+
DingoPen.disable_all_mediation!
|
206
|
+
dp = DingoPen.new(@dingo_pen_attributes)
|
207
|
+
dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
|
208
|
+
dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
|
209
|
+
dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
|
210
|
+
dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
|
211
|
+
dp.save!
|
212
|
+
dp.reload
|
213
|
+
dp.dingos_count.should == 2
|
214
|
+
dp.total_biscuits.should be_nil
|
215
|
+
dp.total_biscuit_weight.should be_nil
|
216
|
+
dp.lock_version.should == 0
|
217
|
+
ensure
|
218
|
+
DingoPen.enable_all_mediation!
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
end
|
223
|
+
|
224
|
+
context "on update" do
|
225
|
+
it "should update_calculations after every child" do
|
226
|
+
dp = DingoPen.create!(@dingo_pen_attributes)
|
227
|
+
dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
|
228
|
+
dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
|
229
|
+
dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
|
230
|
+
dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
|
231
|
+
dp.reload
|
232
|
+
dp.dingos_count.should == 2
|
233
|
+
dp.dingos[0].belly.should == 10
|
234
|
+
dp.dingos[1].belly.should == 6
|
235
|
+
# biscuit amounts adjusted and dingos ate
|
236
|
+
dp.total_biscuits.should == 142
|
237
|
+
dp.total_biscuit_weight.should == 67 * 2 + 75 * 0.5
|
238
|
+
dp.lock_version.should == 5
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should have updated calculations only once within a mediated transaction" do
|
242
|
+
dp = DingoPen.create!(@dingo_pen_attributes)
|
243
|
+
dp.mediated_transaction do
|
244
|
+
dp.dingos << Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
|
245
|
+
dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
|
246
|
+
dp.biscuits << BigBiscuit.new(:amount => 35, :weight => 2.0)
|
247
|
+
dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
|
248
|
+
dp.save!
|
249
|
+
end
|
250
|
+
dp.reload
|
251
|
+
dp.dingos_count.should == 2
|
252
|
+
dp.dingos[0].belly.should == 10
|
253
|
+
dp.dingos[1].belly.should == 6
|
254
|
+
# biscuit amounts adjusted, and dingos ate
|
255
|
+
dp.total_biscuits.should == 142
|
256
|
+
dp.total_biscuit_weight.should == 67 * 2 + 75 * 0.5
|
257
|
+
dp.lock_version.should == 2
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context "on delete" do
|
262
|
+
before(:each) do
|
263
|
+
@dp = DingoPen.new(@dingo_pen_attributes)
|
264
|
+
@dp.dingos << @spot = Dingo.new(:name => "Spot", :breed => "Patagonian Leopard Dingo", :voracity => 10)
|
265
|
+
@dp.dingos << Dingo.new(:name => "Foo", :breed => "Theoretical Testing Dingo", :voracity => 5)
|
266
|
+
@dp.biscuits << @big = BigBiscuit.new(:amount => 35, :weight => 2.0)
|
267
|
+
@dp.biscuits << LittleBiscuit.new(:amount => 75, :weight => 0.5)
|
268
|
+
@dp.save!
|
269
|
+
@spot.reload
|
270
|
+
@big.reload
|
271
|
+
end
|
272
|
+
|
273
|
+
it "should increment graph version after deletion of a child" do
|
274
|
+
lambda {
|
275
|
+
@spot.destroy
|
276
|
+
@dp.reload
|
277
|
+
}.should change(@dp, :lock_version).by(1)
|
278
|
+
end
|
279
|
+
|
280
|
+
it "should update_calculations after deletion of a child" do
|
281
|
+
@big.destroy
|
282
|
+
@dp.reload
|
283
|
+
@dp.total_biscuits.should == 75
|
284
|
+
@dp.total_biscuit_weight.should == 75 * 0.5
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
end
|