graph_mediator 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|