save_queue 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (38) hide show
  1. data/.gitignore +13 -4
  2. data/.travis.yml +4 -1
  3. data/CONTRIBUTING.md +120 -0
  4. data/HISTORY.md +11 -0
  5. data/LICENSE +13 -4
  6. data/README.md +282 -55
  7. data/Rakefile +10 -3
  8. data/lib/save_queue/exceptions.rb +13 -0
  9. data/lib/save_queue/object.rb +41 -18
  10. data/lib/save_queue/object_queue.rb +87 -0
  11. data/lib/save_queue/plugins/notification/object.rb +35 -0
  12. data/lib/save_queue/plugins/notification/queue.rb +25 -0
  13. data/lib/save_queue/plugins/notification.rb +15 -0
  14. data/lib/save_queue/plugins/validation/exceptions.rb +12 -0
  15. data/lib/save_queue/plugins/validation/queue.rb +16 -17
  16. data/lib/save_queue/plugins/validation.rb +4 -17
  17. data/lib/save_queue/ruby1.9/observer.rb +204 -0
  18. data/lib/save_queue/uniq_queue.rb +38 -0
  19. data/lib/save_queue/version.rb +1 -1
  20. data/lib/save_queue.rb +1 -0
  21. data/save_queue.gemspec +4 -3
  22. data/spec/notification/notification_spec.rb +45 -0
  23. data/spec/notification/object_spec.rb +54 -0
  24. data/spec/notification/queue_spec.rb +28 -0
  25. data/spec/object_queue_spec.rb +155 -0
  26. data/spec/object_spec.rb +208 -0
  27. data/spec/save_queue_spec.rb +75 -0
  28. data/spec/support/object_helpers.rb +10 -0
  29. data/spec/support/queue_helpers.rb +26 -0
  30. data/spec/uniq_queue_spec.rb +132 -0
  31. data/spec/validation/queue_spec.rb +139 -0
  32. data/spec/validation/validation_spec.rb +42 -0
  33. metadata +35 -20
  34. data/lib/save_queue/plugins/validation/object.rb +0 -25
  35. data/lib/save_queue/queue.rb +0 -45
  36. data/spec/save_queue_usage_spec.rb +0 -311
  37. data/spec/support/mock_helpers.rb +0 -17
  38. data/spec/validation_spec.rb +0 -126
@@ -1,39 +1,54 @@
1
- require "save_queue/queue"
2
- require 'active_support/core_ext/class/inheritable_attributes'
1
+ require 'save_queue/object_queue'
3
2
 
4
3
  module SaveQueue
5
4
  module Object
6
- #class_inheritable_accessor :queue_class
7
5
  def self.included base
8
6
  base.class_eval do
9
- class_inheritable_accessor :queue_class
10
- #class<<self
11
- # attr_accessor :queue_class
12
- #end
13
7
 
14
- self.queue_class ||= Queue
8
+ class<<self
9
+ attr_reader :queue_class
10
+
11
+ def queue_class=(klass)
12
+ raise "Your Queue implementation: #{klass} should include Hooks module!" unless klass.include? Hooks
13
+ @queue_class = klass
14
+ end
15
+ end
16
+
17
+ def self.inherited base
18
+ base.queue_class = self.queue_class
19
+ end
20
+
21
+ self.queue_class ||= ObjectQueue
15
22
  end
16
23
  end
17
24
 
25
+
18
26
  module RunAlwaysFirst
19
- # @return [Boolean]
27
+ # can not reilly on save! here, because client may not define it at all
20
28
  def save(*args)
21
- #return false if defined?(super) and false == super
29
+ super_result = true
30
+ super_result = super if defined?(super)
31
+
32
+ return false unless !!super_result
22
33
 
23
- super_saved = true
24
- super_saved = super if defined?(super)
25
- # object is saved here
26
34
  mark_as_saved
27
- return (super_saved and save_queue.save)
35
+ if save_queue.save
36
+ true == super_result ? true : super_result # super_result may be not boolean, String for ex
37
+ else
38
+ false
39
+ end
40
+ end
28
41
 
