georgepalmer-couch_foo 0.7.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.
@@ -0,0 +1,142 @@
1
+ module CouchFoo
2
+ # Track unsaved attribute changes.
3
+ #
4
+ # A newly instantiated object is unchanged:
5
+ # person = Person.find_by_name('uncle bob')
6
+ # person.changed? # => false
7
+ #
8
+ # Change the name:
9
+ # person.name = 'Bob'
10
+ # person.changed? # => true
11
+ # person.name_changed? # => true
12
+ # person.name_was # => 'uncle bob'
13
+ # person.name_change # => ['uncle bob', 'Bob']
14
+ # person.name = 'Bill'
15
+ # person.name_change # => ['uncle bob', 'Bill']
16
+ #
17
+ # Save the changes:
18
+ # person.save
19
+ # person.changed? # => false
20
+ # person.name_changed? # => false
21
+ #
22
+ # Assigning the same value leaves the attribute unchanged:
23
+ # person.name = 'Bill'
24
+ # person.name_changed? # => false
25
+ # person.name_change # => nil
26
+ #
27
+ # Which attributes have changed?
28
+ # person.name = 'bob'
29
+ # person.changed # => ['name']
30
+ # person.changes # => { 'name' => ['Bill', 'bob'] }
31
+ #
32
+ # Before modifying an attribute in-place:
33
+ # person.name_will_change!
34
+ # person.name << 'by'
35
+ # person.name_change # => ['uncle bob', 'uncle bobby']
36
+ module Dirty
37
+ def self.included(base)
38
+ base.attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
39
+ base.alias_method_chain :write_attribute, :dirty
40
+ base.alias_method_chain :save, :dirty
41
+ base.alias_method_chain :save!, :dirty
42
+ base.alias_method_chain :reload, :dirty
43
+ end
44
+
45
+ # Do any attributes have unsaved changes?
46
+ # person.changed? # => false
47
+ # person.name = 'bob'
48
+ # person.changed? # => true
49
+ def changed?
50
+ !changed_attributes.empty?
51
+ end
52
+
53
+ # List of attributes with unsaved changes.
54
+ # person.changed # => []
55
+ # person.name = 'bob'
56
+ # person.changed # => ['name']
57
+ def changed
58
+ changed_attributes.keys
59
+ end
60
+
61
+ # Map of changed attrs => [original value, new value]
62
+ # person.changes # => {}
63
+ # person.name = 'bob'
64
+ # person.changes # => { 'name' => ['bill', 'bob'] }
65
+ def changes
66
+ changed.inject({}) { |h, attr| h[attr] = attribute_change(attr); h }
67
+ end
68
+
69
+ # Attempts to +save+ the record and clears changed attributes if successful.
70
+ def save_with_dirty(*args) #:nodoc:
71
+ if status = save_without_dirty(*args)
72
+ changed_attributes.clear
73
+ end
74
+ status
75
+ end
76
+
77
+ # Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
78
+ def save_with_dirty!(*args) #:nodoc:
79
+ status = save_without_dirty!(*args)
80
+ changed_attributes.clear
81
+ status
82
+ end
83
+
84
+ # <tt>reload</tt> the record and clears changed attributes.
85
+ def reload_with_dirty(*args) #:nodoc:
86
+ record = reload_without_dirty(*args)
87
+ changed_attributes.clear
88
+ record
89
+ end
90
+
91
+ private
92
+ # Map of change attr => original value.
93
+ def changed_attributes
94
+ @changed_attributes ||= {}
95
+ end
96
+
97
+ # Handle *_changed? for method_missing.
98
+ def attribute_changed?(attr)
99
+ changed_attributes.include?(attr)
100
+ end
101
+
102
+ # Handle *_change for method_missing.
103
+ def attribute_change(attr)
104
+ [changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
105
+ end
106
+
107
+ # Handle *_was for method_missing.
108
+ def attribute_was(attr)
109
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
110
+ end
111
+
112
+ # Handle *_will_change! for method_missing.
113
+ def attribute_will_change!(attr)
114
+ changed_attributes[attr] = clone_attribute_value(:read_attribute, attr)
115
+ end
116
+
117
+ # Wrap write_attribute to remember original attribute value.
118
+ def write_attribute_with_dirty(attr, value)
119
+ attr = attr.to_s
120
+
121
+ # The attribute already has an unsaved change.
122
+ if changed_attributes.include?(attr)
123
+ old = changed_attributes[attr]
124
+ changed_attributes.delete(attr) unless field_changed?(attr, old, value)
125
+ else
126
+ old = clone_attribute_value(:read_attribute, attr)
127
+ changed_attributes[attr] = old if field_changed?(attr, old, value)
128
+ end
129
+
130
+ # Carry on.
131
+ write_attribute_without_dirty(attr, value)
132
+ end
133
+
134
+ def field_changed?(attr, old, value)
135
+ if type = type_for_property(attr.to_sym)
136
+ value = convert_to_type(value, type)
137
+ end
138
+
139
+ old != value
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,168 @@
1
+ module CouchFoo
2
+ module NamedScope
3
+ # All subclasses of CouchFoo::Base have two named_scopes:
4
+ # * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
5
+ # * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly: <tt>Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)</tt>
6
+ #
7
+ # These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
8
+ # intermediate values (scopes) around as first-class objects is convenient.
9
+ def self.included(base)
10
+ base.class_eval do
11
+ extend ClassMethods
12
+ named_scope :scoped, lambda { |scope| scope }
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def scopes
18
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
19
+ end
20
+
21
+ # Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
22
+ # such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
23
+ #
24
+ # class Shirt < CouchFoo::Base
25
+ # named_scope :red, :conditions => {:color => 'red'}
26
+ # named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
27
+ # end
28
+ #
29
+ # The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
30
+ # in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
31
+ #
32
+ # Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
33
+ # constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
34
+ # <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
35
+ # as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
36
+ # <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
37
+ #
38
+ # These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
39
+ # Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
40
+ # for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
41
+ #
42
+ # All scopes are available as class methods on the CouchFoo::Base descendent upon which the scopes were defined. But they are also available to
43
+ # <tt>has_many</tt> associations. If,
44
+ #
45
+ # class Person < CouchFoo::Base
46
+ # has_many :shirts
47
+ # end
48
+ #
49
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
50
+ # only shirts.
51
+ #
52
+ # Named scopes can also be procedural.
53
+ #
54
+ # class Shirt < CouchFoo::Base
55
+ # named_scope :colored, lambda { |color|
56
+ # { :conditions => { :color => color } }
57
+ # }
58
+ # end
59
+ #
60
+ # In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
61
+ #
62
+ # Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
63
+ #
64
+ # class Shirt < CouchFoo::Base
65
+ # named_scope :red, :conditions => {:color => 'red'} do
66
+ # def dom_id
67
+ # 'red_shirts'
68
+ # end
69
+ # end
70
+ # end
71
+ #
72
+ #
73
+ # For testing complex named scopes, you can examine the scoping options using the
74
+ # <tt>proxy_options</tt> method on the proxy itself.
75
+ #
76
+ # class Shirt < CouchFoo::Base
77
+ # named_scope :colored, lambda { |color|
78
+ # { :conditions => { :color => color } }
79
+ # }
80
+ # end
81
+ #
82
+ # expected_options = { :conditions => { :colored => 'red' } }
83
+ # assert_equal expected_options, Shirt.colored('red').proxy_options
84
+ def named_scope(name, options = {}, &block)
85
+ name = name.to_sym
86
+ scopes[name] = lambda do |parent_scope, *args|
87
+ Scope.new(parent_scope, case options
88
+ when Hash
89
+ options
90
+ when Proc
91
+ options.call(*args)
92
+ end, &block)
93
+ end
94
+ (class << self; self end).instance_eval do
95
+ define_method name do |*args|
96
+ scopes[name].call(self, *args)
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ class Scope
103
+ attr_reader :proxy_scope, :proxy_options
104
+
105
+ [].methods.each do |m|
106
+ unless m =~ /(^__|^nil\?|^send|^object_id$|class|extend|^find$|count|sum|average|maximum|minimum|paginate|first|last|empty\?|respond_to\?)/
107
+ delegate m, :to => :proxy_found
108
+ end
109
+ end
110
+
111
+ delegate :scopes, :with_scope, :to => :proxy_scope
112
+
113
+ def initialize(proxy_scope, options, &block)
114
+ [options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
115
+ extend Module.new(&block) if block_given?
116
+ @proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
117
+ end
118
+
119
+ def reload
120
+ load_found; self
121
+ end
122
+
123
+ def first(*args)
124
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
125
+ proxy_found.first(*args)
126
+ else
127
+ find(:first, *args)
128
+ end
129
+ end
130
+
131
+ def last(*args)
132
+ if args.first.kind_of?(Integer) || (@found && !args.first.kind_of?(Hash))
133
+ proxy_found.last(*args)
134
+ else
135
+ find(:last, *args)
136
+ end
137
+ end
138
+
139
+ def empty?
140
+ @found ? @found.empty? : count.zero?
141
+ end
142
+
143
+ def respond_to?(method, include_private = false)
144
+ super || @proxy_scope.respond_to?(method, include_private)
145
+ end
146
+
147
+ protected
148
+ def proxy_found
149
+ @found || load_found
150
+ end
151
+
152
+ private
153
+ def method_missing(method, *args, &block)
154
+ if scopes.include?(method)
155
+ scopes[method].call(self, *args)
156
+ else
157
+ with_scope :find => proxy_options do
158
+ proxy_scope.send(method, *args, &block)
159
+ end
160
+ end
161
+ end
162
+
163
+ def load_found
164
+ @found = find(:all)
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,195 @@
1
+ require 'singleton'
2
+ require 'set'
3
+
4
+ module CouchFoo
5
+ module Observing # :nodoc:
6
+ def self.included(base)
7
+ base.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ # Activates the observers assigned. Examples:
12
+ #
13
+ # # Calls PersonObserver.instance
14
+ # CouchFoo::Base.observers = :person_observer
15
+ #
16
+ # # Calls Cacher.instance and GarbageCollector.instance
17
+ # CouchFoo::Base.observers = :cacher, :garbage_collector
18
+ #
19
+ # # Same as above, just using explicit class references
20
+ # CouchFoo::Base.observers = Cacher, GarbageCollector
21
+ #
22
+ # Note: Setting this does not instantiate the observers yet. +instantiate_observers+ is
23
+ # called during startup, and before each development request.
24
+ def observers=(*observers)
25
+ @observers = observers.flatten
26
+ end
27
+
28
+ # Gets the current observers.
29
+ def observers
30
+ @observers ||= []
31
+ end
32
+
33
+ # Instantiate the global Couch Foo observers.
34
+ def instantiate_observers
35
+ return if @observers.blank?
36
+ @observers.each do |observer|
37
+ if observer.respond_to?(:to_sym) # Symbol or String
38
+ observer.to_s.camelize.constantize.instance
39
+ elsif observer.respond_to?(:instance)
40
+ observer.instance
41
+ else
42
+ raise ArgumentError, "#{observer} must be a lowercase, underscored class name (or an instance of the class itself) responding to the instance method. Example: Person.observers = :big_brother # calls BigBrother.instance"
43
+ end
44
+ end
45
+ end
46
+
47
+ protected
48
+ # Notify observers when the observed class is subclassed.
49
+ def inherited(subclass)
50
+ super
51
+ changed
52
+ notify_observers :observed_class_inherited, subclass
53
+ end
54
+ end
55
+ end
56
+
57
+ # Observer classes respond to lifecycle callbacks to implement trigger-like
58
+ # behavior outside the original class. This is a great way to reduce the
59
+ # clutter that normally comes when the model class is burdened with
60
+ # functionality that doesn't pertain to the core responsibility of the
61
+ # class. Example:
62
+ #
63
+ # class CommentObserver < CouchFoo::Observer
64
+ # def after_save(comment)
65
+ # Notifications.deliver_comment("admin@do.com", "New comment was posted", comment)
66
+ # end
67
+ # end
68
+ #
69
+ # This Observer sends an email when a Comment#save is finished.
70
+ #
71
+ # class ContactObserver < CouchFoo::Observer
72
+ # def after_create(contact)
73
+ # contact.logger.info('New contact added!')
74
+ # end
75
+ #
76
+ # def after_destroy(contact)
77
+ # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
78
+ # end
79
+ # end
80
+ #
81
+ # This Observer uses logger to log when specific callbacks are triggered.
82
+ #
83
+ # == Observing a class that can't be inferred
84
+ #
85
+ # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
86
+ # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name
87
+ # your observer differently than the class you're interested in observing, you can use the Observer.observe
88
+ # class method which takes either the concrete class (Product) or a symbol for that class (:product):
89
+ #
90
+ # class AuditObserver < CouchFoo::Observer
91
+ # observe :account
92
+ #
93
+ # def after_update(account)
94
+ # AuditTrail.new(account, "UPDATED")
95
+ # end
96
+ # end
97
+ #
98
+ # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
99
+ #
100
+ # class AuditObserver < CouchFoo::Observer
101
+ # observe :account, :balance
102
+ #
103
+ # def after_update(record)
104
+ # AuditTrail.new(record, "UPDATED")
105
+ # end
106
+ # end
107
+ #
108
+ # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
109
+ #
110
+ # == Available callback methods
111
+ #
112
+ # The observer can implement callback methods for each of the methods described in the Callbacks module.
113
+ #
114
+ # == Storing Observers in Rails
115
+ #
116
+ # If you're using Couch Foo within Rails, observer classes are usually stored in app/models with the
117
+ # naming convention of app/models/audit_observer.rb.
118
+ #
119
+ # == Configuration
120
+ # In order to activate an observer, list it in the <tt>config.couch_foo.observers</tt> configuration setting
121
+ # in your <tt>config/environment.rb</tt> file.
122
+ #
123
+ # config.couch_foo.observers = :comment_observer, :signup_observer
124
+ #
125
+ # Observers will not be invoked unless you define these in your application configuration.
126
+ #
127
+ # == Loading
128
+ #
129
+ # Observers register themselves in the model class they observe, since it is the class that
130
+ # notifies them of events when they occur. As a side-effect, when an observer is loaded its
131
+ # corresponding model class is loaded.
132
+ #
133
+ # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
134
+ # application initializers. Now observers are loaded after application initializers,
135
+ # so observed models can make use of extensions.
136
+ #
137
+ # If by any chance you are using observed models in the initialization you can still
138
+ # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
139
+ # singletons and that call instantiates and registers them.
140
+ class Observer
141
+ include Singleton
142
+
143
+ class << self
144
+ # Attaches the observer to the supplied model classes.
145
+ def observe(*models)
146
+ models.flatten!
147
+ models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
148
+ define_method(:observed_classes) { Set.new(models) }
149
+ end
150
+
151
+ # The class observed by default is inferred from the observer's class name:
152
+ # assert_equal Person, PersonObserver.observed_class
153
+ def observed_class
154
+ if observed_class_name = name[/(.*)Observer/, 1]
155
+ observed_class_name.constantize
156
+ else
157
+ nil
158
+ end
159
+ end
160
+ end
161
+
162
+ # Start observing the declared classes and their subclasses.
163
+ def initialize
164
+ Set.new(observed_classes + observed_subclasses).each { |klass| add_observer! klass }
165
+ end
166
+
167
+ # Send observed_method(object) if the method exists.
168
+ def update(observed_method, object) #:nodoc:
169
+ send(observed_method, object) if respond_to?(observed_method)
170
+ end
171
+
172
+ # Special method sent by the observed class when it is inherited.
173
+ # Passes the new subclass.
174
+ def observed_class_inherited(subclass) #:nodoc:
175
+ self.class.observe(observed_classes + [subclass])
176
+ add_observer!(subclass)
177
+ end
178
+
179
+ protected
180
+ def observed_classes
181
+ Set.new([self.class.observed_class].compact.flatten)
182
+ end
183
+
184
+ def observed_subclasses
185
+ observed_classes.sum([]) { |klass| klass.send(:subclasses) }
186
+ end
187
+
188
+ def add_observer!(klass)
189
+ klass.add_observer(self)
190
+ if respond_to?(:after_find) && !klass.method_defined?(:after_find)
191
+ klass.class_eval 'def after_find() end'
192
+ end
193
+ end
194
+ end
195
+ end