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,239 @@
1
+ module CouchFoo
2
+ module Reflection # :nodoc:
3
+ def self.included(base)
4
+ base.extend(ClassMethods)
5
+ end
6
+
7
+ # Reflection allows you to interrogate Couch Foo classes and objects about their associations and aggregations.
8
+ # This information can, for example, be used in a form builder that took an Couch Foo object and created input
9
+ # fields for all of the attributes depending on their type and displayed the associations to other objects.
10
+ #
11
+ # You can find the interface for the AggregateReflection and AssociationReflection classes in the abstract MacroReflection class.
12
+ module ClassMethods
13
+ def create_reflection(macro, name, options, couch_foo)
14
+ case macro
15
+ when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
16
+ reflection = AssociationReflection.new(macro, name, options, couch_foo)
17
+ #when :composed_of
18
+ # reflection = AggregateReflection.new(macro, name, options, couch_foo)
19
+ end
20
+ write_inheritable_hash :reflections, name => reflection
21
+ reflection
22
+ end
23
+
24
+ # Returns a hash containing all AssociationReflection objects for the current class
25
+ # Example:
26
+ #
27
+ # Invoice.reflections
28
+ # Account.reflections
29
+ #
30
+ def reflections
31
+ read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {})
32
+ end
33
+
34
+ # Returns an array of AggregateReflection objects for all the aggregations in the class.
35
+ def reflect_on_all_aggregations
36
+ reflections.values.select { |reflection| reflection.is_a?(AggregateReflection) }
37
+ end
38
+
39
+ # Returns the AggregateReflection object for the named +aggregation+ (use the symbol). Example:
40
+ #
41
+ # Account.reflect_on_aggregation(:balance) # returns the balance AggregateReflection
42
+ #
43
+ def reflect_on_aggregation(aggregation)
44
+ reflections[aggregation].is_a?(AggregateReflection) ? reflections[aggregation] : nil
45
+ end
46
+
47
+ # Returns an array of AssociationReflection objects for all the associations in the class. If you only want to reflect on a
48
+ # certain association type, pass in the symbol (<tt>:has_many</tt>, <tt>:has_one</tt>, <tt>:belongs_to</tt>) for that as the first parameter.
49
+ # Example:
50
+ #
51
+ # Account.reflect_on_all_associations # returns an array of all associations
52
+ # Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
53
+ #
54
+ def reflect_on_all_associations(macro = nil)
55
+ association_reflections = reflections.values.select { |reflection| reflection.is_a?(AssociationReflection) }
56
+ macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
57
+ end
58
+
59
+ # Returns the AssociationReflection object for the named +association+ (use the symbol). Example:
60
+ #
61
+ # Account.reflect_on_association(:owner) # returns the owner AssociationReflection
62
+ # Invoice.reflect_on_association(:line_items).macro # returns :has_many
63
+ #
64
+ def reflect_on_association(association)
65
+ reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil
66
+ end
67
+ end
68
+
69
+
70
+ # Abstract base class for AggregateReflection and AssociationReflection that describes the interface available for both of
71
+ # those classes. Objects of AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
72
+ class MacroReflection
73
+ attr_reader :couch_foo
74
+
75
+ def initialize(macro, name, options, couch_foo)
76
+ @macro, @name, @options, @couch_foo = macro, name, options, couch_foo
77
+ end
78
+
79
+ # Returns the name of the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return
80
+ # <tt>:balance</tt> or for <tt>has_many :clients</tt> it will return <tt>:clients</tt>.
81
+ def name
82
+ @name
83
+ end
84
+
85
+ # Returns the macro type. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> will return <tt>:composed_of</tt>
86
+ # or for <tt>has_many :clients</tt> will return <tt>:has_many</tt>.
87
+ def macro
88
+ @macro
89
+ end
90
+
91
+ # Returns the hash of options used for the macro. For example, it would return <tt>{ :class_name => "Money" }</tt> for
92
+ # <tt>composed_of :balance, :class_name => 'Money'</tt> or +{}+ for <tt>has_many :clients</tt>.
93
+ def options
94
+ @options
95
+ end
96
+
97
+ # Returns the class for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money
98
+ # class and <tt>has_many :clients</tt> returns the Client class.
99
+ def klass
100
+ @klass ||= class_name.constantize
101
+ end
102
+
103
+ # Returns the class name for the macro. For example, <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
104
+ # and <tt>has_many :clients</tt> returns <tt>'Client'</tt>.
105
+ def class_name
106
+ @class_name ||= options[:class_name] || derive_class_name
107
+ end
108
+
109
+ def document_class_name
110
+ @document_class_name ||= klass.document_class_name
111
+ end
112
+
113
+ # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +couch_foo+ attribute,
114
+ # and +other_aggregation+ has an options hash assigned to it.
115
+ def ==(other_aggregation)
116
+ name == other_aggregation.name && other_aggregation.options && couch_foo == other_aggregation.couch_foo
117
+ end
118
+
119
+ def sanitized_conditions #:nodoc:
120
+ @sanitized_conditions ||= klass.send(:sanitize_conditions, options[:conditions]) if options[:conditions]
121
+ end
122
+
123
+ private
124
+ def derive_class_name
125
+ name.to_s.camelize
126
+ end
127
+ end
128
+
129
+
130
+ # Holds all the meta-data about an aggregation as it was specified in the Couch Foo class.
131
+ class AggregateReflection < MacroReflection #:nodoc:
132
+ end
133
+
134
+ # Holds all the meta-data about an association as it was specified in the Couch Foo class.
135
+ class AssociationReflection < MacroReflection #:nodoc:
136
+ def klass
137
+ @klass ||= couch_foo.send(:compute_type, class_name)
138
+ end
139
+
140
+ def primary_key_name
141
+ @primary_key_name ||= options[:foreign_key] || derive_primary_key_name
142
+ end
143
+
144
+ def association_foreign_key
145
+ @association_foreign_key ||= @options[:association_foreign_key] || class_name.foreign_key
146
+ end
147
+
148
+ def counter_cache_property
149
+ if options[:counter_cache] == true
150
+ "#{couch_foo.name.underscore.pluralize}_count"
151
+ elsif options[:counter_cache]
152
+ options[:counter_cache]
153
+ end
154
+ end
155
+
156
+ # Returns the AssociationReflection object specified in the <tt>:through</tt> option
157
+ # of a HasManyThrough or HasOneThrough association. Example:
158
+ #
159
+ # class Post < CouchFoo::Base
160
+ # has_many :taggings
161
+ # has_many :tags, :through => :taggings
162
+ # end
163
+ #
164
+ # tags_reflection = Post.reflect_on_association(:tags)
165
+ # taggings_reflection = tags_reflection.through_reflection
166
+ #
167
+ def through_reflection
168
+ @through_reflection ||= options[:through] ? couch_foo.reflect_on_association(options[:through]) : false
169
+ end
170
+
171
+ # Gets an array of possible <tt>:through</tt> source reflection names:
172
+ #
173
+ # [:singularized, :pluralized]
174
+ #
175
+ def source_reflection_names
176
+ @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
177
+ end
178
+
179
+ # Gets the source of the through reflection. It checks both a singularized and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
180
+ # (The <tt>:tags</tt> association on Tagging below.)
181
+ #
182
+ # class Post < CouchFoo::Base
183
+ # has_many :taggings
184
+ # has_many :tags, :through => :taggings
185
+ # end
186
+ #
187
+ def source_reflection
188
+ return nil unless through_reflection
189
+ @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
190
+ end
191
+
192
+ def check_validity!
193
+ if options[:through]
194
+ if through_reflection.nil?
195
+ raise HasManyThroughAssociationNotFoundError.new(couch_foo.name, self)
196
+ end
197
+
198
+ if source_reflection.nil?
199
+ raise HasManyThroughSourceAssociationNotFoundError.new(self)
200
+ end
201
+
202
+ if options[:source_type] && source_reflection.options[:polymorphic].nil?
203
+ raise HasManyThroughAssociationPointlessSourceTypeError.new(couch_foo.name, self, source_reflection)
204
+ end
205
+
206
+ if source_reflection.options[:polymorphic] && options[:source_type].nil?
207
+ raise HasManyThroughAssociationPolymorphicError.new(couch_foo.name, self, source_reflection)
208
+ end
209
+
210
+ unless [:belongs_to, :has_many].include?(source_reflection.macro) && source_reflection.options[:through].nil?
211
+ raise HasManyThroughSourceAssociationMacroError.new(self)
212
+ end
213
+ end
214
+ end
215
+
216
+ private
217
+ def derive_class_name
218
+ # get the class_name of the belongs_to association of the through reflection
219
+ if through_reflection
220
+ options[:source_type] || source_reflection.class_name
221
+ else
222
+ class_name = name.to_s.camelize
223
+ class_name = class_name.singularize if [ :has_many, :has_and_belongs_to_many ].include?(macro)
224
+ class_name
225
+ end
226
+ end
227
+
228
+ def derive_primary_key_name
229
+ if macro == :belongs_to
230
+ "#{name}_id"
231
+ elsif options[:as]
232
+ "#{options[:as]}_id"
233
+ else
234
+ couch_foo.name.foreign_key
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,41 @@
1
+ module CouchFoo
2
+ # Couch Foo automatically timestamps create and update operations if the document has attributes
3
+ # named created_at/created_on or updated_at/updated_on.
4
+ #
5
+ # Timestamping can be turned off by setting
6
+ # <tt>ActiveRecord::Base.record_timestamps = false</tt>
7
+ #
8
+ # Timestamps are in the local timezone by default but you can use UTC by setting
9
+ # <tt>ActiveRecord::Base.default_timezone = :utc</tt>
10
+ module Timestamp
11
+ def self.included(base) #:nodoc:
12
+ base.alias_method_chain :create, :timestamps
13
+ base.alias_method_chain :update, :timestamps
14
+
15
+ base.class_inheritable_accessor :record_timestamps, :instance_writer => false
16
+ base.record_timestamps = true
17
+ end
18
+
19
+ private
20
+ def create_with_timestamps() #:nodoc:
21
+ if record_timestamps
22
+ t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
23
+ write_attribute('created_at', t) if respond_to?(:created_at) && created_at.nil?
24
+ write_attribute('created_on', t) if respond_to?(:created_on) && created_on.nil?
25
+
26
+ write_attribute('updated_at', t) if respond_to?(:updated_at)
27
+ write_attribute('updated_on', t) if respond_to?(:updated_on)
28
+ end
29
+ create_without_timestamps()
30
+ end
31
+
32
+ def update_with_timestamps(*args) #:nodoc:
33
+ if record_timestamps && changed?
34
+ t = self.class.default_timezone == :utc ? Time.now.utc : Time.now
35
+ write_attribute('updated_at', t) if respond_to?(:updated_at)
36
+ write_attribute('updated_on', t) if respond_to?(:updated_on)
37
+ end
38
+ update_without_timestamps(*args)
39
+ end
40
+ end
41
+ end