42
+ # Suppose,that save! raise an Exception if failed to save an object
43
+ def save!
44
+ super if defined?(super)
45
+ mark_as_saved
46
+ save_queue.save!
29
47
  end
30
48
  end
31
49
 
32
-
33
50
  def initialize(*args)
34
- queue = self.class.queue_class.new
35
- instance_variable_set "@_save_queue", queue
36
-
51
+ create_queue
37
52
  super if defined?(super)
38
53
 
39
54
  # this will make RunAlwaysFirst methods triggered first in inheritance tree
@@ -44,6 +59,7 @@ module SaveQueue
44
59
  instance_variable_set "@_changed_mark", true
45
60
  end
46
61
 
62
+ # @returns [Boolean] true if object has been modified
47
63
  def has_unsaved_changes?
48
64
  status = instance_variable_get("@_changed_mark")
49
65
  status.nil? ? false : status
@@ -56,5 +72,12 @@ module SaveQueue
56
72
  def mark_as_saved
57
73
  instance_variable_set "@_changed_mark", false
58
74
  end
75
+
76
+ private
77
+ def create_queue
78
+ klass = self.class.queue_class
79
+ queue = klass.new
80
+ instance_variable_set "@_save_queue", queue
81
+ end
59
82
  end
60
83
  end
@@ -0,0 +1,87 @@
1
+ require 'forwardable'
2
+ # TODO remove hooks or extract to a module
3
+ require 'hooks'
4
+ require 'save_queue/uniq_queue'
5
+
6
+ module SaveQueue
7
+ class ObjectQueue < UniqQueue
8
+ include Hooks
9
+
10
+ define_hook :before_save
11
+ # triggered only after successful save
12
+ define_hook :after_save
13
+
14
+ define_hook :before_add
15
+ define_hook :after_add
16
+
17
+ # @return [Hash] save
18
+ # @option save [Array<Object>] :processed
19
+ # @option save [Array<Object>] :saved
20
+ # @option save [Object] :failed
21
+ # @option save [Array<Object>] :pending
22
+ attr_reader :errors
23
+ def initialize(*args)
24
+ super
25
+ @errors = {}
26
+ end
27
+
28
+ def add object
29
+ run_hook :before_add
30
+
31
+ check_requirements_for object
32
+ result = super object
33
+
34
+ run_hook :after_add, result, object
35
+
36
+ result
37
+ end
38
+
39
+ def << object
40
+ add object
41
+ self
42
+ end
43
+
44
+ alias_method :push, :add
45
+
46
+ def save
47
+ save!
48
+ true
49
+ rescue SaveQueue::Error
50
+ false
51
+ end
52
+
53
+ def save!
54
+ run_hook :before_save
55
+ @errors = {}
56
+ saved = []
57
+ processed = []
58
+
59
+ @queue.each do |object|
60
+ if object.has_unsaved_changes?
61
+
62
+ result = object.save
63
+ if false == result
64
+ @errors[:save] = {:processed => processed, :saved => saved, :failed => object, :pending => @queue - (saved + [object])}
65
+ raise FailedSaveError, errors[:save]
66
+ end
67
+
68
+ saved << object
69
+ end
70
+ processed << object
71
+ end
72
+
73
+ @queue.clear
74
+
75
+ run_hook :after_save
76
+ true
77
+ end
78
+
79
+
80
+ private
81
+ def check_requirements_for object
82
+ [:save, :has_unsaved_changes?].each do |method|
83
+ raise ArgumentError, "#{object.inspect} does not respond to ##{method}" unless object.respond_to? method
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,35 @@
1
+ module SaveQueue
2
+ module Plugins
3
+ module Notification
4
+ module Object
5
+ module AddObserverToQueue
6
+ def create_queue
7
+ super
8
+ queue = instance_variable_get("@_save_queue")
9
+ raise "save queue should respond to add_observer in order to work correctly" unless queue.respond_to? :add_observer
10
+ queue.add_observer(self, :queue_changed_event)
11
+ end
12
+ end
13
+
14
+ def self.included base
15
+
16
+ #queue_creator = Module.new do
17
+ # def create_queue
18
+ # super
19
+ # queue = instance_variable_get("@_save_queue")
20
+ # raise "save queue should respond to add_observer in order to work correctly" unless queue.respond_to? :add_observer
21
+ # queue.add_observer(self, :queue_changed_event)
22
+ # end
23
+ #end
24
+
25
+ #base.send :include, queue_creator
26
+ base.send :include, AddObserverToQueue unless base.include?(AddObserverToQueue)
27
+ end
28
+
29
+ def queue_changed_event(result, object)
30
+ mark_as_changed if result
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,25 @@
1
+ if RUBY_VERSION < "1.9"
2
+ require "save_queue/ruby1.9/observer"
3
+ else
4
+ require 'observer'
5
+ end
6
+
7
+ module SaveQueue
8
+ module Plugins
9
+ module Notification
10
+ module Queue
11
+ def self.included base
12
+ base.send :include, Observable unless base.include? Observable
13
+ base.after_add :change_and_notify # if base.respond_to? :after_add
14
+ end
15
+
16
+ private
17
+ def change_and_notify(*args)
18
+ changed
19
+ notify_observers(*args)
20
+ end
21
+
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ require "save_queue/plugins/notification/queue"
2
+ require "save_queue/plugins/notification/object"
3
+
4
+ module SaveQueue
5
+ module Plugins
6
+ module Notification
7
+ def self.included base
8
+ klass = Class.new(base.queue_class)
9
+ klass.send :include, Notification::Queue unless klass.include? Notification::Queue
10
+ base.send :include, Notification::Object unless base.include? Notification::Object
11
+ base.queue_class = klass
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,12 @@
1
+ module SaveQueue
2
+ class FailedValidationError < Error
3
+ attr_reader :failed_objects
4
+ def initialize(failed_objects)
5
+ @failed_objects = Array(failed_objects)
6
+ end
7
+
8
+ def to_s # Some default way to display errors
9
+ "#{super}: " + @failed_objects.map{|object| "\"#{object.to_s}\": " + object.errors.full_messages.join(', ')}.join("\n")
10
+ end
11
+ end
12
+ end
@@ -1,33 +1,32 @@
1
- require "save_queue/queue"
1
+ require "save_queue/plugins/validation/exceptions"
2
+
2
3
  module SaveQueue
