save_queue 0.2.3 → 0.3.0

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.
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