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.
- data/README.rdoc +113 -0
- data/VERSION.yml +4 -0
- data/lib/boolean.rb +3 -0
- data/lib/couch_foo/associations/association_collection.rb +346 -0
- data/lib/couch_foo/associations/association_proxy.rb +204 -0
- data/lib/couch_foo/associations/belongs_to_association.rb +57 -0
- data/lib/couch_foo/associations/belongs_to_polymorphic_association.rb +48 -0
- data/lib/couch_foo/associations/has_and_belongs_to_many_association.rb +111 -0
- data/lib/couch_foo/associations/has_many_association.rb +97 -0
- data/lib/couch_foo/associations/has_one_association.rb +95 -0
- data/lib/couch_foo/associations.rb +1118 -0
- data/lib/couch_foo/attribute_methods.rb +316 -0
- data/lib/couch_foo/base.rb +2117 -0
- data/lib/couch_foo/calculations.rb +117 -0
- data/lib/couch_foo/callbacks.rb +311 -0
- data/lib/couch_foo/database.rb +157 -0
- data/lib/couch_foo/dirty.rb +142 -0
- data/lib/couch_foo/named_scope.rb +168 -0
- data/lib/couch_foo/observer.rb +195 -0
- data/lib/couch_foo/reflection.rb +239 -0
- data/lib/couch_foo/timestamp.rb +41 -0
- data/lib/couch_foo/validations.rb +927 -0
- data/lib/couch_foo/view_methods.rb +234 -0
- data/lib/couch_foo.rb +43 -0
- data/test/couch_foo_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +116 -0
data/README.rdoc
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
= CouchFoo
|
2
|
+
|
3
|
+
== Overview
|
4
|
+
|
5
|
+
CouchFoo provides an ActiveRecord styled interface to CouchDB. The external API is nearly identical
|
6
|
+
to ActiveRecord so it should be possible to migrate your applications quite easily. That said, there
|
7
|
+
are a few minor differences to the way CouchDB works. In particular:
|
8
|
+
* CouchDB is schema free so property defintions for the document are defined in the model (like DataMapper)
|
9
|
+
* :select, :joins, :having, :group, :from and :lock are not available on find or associations as they don't apply (locking is handled as conflict resolution at insertion time)
|
10
|
+
* :conditions can only accept a hash and not an array or SQL. For example :conditions => {:user_name => "Georgio_1999"}
|
11
|
+
* :offset is less efficient in CouchDB - there's more on this in the rdoc
|
12
|
+
* :order is applied after results are retrieved from the database. Therefore :order cannot be used with :limit without a new option :use_key. This is explained fully in the quick start guide and CouchFoo#find documentation
|
13
|
+
* :include isn't implemented yet but the finders and associations still accept the option so you won't need to make any code changes
|
14
|
+
* By default results are ordered by document key. The key uses a UUID scheme so these don't auto-increment and are likely to come out in a different order to insertion. default_sort can be used on a model to sort by create date by default and overcome this
|
15
|
+
* validates_uniqueness_of has had the :case_sensitive option removed
|
16
|
+
* Because there's no SQL there's no SQL finder methods
|
17
|
+
* Timezones, aggregations and fixtures are not yet implemented
|
18
|
+
* The price of index updating is paid when next accessing the index rather than the point of insertion. This can be more efficient or less depending on your application. It may make sense to use an external process to do the updating for you - see CouchFoo#find for more on this
|
19
|
+
* On that note, occasional compacting of CouchDB is required to recover space from old versions of documents. This can be kicked off in several ways (see quick start guide)
|
20
|
+
|
21
|
+
It is recommend that you read the quick start and performance sections in the rdoc for a full overview of differences and points to be aware of when developing.
|
22
|
+
|
23
|
+
|
24
|
+
== Getting started
|
25
|
+
|
26
|
+
CouchFoo::Base.set_database("http://localhost:5984/opnli_dev", <yourcouchdbversion>)
|
27
|
+
|
28
|
+
If using with Rails you will need to create an initializer to do this (until proper integration is added)
|
29
|
+
|
30
|
+
|
31
|
+
== Examples of usage
|
32
|
+
|
33
|
+
Basic operations are the same as ActiveRecord:
|
34
|
+
class Address < CouchFoo::Base
|
35
|
+
property :number, Integer
|
36
|
+
property :street, String
|
37
|
+
property :postcode # Any generic type is fine as long as .to_json can be called on it
|
38
|
+
end
|
39
|
+
|
40
|
+
address1 = Address.create(:number => 3, :street => "My Street", :postcode => "secret") # Create address
|
41
|
+
address2 = Address.create(:number => 27, :street => "Another Street", :postcode => "secret")
|
42
|
+
Address.find.all # = [address1, address2] or maybe [address2, address2] depending on key generation
|
43
|
+
Address.first # = address1 or address2 depending on keys so probably isn't as expected
|
44
|
+
Address.find_by_street("My Street") # = address1
|
45
|
+
|
46
|
+
As key generation is through a UUID scheme, the order can't be predicted. However you can order the
|
47
|
+
results by default:
|
48
|
+
class Address < CouchFoo::Base
|
49
|
+
property :number, Integer
|
50
|
+
property :street, String
|
51
|
+
property :postcode # Any generic type is fine as long as .to_json can be called on it
|
52
|
+
property :created_at, DateTime
|
53
|
+
|
54
|
+
default_sort :created_at
|
55
|
+
end
|
56
|
+
|
57
|
+
Address.find.all # = [address1, address2]
|
58
|
+
Address.first # = address1 or address2, sorting is applied after results
|
59
|
+
Address.first(:use_key => :created_at) # = address1 but at the price of creating a new index
|
60
|
+
|
61
|
+
Conditions work slightly differently:
|
62
|
+
Address.find(:all, :conditions {:street => "My Street"}) # = address1, creates index on :street
|
63
|
+
Address.find(:all, :conditions {:created_at => "sometime"}) # Uses same index as :use_key => :created_at
|
64
|
+
Address.find(:all, :use_key => :street, :startkey => 'p') # All streets from p in alphabet, reuses the index created 2 lines up
|
65
|
+
|
66
|
+
As well as providing support for people using relational databases, CouchFoo attempts to provide a library for those wanting to use CouchDB as a document-orientated database:
|
67
|
+
class Document < CouchFoo::Base
|
68
|
+
property :number, Integer
|
69
|
+
property :street, String
|
70
|
+
|
71
|
+
view :number_ordered, "function(doc) {emit([doc.number , doc.street], doc); }", nil, :descending => true
|
72
|
+
end
|
73
|
+
|
74
|
+
Document.number_ordered(:limit => 75) # Will get the last 75 documents in the database ordered by number, street attributes
|
75
|
+
|
76
|
+
Associations work as expected but you must to remember to add the properties required for an association (we'll make this automatic soon):
|
77
|
+
class House < CouchFoo::Base
|
78
|
+
has_many :windows
|
79
|
+
end
|
80
|
+
|
81
|
+
class Window < CouchFoo::Base
|
82
|
+
property :house_id, String
|
83
|
+
belongs_to :house
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
== Credits
|
88
|
+
|
89
|
+
This gem was inspired some excellent work on CouchPotato, CouchREST, ActiveCouch and RelaxDB gems. Each offered
|
90
|
+
its own benefits and own challenges. After hacking with each I couldn't get a library was happy with. So I
|
91
|
+
started with ActiveRecord and modified it to work with CouchDB. Some areas required more work than others but
|
92
|
+
a lot of features were achieved for free once the base level of functionality had been achieved. Credit to DHH,
|
93
|
+
the rails core guys and the CouchDB gems that inspired this work.
|
94
|
+
|
95
|
+
|
96
|
+
== What's left to do?
|
97
|
+
|
98
|
+
Please feel free to fork and hit me with a request to merge back in. At the moment, the following areas need addressing:
|
99
|
+
|
100
|
+
* Proper integration with rails using a couchdb.yml config file
|
101
|
+
* Example ruby script for updating indexes as an external process, and the same for database compacting
|
102
|
+
* :include for database queries
|
103
|
+
* Remove dependency from CouchRest? Add support for attachments and log calls to server so know performance times. General tidy up of database.rb
|
104
|
+
* inline and HABTM inline associations. Also x_id and x_type properties should be automatically defined
|
105
|
+
* compatability with willpaginate and friends
|
106
|
+
* cleanup of associations, reflection, validations classes (were modified in a rush)
|
107
|
+
* has_many :through
|
108
|
+
* Rest of calculations, timezones, aggregations and fixtures
|
109
|
+
* Diff with AR to check got everything in
|
110
|
+
* DataMapper interface
|
111
|
+
* Tool to migrate DB from MySQL to CouchDB
|
112
|
+
* some kind of generic admin interface where can just browse around data structure?
|
113
|
+
* grep of code and doc for records and columns, should be documents and attributes
|
data/VERSION.yml
ADDED
data/lib/boolean.rb
ADDED
@@ -0,0 +1,346 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module CouchFoo
|
4
|
+
module Associations
|
5
|
+
class AssociationCollection < AssociationProxy #:nodoc:
|
6
|
+
def initialize(owner, reflection)
|
7
|
+
super
|
8
|
+
construct_conditions
|
9
|
+
end
|
10
|
+
|
11
|
+
def find(*args)
|
12
|
+
options = args.extract_options!
|
13
|
+
|
14
|
+
options[:conditions] = @association_conditions.merge(options[:conditions] || {})
|
15
|
+
|
16
|
+
# Multiple ordering not supported at the minute
|
17
|
+
#if options[:order] && @reflection.options[:order]
|
18
|
+
# options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
|
19
|
+
#elsif @reflection.options[:order]
|
20
|
+
if @reflection.options[:order]
|
21
|
+
options[:order] = @reflection.options[:order]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Build options specific to association
|
25
|
+
construct_find_options!(options)
|
26
|
+
|
27
|
+
merge_options_from_reflection!(options)
|
28
|
+
|
29
|
+
# Pass through args exactly as we received them.
|
30
|
+
args << options
|
31
|
+
|
32
|
+
@reflection.klass.find(*args)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Fetches the first element directly if it can
|
36
|
+
def first(*args)
|
37
|
+
if fetch_first_or_last_using_find? args
|
38
|
+
find(:first, *args)
|
39
|
+
else
|
40
|
+
load_target unless loaded?
|
41
|
+
@target.first(*args)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetches the last element directly if it can
|
46
|
+
def last(*args)
|
47
|
+
if fetch_first_or_last_using_find? args
|
48
|
+
find(:last, *args)
|
49
|
+
else
|
50
|
+
load_target unless loaded?
|
51
|
+
@target.last(*args)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_ary
|
56
|
+
load_target
|
57
|
+
@target.to_ary
|
58
|
+
end
|
59
|
+
|
60
|
+
def reset
|
61
|
+
reset_target!
|
62
|
+
@loaded = false
|
63
|
+
end
|
64
|
+
|
65
|
+
def build(attributes = {}, &block)
|
66
|
+
if attributes.is_a?(Array)
|
67
|
+
attributes.collect { |attr| build(attr, &block) }
|
68
|
+
else
|
69
|
+
build_record(attributes) do |record|
|
70
|
+
block.call(record) if block_given?
|
71
|
+
set_belongs_to_association_for(record)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Add +records+ to this association. Returns +self+ so method calls may be chained.
|
77
|
+
# Since << flattens its argument list and inserts each record, +push+ and +concat+ behave identically.
|
78
|
+
def <<(*records)
|
79
|
+
result = true
|
80
|
+
load_target if @owner.new_record?
|
81
|
+
|
82
|
+
@owner.transaction do
|
83
|
+
flatten_deeper(records).each do |record|
|
84
|
+
raise_on_type_mismatch(record)
|
85
|
+
add_record_to_target_with_callbacks(record) do |r|
|
86
|
+
result &&= insert_record(record) unless @owner.new_record?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
result && self
|
92
|
+
end
|
93
|
+
|
94
|
+
alias_method :push, :<<
|
95
|
+
alias_method :concat, :<<
|
96
|
+
|
97
|
+
# Remove all records from this association
|
98
|
+
def delete_all
|
99
|
+
load_target
|
100
|
+
delete(@target)
|
101
|
+
reset_target!
|
102
|
+
end
|
103
|
+
|
104
|
+
# Calculate sum
|
105
|
+
def sum(*args)
|
106
|
+
if block_given?
|
107
|
+
calculate(:sum, *args) { |*block_args| yield(*block_args) }
|
108
|
+
else
|
109
|
+
calculate(:sum, *args)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Remove +records+ from this association. Does not destroy +records+.
|
114
|
+
def delete(*records)
|
115
|
+
records = flatten_deeper(records)
|
116
|
+
records.each { |record| raise_on_type_mismatch(record) }
|
117
|
+
|
118
|
+
@owner.transaction do
|
119
|
+
records.each { |record| callback(:before_remove, record) }
|
120
|
+
|
121
|
+
old_records = records.reject {|r| r.new_record? }
|
122
|
+
delete_records(old_records) if old_records.any?
|
123
|
+
|
124
|
+
records.each do |record|
|
125
|
+
@target.delete(record)
|
126
|
+
callback(:after_remove, record)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Removes all records from this association. Returns +self+ so method calls may be chained.
|
132
|
+
def clear
|
133
|
+
return self if length.zero? # forces load_target if it hasn't happened already
|
134
|
+
|
135
|
+
if @reflection.options[:dependent] && @reflection.options[:dependent] == :destroy
|
136
|
+
destroy_all
|
137
|
+
else
|
138
|
+
delete_all
|
139
|
+
end
|
140
|
+
|
141
|
+
self
|
142
|
+
end
|
143
|
+
|
144
|
+
def destroy_all
|
145
|
+
@owner.transaction do
|
146
|
+
each { |record| record.destroy }
|
147
|
+
end
|
148
|
+
|
149
|
+
reset_target!
|
150
|
+
end
|
151
|
+
|
152
|
+
def create(attrs = {})
|
153
|
+
if attrs.is_a?(Array)
|
154
|
+
attrs.collect { |attr| create(attr) }
|
155
|
+
else
|
156
|
+
create_record(attrs) do |record|
|
157
|
+
yield(record) if block_given?
|
158
|
+
record.save
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def create!(attrs = {})
|
164
|
+
create_record(attrs) do |record|
|
165
|
+
yield(record) if block_given?
|
166
|
+
record.save!
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns the size of the collection by executing a count query if the collection hasn't been
|
171
|
+
# loaded and calling collection.size if it has. If it's more likely than not that the
|
172
|
+
# collection does have a size larger than zero and you need to fetch that collection afterwards,
|
173
|
+
# it'll take one database query if you use length.
|
174
|
+
def size
|
175
|
+
if @owner.new_record? || (loaded? && !@reflection.options[:uniq])
|
176
|
+
@target.size
|
177
|
+
elsif !loaded? && !@reflection.options[:uniq] && @target.is_a?(Array)
|
178
|
+
unsaved_records = @target.select { |r| r.new_record? }
|
179
|
+
unsaved_records.size + count_records
|
180
|
+
else
|
181
|
+
count_records
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the size of the collection by loading it and calling size on the array. If you want
|
186
|
+
# to use this method to check whether the collection is empty, use collection.length.zero?
|
187
|
+
# instead of collection.empty?
|
188
|
+
def length
|
189
|
+
load_target.size
|
190
|
+
end
|
191
|
+
|
192
|
+
def empty?
|
193
|
+
size.zero?
|
194
|
+
end
|
195
|
+
|
196
|
+
def any?
|
197
|
+
if block_given?
|
198
|
+
method_missing(:any?) { |*block_args| yield(*block_args) }
|
199
|
+
else
|
200
|
+
!empty?
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
def uniq(collection = self)
|
205
|
+
seen = Set.new
|
206
|
+
collection.inject([]) do |kept, record|
|
207
|
+
unless seen.include?(record.id)
|
208
|
+
kept << record
|
209
|
+
seen << record.id
|
210
|
+
end
|
211
|
+
kept
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Replace this collection with +other_array+
|
216
|
+
# This will perform a diff and delete/add only records that have changed.
|
217
|
+
def replace(other_array)
|
218
|
+
other_array.each { |val| raise_on_type_mismatch(val) }
|
219
|
+
|
220
|
+
load_target
|
221
|
+
other = other_array.size < 100 ? other_array : other_array.to_set
|
222
|
+
current = @target.size < 100 ? @target : @target.to_set
|
223
|
+
|
224
|
+
@owner.transaction do
|
225
|
+
delete(@target.select { |v| !other.include?(v) })
|
226
|
+
concat(other_array.select { |v| !current.include?(v) })
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def include?(record)
|
231
|
+
return false unless record.is_a?(@reflection.klass)
|
232
|
+
load_target if !loaded?
|
233
|
+
return @target.include?(record) if loaded?
|
234
|
+
exists?(record)
|
235
|
+
end
|
236
|
+
|
237
|
+
protected
|
238
|
+
def construct_find_options!(options)
|
239
|
+
end
|
240
|
+
|
241
|
+
def load_target
|
242
|
+
if !@owner.new_record? || foreign_key_present
|
243
|
+
begin
|
244
|
+
if !loaded?
|
245
|
+
if @target.is_a?(Array) && @target.any?
|
246
|
+
@target = find_target + @target.find_all {|t| t.new_record? }
|
247
|
+
else
|
248
|
+
@target = find_target
|
249
|
+
end
|
250
|
+
end
|
251
|
+
rescue CouchFoo::DocumentNotFound
|
252
|
+
reset
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
loaded if target
|
257
|
+
target
|
258
|
+
end
|
259
|
+
|
260
|
+
def method_missing(method, *args)
|
261
|
+
if @target.respond_to?(method) || (!@reflection.klass.respond_to?(method) && Class.respond_to?(method))
|
262
|
+
if block_given?
|
263
|
+
super { |*block_args| yield(*block_args) }
|
264
|
+
else
|
265
|
+
super
|
266
|
+
end
|
267
|
+
elsif @reflection.klass.scopes.include?(method)
|
268
|
+
@reflection.klass.scopes[method].call(self, *args)
|
269
|
+
else
|
270
|
+
with_scope(construct_scope) do
|
271
|
+
if block_given?
|
272
|
+
@reflection.klass.send(method, *args) { |*block_args| yield(*block_args) }
|
273
|
+
else
|
274
|
+
@reflection.klass.send(method, *args)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# overloaded in derived Association classes to provide useful scoping depending on association type.
|
281
|
+
def construct_scope
|
282
|
+
{}
|
283
|
+
end
|
284
|
+
|
285
|
+
def reset_target!
|
286
|
+
@target = Array.new
|
287
|
+
end
|
288
|
+
|
289
|
+
def find_target
|
290
|
+
@reflection.options[:uniq] ? uniq(records) : find(:all)
|
291
|
+
end
|
292
|
+
|
293
|
+
private
|
294
|
+
def create_record(attrs)
|
295
|
+
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
296
|
+
ensure_owner_is_not_new
|
297
|
+
record = @reflection.klass.send(:with_scope, :create => construct_scope[:create]) { @reflection.klass.new(attrs) }
|
298
|
+
if block_given?
|
299
|
+
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
300
|
+
else
|
301
|
+
add_record_to_target_with_callbacks(record)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def build_record(attrs)
|
306
|
+
attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
|
307
|
+
record = @reflection.klass.new(attrs)
|
308
|
+
if block_given?
|
309
|
+
add_record_to_target_with_callbacks(record) { |*block_args| yield(*block_args) }
|
310
|
+
else
|
311
|
+
add_record_to_target_with_callbacks(record)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def add_record_to_target_with_callbacks(record)
|
316
|
+
callback(:before_add, record)
|
317
|
+
yield(record) if block_given?
|
318
|
+
@target ||= [] unless loaded?
|
319
|
+
@target << record unless @reflection.options[:uniq] && @target.include?(record)
|
320
|
+
callback(:after_add, record)
|
321
|
+
record
|
322
|
+
end
|
323
|
+
|
324
|
+
def callback(method, record)
|
325
|
+
callbacks_for(method).each do |callback|
|
326
|
+
ActiveSupport::Callbacks::Callback.new(method, callback, record).call(@owner, record)
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def callbacks_for(callback_name)
|
331
|
+
full_callback_name = "#{callback_name}_for_#{@reflection.name}"
|
332
|
+
@owner.class.read_inheritable_attribute(full_callback_name.to_sym) || []
|
333
|
+
end
|
334
|
+
|
335
|
+
def ensure_owner_is_not_new
|
336
|
+
if @owner.new_record?
|
337
|
+
raise CouchFoo::DocumentNotSaved, "You cannot call create unless the parent is saved"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
def fetch_first_or_last_using_find?(args)
|
342
|
+
args.first.kind_of?(Hash) || !(loaded? || @owner.new_record? || !@target.blank? || args.first.kind_of?(Integer))
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
module CouchFoo
|
2
|
+
module Associations
|
3
|
+
# This is the root class of all association proxies:
|
4
|
+
#
|
5
|
+
# AssociationProxy
|
6
|
+
# BelongsToAssociation
|
7
|
+
# HasOneAssociation
|
8
|
+
# BelongsToPolymorphicAssociation
|
9
|
+
# AssociationCollection
|
10
|
+
# HasAndBelongsToManyAssociation
|
11
|
+
# HasManyAssociation
|
12
|
+
#
|
13
|
+
# At there moment there are no Through associations
|
14
|
+
#
|
15
|
+
# Association proxies in Couch Foo are middlemen between the object that
|
16
|
+
# holds the association, known as the <tt>@owner</tt>, and the actual associated
|
17
|
+
# object, known as the <tt>@target</tt>. The kind of association any proxy is
|
18
|
+
# about is available in <tt>@reflection</tt>. That's an instance of the class
|
19
|
+
# CouchFoo::Reflection::AssociationReflection.
|
20
|
+
#
|
21
|
+
# For example, given
|
22
|
+
#
|
23
|
+
# class Blog < CouchFoo::Base
|
24
|
+
# has_many :posts
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# blog = Blog.find(:first)
|
28
|
+
#
|
29
|
+
# the association proxy in <tt>blog.posts</tt> has the object in +blog+ as
|
30
|
+
# <tt>@owner</tt>, the collection of its posts as <tt>@target</tt>, and
|
31
|
+
# the <tt>@reflection</tt> object represents a <tt>:has_many</tt> macro.
|
32
|
+
#
|
33
|
+
# This class has most of the basic instance methods removed, and delegates
|
34
|
+
# unknown methods to <tt>@target</tt> via <tt>method_missing</tt>. As a
|
35
|
+
# corner case, it even removes the +class+ method and that's why you get
|
36
|
+
#
|
37
|
+
# blog.posts.class # => Array
|
38
|
+
#
|
39
|
+
# though the object behind <tt>blog.posts</tt> is not an Array, but an
|
40
|
+
# CouchFoo::Associations::HasManyAssociation.
|
41
|
+
#
|
42
|
+
# The <tt>@target</tt> object is not loaded until needed. For example,
|
43
|
+
#
|
44
|
+
# blog.posts.count
|
45
|
+
#
|
46
|
+
# is computed directly through a count view and does not trigger by itself the
|
47
|
+
# instantiation of the actual post records.
|
48
|
+
class AssociationProxy #:nodoc:
|
49
|
+
alias_method :proxy_respond_to?, :respond_to?
|
50
|
+
alias_method :proxy_extend, :extend
|
51
|
+
delegate :to_param, :to => :proxy_target
|
52
|
+
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
|
53
|
+
|
54
|
+
def initialize(owner, reflection)
|
55
|
+
@owner, @reflection = owner, reflection
|
56
|
+
Array(reflection.options[:extend]).each { |ext| proxy_extend(ext) }
|
57
|
+
reset
|
58
|
+
end
|
59
|
+
|
60
|
+
def proxy_owner
|
61
|
+
@owner
|
62
|
+
end
|
63
|
+
|
64
|
+
def proxy_reflection
|
65
|
+
@reflection
|
66
|
+
end
|
67
|
+
|
68
|
+
def proxy_target
|
69
|
+
@target
|
70
|
+
end
|
71
|
+
|
72
|
+
def respond_to?(*args)
|
73
|
+
proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Explicitly proxy === because the instance method removal above
|
77
|
+
# doesn't catch it.
|
78
|
+
def ===(other)
|
79
|
+
load_target
|
80
|
+
other === @target
|
81
|
+
end
|
82
|
+
|
83
|
+
def conditions
|
84
|
+
@conditions ||= @reflection.options[:conditions]
|
85
|
+
end
|
86
|
+
|
87
|
+
def reset
|
88
|
+
@loaded = false
|
89
|
+
@target = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def reload
|
93
|
+
reset
|
94
|
+
load_target
|
95
|
+
self unless @target.nil?
|
96
|
+
end
|
97
|
+
|
98
|
+
def loaded?
|
99
|
+
@loaded
|
100
|
+
end
|
101
|
+
|
102
|
+
def loaded
|
103
|
+
@loaded = true
|
104
|
+
end
|
105
|
+
|
106
|
+
def target
|
107
|
+
@target
|
108
|
+
end
|
109
|
+
|
110
|
+
def target=(target)
|
111
|
+
@target = target
|
112
|
+
loaded
|
113
|
+
end
|
114
|
+
|
115
|
+
def inspect
|
116
|
+
load_target
|
117
|
+
@target.inspect
|
118
|
+
end
|
119
|
+
|
120
|
+
protected
|
121
|
+
def dependent?
|
122
|
+
@reflection.options[:dependent]
|
123
|
+
end
|
124
|
+
|
125
|
+
def quoted_record_ids(records)
|
126
|
+
records.map { |record| record.quoted_id }.join(',')
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_belongs_to_association_for(record)
|
130
|
+
if @reflection.options[:as]
|
131
|
+
record["#{@reflection.options[:as]}_id".to_sym] = @owner.id unless @owner.new_record?
|
132
|
+
record["#{@reflection.options[:as]}_type".to_sym] = @owner.class.name.to_s
|
133
|
+
else
|
134
|
+
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def merge_options_from_reflection!(options)
|
139
|
+
options.reverse_merge!(
|
140
|
+
:limit => @reflection.options[:limit],
|
141
|
+
:offset => @reflection.options[:offset],
|
142
|
+
:include => @reflection.options[:include],
|
143
|
+
:readonly => @reflection.options[:readonly]
|
144
|
+
)
|
145
|
+
end
|
146
|
+
|
147
|
+
def with_scope(*args, &block)
|
148
|
+
@reflection.klass.send :with_scope, *args, &block
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
def method_missing(method, *args)
|
153
|
+
if load_target
|
154
|
+
if block_given?
|
155
|
+
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
156
|
+
else
|
157
|
+
@target.send(method, *args)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Loads the target if needed and returns it.
|
163
|
+
#
|
164
|
+
# This method is abstract in the sense that it relies on +find_target+,
|
165
|
+
# which is expected to be provided by descendants.
|
166
|
+
#
|
167
|
+
# If the target is already loaded it is just returned. Thus, you can call
|
168
|
+
# +load_target+ unconditionally to get the target.
|
169
|
+
#
|
170
|
+
# CouchFoo::DocumentNotFound is rescued within the method, and it is
|
171
|
+
# not reraised. The proxy is reset and +nil+ is the return value.
|
172
|
+
def load_target
|
173
|
+
return nil unless defined?(@loaded)
|
174
|
+
|
175
|
+
if !loaded? and (!@owner.new_record? || foreign_key_present)
|
176
|
+
@target = find_target
|
177
|
+
end
|
178
|
+
|
179
|
+
@loaded = true
|
180
|
+
@target
|
181
|
+
rescue CouchFoo::DocumentNotFound
|
182
|
+
reset
|
183
|
+
end
|
184
|
+
|
185
|
+
# Can be overwritten by associations that might have the foreign key available for an association without
|
186
|
+
# having the object itself (and still being a new record). Currently, only belongs_to presents this scenario.
|
187
|
+
def foreign_key_present
|
188
|
+
false
|
189
|
+
end
|
190
|
+
|
191
|
+
def raise_on_type_mismatch(record)
|
192
|
+
unless record.is_a?(@reflection.klass)
|
193
|
+
message = "#{@reflection.class_name}(##{@reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
|
194
|
+
raise CouchFoo::AssociationTypeMismatch, message
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# Array#flatten has problems with recursive arrays. Going one level deeper solves the majority of the problems.
|
199
|
+
def flatten_deeper(array)
|
200
|
+
array.collect { |element| element.respond_to?(:flatten) ? element.flatten : element }.flatten
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|