3
4
  module Plugins
4
5
  module Validation
5
- class Queue < ::SaveQueue::Queue
6
- attr_reader :objects_with_errors
7
-
8
- def initialize(*args)
9
- @objects_with_errors = []
10
- super
6
+ module Queue
7
+ def self.included base
8
+ base.before_save :validate! if base.respond_to? :before_save
11
9
  end
12
10
 
13
11
  def valid?
14
- @objects_with_errors = []
12
+ validate
13
+ end
14
+
15
+ def validate
15
16
  @queue.each do |object|
16
- @objects_with_errors << object unless object.valid?
17
+ unless object.valid?
18
+ @errors[:validation] ||= []
19
+ @errors[:validation].push(object)
20
+ end
17
21
  end
18
-
19
- @objects_with_errors.empty?
22
+
23
+ @errors.empty?
20
24
  end
21
25
 
22
26
  def validate!
23
- raise FailedValidationError, @objects_with_errors unless valid?
24
-
27
+ raise FailedValidationError, @errors[:validation] unless valid?
25
28
  true
26
29
  end
27
-
28
- def errors
29
- @objects_with_errors.map(&:errors).reduce(:+)
30
- end
31
30
  end
32
31
  end
33
32
  end
@@ -1,27 +1,14 @@
1
- require "save_queue/queue"
2
-
3
- require "save_queue/plugins/validation/object"
4
1
  require "save_queue/plugins/validation/queue"
2
+ require "save_queue/plugins/validation/exceptions"
5
3
 
6
4
 
7
5
  module SaveQueue
8
6
  module Plugins
9
7
  module Validation
10
8
  def self.included base
11
- # must be included after SaveQueue::Object
12
- base.send :include, Validation::Object
13
- base.queue_class = Validation::Queue
14
- end
15
-
16
- class FailedValidationError < RuntimeError
17
- attr_reader :failed_objects
18
- def initialize(failed_objects)
19
- @failed_objects = Array(failed_objects)
20
- end
21
-
22
- def to_s # Some default way to display errors
23
- "#{super}: " + @failed_objects.map{|object| "\"#{object.to_s}\": " + object.errors.full_messages.join(', ')}.join("\n")
24
- end
9
+ klass = Class.new(base.queue_class)
10
+ klass.send :include, Validation::Queue
11
+ base.queue_class = klass
25
12
  end
26
13
  end
27
14
  end
@@ -0,0 +1,204 @@
1
+ module SaveQueue
2
+ #
3
+ # Implementation of the _Observer_ object-oriented design pattern. The
4
+ # following documentation is copied, with modifications, from "Programming
5
+ # Ruby", by Hunt and Thomas; http://www.rubycentral.com/book/lib_patterns.html.
6
+ #
7
+ # See Observable for more info.
8
+
9
+ # The Observer pattern (also known as publish/subscribe) provides a simple
10
+ # mechanism for one object to inform a set of interested third-party objects
11
+ # when its state changes.
12
+ #
13
+ # == Mechanism
14
+ #
15
+ # The notifying class mixes in the +Observable+
16
+ # module, which provides the methods for managing the associated observer
17
+ # objects.
18
+ #
19
+ # The observers must implement a method called +update+ to receive
20
+ # notifications.
21
+ #
22
+ # The observable object must:
23
+ # * assert that it has +#changed+
24
+ # * call +#notify_observers+
25
+ #
26
+ # === Example
27
+ #
28
+ # The following example demonstrates this nicely. A +Ticker+, when run,
29
+ # continually receives the stock +Price+ for its <tt>@symbol</tt>. A +Warner+
30
+ # is a general observer of the price, and two warners are demonstrated, a
31
+ # +WarnLow+ and a +WarnHigh+, which print a warning if the price is below or
32
+ # above their set limits, respectively.
33
+ #
34
+ # The +update+ callback allows the warners to run without being explicitly
35
+ # called. The system is set up with the +Ticker+ and several observers, and the
36
+ # observers do their duty without the top-level code having to interfere.
37
+ #
38
+ # Note that the contract between publisher and subscriber (observable and
39
+ # observer) is not declared or enforced. The +Ticker+ publishes a time and a
40
+ # price, and the warners receive that. But if you don't ensure that your
41
+ # contracts are correct, nothing else can warn you.
42
+ #
43
+ # require "observer"
44
+ #
45
+ # class Ticker ### Periodically fetch a stock price.
46
+ # include Observable
47
+ #
48
+ # def initialize(symbol)
49
+ # @symbol = symbol
50
+ # end
51
+ #
52
+ # def run
53
+ # lastPrice = nil
54
+ # loop do
55
+ # price = Price.fetch(@symbol)
56
+ # print "Current price: #{price}\n"
57
+ # if price != lastPrice
58
+ # changed # notify observers
59
+ # lastPrice = price
60
+ # notify_observers(Time.now, price)
61
+ # end
62
+ # sleep 1
63
+ # end
64
+ # end
65
+ # end
66
+ #
67
+ # class Price ### A mock class to fetch a stock price (60 - 140).
68
+ # def Price.fetch(symbol)
69
+ # 60 + rand(80)
70
+ # end
71
+ # end
72
+ #
73
+ # class Warner ### An abstract observer of Ticker objects.
74
+ # def initialize(ticker, limit)
75
+ # @limit = limit
76
+ # ticker.add_observer(self)
77
+ # end
78
+ # end
79
+ #
80
+ # class WarnLow < Warner
81
+ # def update(time, price) # callback for observer
82
+ # if price < @limit
83
+ # print "--- #{time.to_s}: Price below #@limit: #{price}\n"
84
+ # end
85
+ # end
86
+ # end
87
+ #
88
+ # class WarnHigh < Warner
89
+ # def update(time, price) # callback for observer
90
+ # if price > @limit
91
+ # print "+++ #{time.to_s}: Price above #@limit: #{price}\n"
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ # ticker = Ticker.new("MSFT")
97
+ # WarnLow.new(ticker, 80)
98
+ # WarnHigh.new(ticker, 120)
99
+ # ticker.run
100
+ #
101
+ # Produces:
102
+ #
103
+ # Current price: 83
104
+ # Current price: 75
105
+ # --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
106
+ # Current price: 90
107
+ # Current price: 134
108
+ # +++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
109
+ # Current price: 134
110
+ # Current price: 112
111
+ # Current price: 79
112
+ # --- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79
113
+ module Observable
114
+
115
+ #
116
+ # Add +observer+ as an observer on this object. so that it will receive
117
+ # notifications.
118
+ #
119
+ # +observer+:: the object that will be notified of changes.
120
+ # +func+:: Symbol naming the method that will be called when this Observable
121
+ # has changes.
122
+ #
123
+ # This method must return true for +observer.respond_to?+ and will
124
+ # receive <tt>*arg</tt> when #notify_observers is called, where
125
+ # <tt>*arg</tt> is the value passed to #notify_observers by this
126
+ # Observable
127
+ def add_observer(observer, func=:update)
128
+ @observer_peers = {} unless defined? @observer_peers
129
+ unless observer.respond_to? func
130
+ raise NoMethodError, "observer does not respond to `#{func.to_s}'"
131
+ end
132
+ @observer_peers[observer] = func
133
+ end
134
+
135
+ #
136
+ # Remove +observer+ as an observer on this object so that it will no longer
137
+ # receive notifications.
138
+ #
139
+ # +observer+:: An observer of this Observable
140
+ def delete_observer(observer)
141
+ @observer_peers.delete observer if defined? @observer_peers
142
+ end
143
+
144
+ #
145
+ # Remove all observers associated with this object.
146
+ #
147
+ def delete_observers
148
+ @observer_peers.clear if defined? @observer_peers
149
+ end
150
+
151
+ #
152
+ # Return the number of observers associated with this object.
153
+ #
154
+ def count_observers
155
+ if defined? @observer_peers
156
+ @observer_peers.size
157
+ else
158
+ 0
159
+ end
160
+ end
161
+
162
+ #
163
+ # Set the changed state of this object. Notifications will be sent only if
164
+ # the changed +state+ is +true+.
165
+ #
166
+ # +state+:: Boolean indicating the changed state of this Observable.
167
+ #
168
+ def changed(state=true)
169
+ @observer_state = state
170
+ end
171
+
172
+ #
173
+ # Returns true if this object's state has been changed since the last
174
+ # #notify_observers call.
175
+ #
176
+ def changed?
177
+ if defined? @observer_state and @observer_state
178
+ true
179
+ else
180
+ false
181
+ end
182
+ end
183
+
184
+ #
185
+ # Notify observers of a change in state *if* this object's changed state is
186
+ # +true+.
187
+ #
188
+ # This will invoke the method named in #add_observer, pasing <tt>*arg</tt>.
189
+ # The changed state is then set to +false+.
190
+ #
191
+ # <tt>*arg</tt>:: Any arguments to pass to the observers.
192
+ def notify_observers(*arg)
193
+ if defined? @observer_state and @observer_state
194
+ if defined? @observer_peers
195
+ @observer_peers.each do |k, v|
196
+ k.send v, *arg
197
+ end
198
+ end
199
+ @observer_state = false
200
+ end
201
+ end
202
+
203
+ end
204
+ end
@@ -0,0 +1,38 @@
1
+ require 'forwardable'
2
+ module SaveQueue
3
+ class UniqQueue
4
+ extend ::Forwardable
5
+ DELEGATED_METHODS = [:empty?,
6
+ :any?,
7
+ :size,
8
+ :count,
9
+ :clear,
10
+ :inspect,
11
+ :to_s,
12
+ :first,
13
+ :last,
14
+ :pop,
15
+ :shift]
16
+
17
+ def_delegators :@queue, *DELEGATED_METHODS
18
+
19
+ def initialize
20
+ @queue = []
21
+ end
22
+
23
+ def add_all objects
24
+ Array(objects).each do |object|
25
+ add object
26
+ end
27
+ end
28
+
29
+ def add object
30
+ return false if @queue.include? object
31
+ @queue << object
32
+
33
+ true
34
+ end
35
+ alias_method :push, :add
36
+ alias_method :<<, :add
37
+ end
38
+ end
@@ -1,3 +1,3 @@
1
1
  module SaveQueue
2
- VERSION = "0.2.3"
2
+ VERSION = "0.3.0"
3
3
  end
data/lib/save_queue.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "save_queue/version"
2
2
  require "save_queue/object"
3
+ require "save_queue/exceptions"
3
4
 
4
5
  module SaveQueue
5
6
  def self.included base
data/save_queue.gemspec CHANGED
@@ -8,8 +8,9 @@ Gem::Specification.new do |s|
8
8
  s.authors = ["Alexander Paramonov"]
9
9
  s.email = ["alexander.n.paramonov@gmail.com"]
10
10
  s.homepage = "http://github.com/AlexParamonov/save_queue"
11
- s.summary = %q{Push related objects to a queue for delayed save}
12
- s.description = %q{Save Queue allows to push related objects to an object's queue for delayed save, that will triggered on object#save. In this case object wil store all related information on its save.}
11
+ s.summary = %q{Push related objects to a queue for a delayed save}
12
+ s.description = %q{Save Queue allows to push objects to other object's queue for a delayed save.
13
+ Queue save will be triggered by object#save.}
13
14
 
14
15
  s.rubyforge_project = "save_queue"
15
16
 
@@ -21,5 +22,5 @@ Gem::Specification.new do |s|
21
22
  # specify any dependencies here; for example:
22
23
  s.add_development_dependency "rspec", ">= 2.6"
23
24
  s.add_development_dependency "rake"
24
- s.add_runtime_dependency "activesupport"
25
+ s.add_runtime_dependency "hooks"
25
26
  end
@@ -0,0 +1,45 @@
1
+ require "spec_helper"
2
+ require "save_queue/plugins/notification"
3
+
4
+ describe SaveQueue::Plugins::Notification do
5
+ describe "#integration" do
6
+ it "should mix Queue to object's save_queue" do
7
+ klass = new_class
8
+ klass.send :include, SaveQueue::Plugins::Notification
9
+
10
+ klass.queue_class.should include SaveQueue::Plugins::Notification::Queue
11
+ end
12
+
13
+ it "should not change original SaveQueue::*Queue class" do
14
+ klass = new_class
15
+ old_queue = klass.queue_class
16
+ klass.queue_class = Class.new(old_queue)
17
+
18
+ klass.send :include, SaveQueue::Plugins::Notification
19
+ old_queue.should_not include SaveQueue::Plugins::Notification::Queue
20
+ end
21
+
22
+ it "should mix Object to object class" do
23
+ klass = new_class
24
+ klass.send :include, SaveQueue::Plugins::Notification
25
+
26
+ klass.should include SaveQueue::Plugins::Notification::Object
27
+ end
28
+ end
29
+
30
+ describe "workflow" do
31
+ let(:object) do
32
+ klass = new_class
33
+ klass.send :include, SaveQueue::Plugins::Notification
34
+ klass.new
35
+ end
36
+
37
+ [:add, :<<, :push].each do |method|
38
+ it "should mark object as changed if save_queue was changed by ##{method}" do
39
+ object.mark_as_saved
40
+ object.save_queue.send method, new_object
41
+ object.should have_unsaved_changes
42
+ end
43
+ end
44
+ end
45
+ end