mongo_mapper-unstable 2009.10.11
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/.gitignore +8 -0
- data/LICENSE +20 -0
- data/README.rdoc +50 -0
- data/Rakefile +87 -0
- data/VERSION +1 -0
- data/bin/mmconsole +55 -0
- data/lib/mongo_mapper/associations/base.rb +83 -0
- data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
- data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
- data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +27 -0
- data/lib/mongo_mapper/associations/many_documents_proxy.rb +116 -0
- data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
- data/lib/mongo_mapper/associations/many_embedded_proxy.rb +67 -0
- data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
- data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
- data/lib/mongo_mapper/associations/proxy.rb +74 -0
- data/lib/mongo_mapper/associations.rb +86 -0
- data/lib/mongo_mapper/callbacks.rb +106 -0
- data/lib/mongo_mapper/dirty.rb +137 -0
- data/lib/mongo_mapper/document.rb +340 -0
- data/lib/mongo_mapper/dynamic_finder.rb +35 -0
- data/lib/mongo_mapper/embedded_document.rb +355 -0
- data/lib/mongo_mapper/finder_options.rb +98 -0
- data/lib/mongo_mapper/key.rb +36 -0
- data/lib/mongo_mapper/observing.rb +50 -0
- data/lib/mongo_mapper/pagination.rb +51 -0
- data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
- data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
- data/lib/mongo_mapper/save_with_validation.rb +19 -0
- data/lib/mongo_mapper/serialization.rb +55 -0
- data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
- data/lib/mongo_mapper/support.rb +161 -0
- data/lib/mongo_mapper/validations.rb +69 -0
- data/lib/mongo_mapper.rb +111 -0
- data/mongo_mapper.gemspec +162 -0
- data/specs.watchr +32 -0
- data/test/NOTE_ON_TESTING +1 -0
- data/test/custom_matchers.rb +55 -0
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
- data/test/functional/associations/test_belongs_to_proxy.rb +49 -0
- data/test/functional/associations/test_many_documents_as_proxy.rb +244 -0
- data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +132 -0
- data/test/functional/associations/test_many_embedded_proxy.rb +174 -0
- data/test/functional/associations/test_many_polymorphic_proxy.rb +297 -0
- data/test/functional/associations/test_many_proxy.rb +331 -0
- data/test/functional/test_associations.rb +44 -0
- data/test/functional/test_binary.rb +18 -0
- data/test/functional/test_callbacks.rb +85 -0
- data/test/functional/test_dirty.rb +138 -0
- data/test/functional/test_document.rb +1051 -0
- data/test/functional/test_embedded_document.rb +97 -0
- data/test/functional/test_logger.rb +20 -0
- data/test/functional/test_pagination.rb +87 -0
- data/test/functional/test_rails_compatibility.rb +30 -0
- data/test/functional/test_validations.rb +279 -0
- data/test/models.rb +195 -0
- data/test/test_helper.rb +30 -0
- data/test/unit/serializers/test_json_serializer.rb +189 -0
- data/test/unit/test_association_base.rb +144 -0
- data/test/unit/test_document.rb +184 -0
- data/test/unit/test_dynamic_finder.rb +125 -0
- data/test/unit/test_embedded_document.rb +656 -0
- data/test/unit/test_finder_options.rb +261 -0
- data/test/unit/test_key.rb +172 -0
- data/test/unit/test_mongomapper.rb +28 -0
- data/test/unit/test_observing.rb +101 -0
- data/test/unit/test_pagination.rb +109 -0
- data/test/unit/test_rails_compatibility.rb +39 -0
- data/test/unit/test_serializations.rb +52 -0
- data/test/unit/test_support.rb +291 -0
- data/test/unit/test_time_zones.rb +40 -0
- data/test/unit/test_validations.rb +503 -0
- metadata +210 -0
@@ -0,0 +1,340 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module MongoMapper
|
4
|
+
module Document
|
5
|
+
def self.included(model)
|
6
|
+
model.class_eval do
|
7
|
+
include EmbeddedDocument
|
8
|
+
include InstanceMethods
|
9
|
+
include Observing
|
10
|
+
include Callbacks
|
11
|
+
include SaveWithValidation
|
12
|
+
include Dirty
|
13
|
+
include RailsCompatibility::Document
|
14
|
+
extend Validations::Macros
|
15
|
+
extend ClassMethods
|
16
|
+
extend Finders
|
17
|
+
|
18
|
+
def self.per_page
|
19
|
+
25
|
20
|
+
end unless respond_to?(:per_page)
|
21
|
+
end
|
22
|
+
|
23
|
+
descendants << model
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.descendants
|
27
|
+
@descendants ||= Set.new
|
28
|
+
end
|
29
|
+
|
30
|
+
module ClassMethods
|
31
|
+
def key(*args)
|
32
|
+
key = super
|
33
|
+
create_indexes_for(key)
|
34
|
+
key
|
35
|
+
end
|
36
|
+
|
37
|
+
def ensure_index(name_or_array, options={})
|
38
|
+
keys_to_index = if name_or_array.is_a?(Array)
|
39
|
+
name_or_array.map { |pair| [pair[0], pair[1]] }
|
40
|
+
else
|
41
|
+
name_or_array
|
42
|
+
end
|
43
|
+
|
44
|
+
MongoMapper.ensure_index(self, keys_to_index, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def find(*args)
|
48
|
+
options = args.extract_options!
|
49
|
+
case args.first
|
50
|
+
when :first then first(options)
|
51
|
+
when :last then last(options)
|
52
|
+
when :all then find_every(options)
|
53
|
+
when Array then find_some(args, options)
|
54
|
+
else
|
55
|
+
case args.size
|
56
|
+
when 0
|
57
|
+
raise DocumentNotFound, "Couldn't find without an ID"
|
58
|
+
when 1
|
59
|
+
find_one(args[0], options)
|
60
|
+
else
|
61
|
+
find_some(args, options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def paginate(options)
|
67
|
+
per_page = options.delete(:per_page) || self.per_page
|
68
|
+
page = options.delete(:page)
|
69
|
+
total_entries = count(options[:conditions] || {})
|
70
|
+
collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
|
71
|
+
|
72
|
+
options[:limit] = collection.limit
|
73
|
+
options[:skip] = collection.skip
|
74
|
+
|
75
|
+
collection.subject = find_every(options)
|
76
|
+
collection
|
77
|
+
end
|
78
|
+
|
79
|
+
def first(options={})
|
80
|
+
options.merge!(:limit => 1)
|
81
|
+
find_every(options)[0]
|
82
|
+
end
|
83
|
+
|
84
|
+
def last(options={})
|
85
|
+
if options[:order].blank?
|
86
|
+
raise ':order option must be provided when using last'
|
87
|
+
end
|
88
|
+
|
89
|
+
options.merge!(:limit => 1)
|
90
|
+
options[:order] = invert_order_clause(options[:order])
|
91
|
+
find_every(options)[0]
|
92
|
+
end
|
93
|
+
|
94
|
+
def all(options={})
|
95
|
+
find_every(options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def find_by_id(id)
|
99
|
+
criteria = FinderOptions.to_mongo_criteria(:_id => id)
|
100
|
+
if doc = collection.find_one(criteria)
|
101
|
+
new(doc)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def count(conditions={})
|
106
|
+
collection.find(FinderOptions.to_mongo_criteria(conditions)).count
|
107
|
+
end
|
108
|
+
|
109
|
+
def exists?(conditions={})
|
110
|
+
!count(conditions).zero?
|
111
|
+
end
|
112
|
+
|
113
|
+
def create(*docs)
|
114
|
+
instances = []
|
115
|
+
docs = [{}] if docs.blank?
|
116
|
+
docs.flatten.each do |attrs|
|
117
|
+
doc = new(attrs); doc.save
|
118
|
+
instances << doc
|
119
|
+
end
|
120
|
+
instances.size == 1 ? instances[0] : instances
|
121
|
+
end
|
122
|
+
|
123
|
+
# For updating single document
|
124
|
+
# Person.update(1, {:foo => 'bar'})
|
125
|
+
#
|
126
|
+
# For updating multiple documents at once:
|
127
|
+
# Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
|
128
|
+
def update(*args)
|
129
|
+
updating_multiple = args.length == 1
|
130
|
+
if updating_multiple
|
131
|
+
update_multiple(args[0])
|
132
|
+
else
|
133
|
+
id, attributes = args
|
134
|
+
update_single(id, attributes)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def delete(*ids)
|
139
|
+
criteria = FinderOptions.to_mongo_criteria(:_id => ids.flatten)
|
140
|
+
collection.remove(criteria)
|
141
|
+
end
|
142
|
+
|
143
|
+
def delete_all(conditions={})
|
144
|
+
criteria = FinderOptions.to_mongo_criteria(conditions)
|
145
|
+
collection.remove(criteria)
|
146
|
+
end
|
147
|
+
|
148
|
+
def destroy(*ids)
|
149
|
+
find_some(ids.flatten).each(&:destroy)
|
150
|
+
end
|
151
|
+
|
152
|
+
def destroy_all(conditions={})
|
153
|
+
find(:all, :conditions => conditions).each(&:destroy)
|
154
|
+
end
|
155
|
+
|
156
|
+
def connection(mongo_connection=nil)
|
157
|
+
if mongo_connection.nil?
|
158
|
+
@connection ||= MongoMapper.connection
|
159
|
+
else
|
160
|
+
@connection = mongo_connection
|
161
|
+
end
|
162
|
+
@connection
|
163
|
+
end
|
164
|
+
|
165
|
+
def database(name=nil)
|
166
|
+
if name.nil?
|
167
|
+
@database ||= MongoMapper.database
|
168
|
+
else
|
169
|
+
@database = connection.db(name)
|
170
|
+
end
|
171
|
+
@database
|
172
|
+
end
|
173
|
+
|
174
|
+
# Changes the collection name from the default to whatever you want
|
175
|
+
def set_collection_name(name=nil)
|
176
|
+
@collection = nil
|
177
|
+
@collection_name = name
|
178
|
+
end
|
179
|
+
|
180
|
+
# Returns the collection name, if not set, defaults to class name tableized
|
181
|
+
def collection_name
|
182
|
+
@collection_name ||= self.to_s.demodulize.tableize
|
183
|
+
end
|
184
|
+
|
185
|
+
# Returns the mongo ruby driver collection object
|
186
|
+
def collection
|
187
|
+
@collection ||= database.collection(collection_name)
|
188
|
+
end
|
189
|
+
|
190
|
+
def timestamps!
|
191
|
+
key :created_at, Time
|
192
|
+
key :updated_at, Time
|
193
|
+
|
194
|
+
class_eval { before_save :update_timestamps }
|
195
|
+
end
|
196
|
+
|
197
|
+
protected
|
198
|
+
def method_missing(method, *args)
|
199
|
+
finder = DynamicFinder.new(method)
|
200
|
+
|
201
|
+
if finder.found?
|
202
|
+
meta_def(finder.method) { |*args| dynamic_find(finder, args) }
|
203
|
+
send(finder.method, *args)
|
204
|
+
else
|
205
|
+
super
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
def create_indexes_for(key)
|
211
|
+
ensure_index key.name if key.options[:index]
|
212
|
+
end
|
213
|
+
|
214
|
+
def find_every(options)
|
215
|
+
criteria, options = FinderOptions.new(options).to_a
|
216
|
+
collection.find(criteria, options).to_a.map do |doc|
|
217
|
+
begin
|
218
|
+
klass = doc['_type'].present? ? doc['_type'].constantize : self
|
219
|
+
klass.new(doc)
|
220
|
+
rescue NameError
|
221
|
+
new(doc)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def invert_order_clause(order)
|
227
|
+
order.split(',').map do |order_segment|
|
228
|
+
if order_segment =~ /\sasc/i
|
229
|
+
order_segment.sub /\sasc/i, ' desc'
|
230
|
+
elsif order_segment =~ /\sdesc/i
|
231
|
+
order_segment.sub /\sdesc/i, ' asc'
|
232
|
+
else
|
233
|
+
"#{order_segment.strip} desc"
|
234
|
+
end
|
235
|
+
end.join(',')
|
236
|
+
end
|
237
|
+
|
238
|
+
def find_some(ids, options={})
|
239
|
+
ids = ids.flatten.compact.uniq
|
240
|
+
documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
|
241
|
+
|
242
|
+
if ids.size == documents.size
|
243
|
+
documents
|
244
|
+
else
|
245
|
+
raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def find_one(id, options={})
|
250
|
+
if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
|
251
|
+
doc
|
252
|
+
else
|
253
|
+
raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
def update_single(id, attrs)
|
258
|
+
if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
|
259
|
+
raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
|
260
|
+
end
|
261
|
+
|
262
|
+
doc = find(id)
|
263
|
+
doc.update_attributes(attrs)
|
264
|
+
doc
|
265
|
+
end
|
266
|
+
|
267
|
+
def update_multiple(docs)
|
268
|
+
unless docs.is_a?(Hash)
|
269
|
+
raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
|
270
|
+
end
|
271
|
+
|
272
|
+
instances = []
|
273
|
+
docs.each_pair { |id, attrs| instances << update(id, attrs) }
|
274
|
+
instances
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
module InstanceMethods
|
279
|
+
def collection
|
280
|
+
self.class.collection
|
281
|
+
end
|
282
|
+
|
283
|
+
def new?
|
284
|
+
read_attribute('_id').blank? || using_custom_id?
|
285
|
+
end
|
286
|
+
|
287
|
+
def save
|
288
|
+
create_or_update
|
289
|
+
end
|
290
|
+
|
291
|
+
def save!
|
292
|
+
create_or_update || raise(DocumentNotValid.new(self))
|
293
|
+
end
|
294
|
+
|
295
|
+
def destroy
|
296
|
+
return false if frozen?
|
297
|
+
|
298
|
+
criteria = FinderOptions.to_mongo_criteria(:_id => id)
|
299
|
+
collection.remove(criteria) unless new?
|
300
|
+
freeze
|
301
|
+
end
|
302
|
+
|
303
|
+
private
|
304
|
+
def create_or_update
|
305
|
+
result = new? ? create : update
|
306
|
+
result != false
|
307
|
+
end
|
308
|
+
|
309
|
+
def create
|
310
|
+
assign_id
|
311
|
+
save_to_collection
|
312
|
+
end
|
313
|
+
|
314
|
+
def assign_id
|
315
|
+
if read_attribute(:_id).blank?
|
316
|
+
write_attribute(:_id, Mongo::ObjectID.new.to_s)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def update
|
321
|
+
save_to_collection
|
322
|
+
end
|
323
|
+
|
324
|
+
def save_to_collection
|
325
|
+
clear_custom_id_flag
|
326
|
+
collection.save(to_mongo)
|
327
|
+
end
|
328
|
+
|
329
|
+
def update_timestamps
|
330
|
+
now = Time.now.utc
|
331
|
+
write_attribute('created_at', now) if new?
|
332
|
+
write_attribute('updated_at', now)
|
333
|
+
end
|
334
|
+
|
335
|
+
def clear_custom_id_flag
|
336
|
+
@using_custom_id = nil
|
337
|
+
end
|
338
|
+
end
|
339
|
+
end # Document
|
340
|
+
end # MongoMapper
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
class DynamicFinder
|
3
|
+
attr_reader :method, :attributes, :finder, :bang, :instantiator
|
4
|
+
|
5
|
+
def initialize(method)
|
6
|
+
@method = method
|
7
|
+
@finder = :first
|
8
|
+
@bang = false
|
9
|
+
match()
|
10
|
+
end
|
11
|
+
|
12
|
+
def found?
|
13
|
+
@finder.present?
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
def match
|
18
|
+
case method.to_s
|
19
|
+
when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
|
20
|
+
@finder = :all if $1 == 'all_by'
|
21
|
+
names = $2
|
22
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
23
|
+
@bang = true
|
24
|
+
names = $1
|
25
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
26
|
+
@instantiator = $1 == 'initialize' ? :new : :create
|
27
|
+
names = $2
|
28
|
+
else
|
29
|
+
@finder = nil
|
30
|
+
end
|
31
|
+
|
32
|
+
@attributes = names && names.split('_and_')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
module MongoMapper
|
4
|
+
module EmbeddedDocument
|
5
|
+
def self.included(model)
|
6
|
+
model.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
|
10
|
+
extend Associations::ClassMethods
|
11
|
+
include Associations::InstanceMethods
|
12
|
+
|
13
|
+
include RailsCompatibility::EmbeddedDocument
|
14
|
+
include Validatable
|
15
|
+
include Serialization
|
16
|
+
|
17
|
+
extend Validations::Macros
|
18
|
+
|
19
|
+
key :_id, String
|
20
|
+
attr_accessor :_root_document
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
module ClassMethods
|
25
|
+
def logger
|
26
|
+
MongoMapper.logger
|
27
|
+
end
|
28
|
+
|
29
|
+
def inherited(subclass)
|
30
|
+
unless subclass.embeddable?
|
31
|
+
subclass.set_collection_name(collection_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
(@subclasses ||= []) << subclass
|
35
|
+
end
|
36
|
+
|
37
|
+
def subclasses
|
38
|
+
@subclasses
|
39
|
+
end
|
40
|
+
|
41
|
+
def keys
|
42
|
+
@keys ||= if parent = parent_model
|
43
|
+
parent.keys.dup
|
44
|
+
else
|
45
|
+
HashWithIndifferentAccess.new
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def key(*args)
|
50
|
+
key = Key.new(*args)
|
51
|
+
|
52
|
+
if keys[key.name].blank?
|
53
|
+
keys[key.name] = key
|
54
|
+
|
55
|
+
create_accessors_for(key)
|
56
|
+
create_key_in_subclasses(*args)
|
57
|
+
create_validations_for(key)
|
58
|
+
|
59
|
+
key
|
60
|
+
end
|
61
|
+
|
62
|
+
key
|
63
|
+
end
|
64
|
+
|
65
|
+
def embeddable?
|
66
|
+
!self.ancestors.include?(Document)
|
67
|
+
end
|
68
|
+
|
69
|
+
def parent_model
|
70
|
+
(ancestors - [self,EmbeddedDocument]).find do |parent_class|
|
71
|
+
parent_class.ancestors.include?(EmbeddedDocument)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def to_mongo(instance)
|
76
|
+
return nil if instance.nil?
|
77
|
+
instance.to_mongo
|
78
|
+
end
|
79
|
+
|
80
|
+
def from_mongo(instance_or_hash)
|
81
|
+
return nil if instance_or_hash.nil?
|
82
|
+
|
83
|
+
if instance_or_hash.is_a?(self)
|
84
|
+
instance_or_hash
|
85
|
+
else
|
86
|
+
new(instance_or_hash)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
def accessors_module
|
92
|
+
if const_defined?('MongoMapperKeys')
|
93
|
+
const_get 'MongoMapperKeys'
|
94
|
+
else
|
95
|
+
const_set 'MongoMapperKeys', Module.new
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def create_accessors_for(key)
|
100
|
+
accessors_module.module_eval <<-end_eval
|
101
|
+
def #{key.name}
|
102
|
+
read_attribute(:'#{key.name}')
|
103
|
+
end
|
104
|
+
|
105
|
+
def #{key.name}_before_typecast
|
106
|
+
read_attribute_before_typecast(:'#{key.name}')
|
107
|
+
end
|
108
|
+
|
109
|
+
def #{key.name}=(value)
|
110
|
+
write_attribute(:'#{key.name}', value)
|
111
|
+
end
|
112
|
+
|
113
|
+
def #{key.name}?
|
114
|
+
read_attribute(:#{key.name}).present?
|
115
|
+
end
|
116
|
+
end_eval
|
117
|
+
include accessors_module
|
118
|
+
end
|
119
|
+
|
120
|
+
def create_key_in_subclasses(*args)
|
121
|
+
return if subclasses.blank?
|
122
|
+
|
123
|
+
subclasses.each do |subclass|
|
124
|
+
subclass.key(*args)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def create_validations_for(key)
|
129
|
+
attribute = key.name.to_sym
|
130
|
+
|
131
|
+
if key.options[:required]
|
132
|
+
validates_presence_of(attribute)
|
133
|
+
end
|
134
|
+
|
135
|
+
if key.options[:unique]
|
136
|
+
validates_uniqueness_of(attribute)
|
137
|
+
end
|
138
|
+
|
139
|
+
if key.options[:numeric]
|
140
|
+
number_options = key.type == Integer ? {:only_integer => true} : {}
|
141
|
+
validates_numericality_of(attribute, number_options)
|
142
|
+
end
|
143
|
+
|
144
|
+
if key.options[:format]
|
145
|
+
validates_format_of(attribute, :with => key.options[:format])
|
146
|
+
end
|
147
|
+
|
148
|
+
if key.options[:length]
|
149
|
+
length_options = case key.options[:length]
|
150
|
+
when Integer
|
151
|
+
{:minimum => 0, :maximum => key.options[:length]}
|
152
|
+
when Range
|
153
|
+
{:within => key.options[:length]}
|
154
|
+
when Hash
|
155
|
+
key.options[:length]
|
156
|
+
end
|
157
|
+
validates_length_of(attribute, length_options)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
module InstanceMethods
|
163
|
+
def logger
|
164
|
+
self.class.logger
|
165
|
+
end
|
166
|
+
|
167
|
+
def initialize(attrs={})
|
168
|
+
unless attrs.nil?
|
169
|
+
self.class.associations.each_pair do |name, association|
|
170
|
+
if collection = attrs.delete(name)
|
171
|
+
if association.many? && association.klass.embeddable?
|
172
|
+
root_document = attrs[:_root_document] || self
|
173
|
+
collection.each do |doc|
|
174
|
+
doc[:_root_document] = root_document
|
175
|
+
end
|
176
|
+
end
|
177
|
+
send("#{association.name}=", collection)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
self.attributes = attrs
|
182
|
+
|
183
|
+
if respond_to?(:_type=) && self['_type'].blank?
|
184
|
+
self._type = self.class.name
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
if self.class.embeddable?
|
189
|
+
if read_attribute(:_id).blank?
|
190
|
+
write_attribute :_id, Mongo::ObjectID.new.to_s
|
191
|
+
@new_document = true
|
192
|
+
else
|
193
|
+
@new_document = false
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def new?
|
199
|
+
!!@new_document
|
200
|
+
end
|
201
|
+
|
202
|
+
def attributes=(attrs)
|
203
|
+
return if attrs.blank?
|
204
|
+
attrs.each_pair do |name, value|
|
205
|
+
writer_method = "#{name}="
|
206
|
+
|
207
|
+
if respond_to?(writer_method)
|
208
|
+
self.send(writer_method, value)
|
209
|
+
else
|
210
|
+
self[name.to_s] = value
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def attributes
|
216
|
+
attrs = HashWithIndifferentAccess.new
|
217
|
+
|
218
|
+
embedded_keys.each do |key|
|
219
|
+
puts key.inspect
|
220
|
+
attrs[key.name] = read_attribute(key.name).try(:attributes)
|
221
|
+
end
|
222
|
+
|
223
|
+
non_embedded_keys.each do |key|
|
224
|
+
attrs[key.name] = read_attribute(key.name)
|
225
|
+
end
|
226
|
+
|
227
|
+
embedded_associations.each do |association|
|
228
|
+
documents = instance_variable_get(association.ivar)
|
229
|
+
next if documents.nil?
|
230
|
+
attrs[association.name] = documents.collect { |doc| doc.attributes }
|
231
|
+
end
|
232
|
+
|
233
|
+
attrs
|
234
|
+
end
|
235
|
+
|
236
|
+
def to_mongo
|
237
|
+
attrs = HashWithIndifferentAccess.new
|
238
|
+
|
239
|
+
_keys.each_pair do |name, key|
|
240
|
+
value = key.set(read_attribute(key.name))
|
241
|
+
attrs[name] = value unless value.nil?
|
242
|
+
end
|
243
|
+
|
244
|
+
embedded_associations.each do |association|
|
245
|
+
if documents = instance_variable_get(association.ivar)
|
246
|
+
attrs[association.name] = documents.map { |document| document.to_mongo }
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
attrs
|
251
|
+
end
|
252
|
+
|
253
|
+
def clone
|
254
|
+
clone_attributes = self.attributes
|
255
|
+
clone_attributes.delete("_id")
|
256
|
+
self.class.new(clone_attributes)
|
257
|
+
end
|
258
|
+
|
259
|
+
def [](name)
|
260
|
+
read_attribute(name)
|
261
|
+
end
|
262
|
+
|
263
|
+
def []=(name, value)
|
264
|
+
ensure_key_exists(name)
|
265
|
+
write_attribute(name, value)
|
266
|
+
end
|
267
|
+
|
268
|
+
def ==(other)
|
269
|
+
other.is_a?(self.class) && id == other.id
|
270
|
+
end
|
271
|
+
|
272
|
+
def id
|
273
|
+
read_attribute(:_id)
|
274
|
+
end
|
275
|
+
|
276
|
+
def id=(value)
|
277
|
+
@using_custom_id = true
|
278
|
+
write_attribute :_id, value
|
279
|
+
end
|
280
|
+
|
281
|
+
def using_custom_id?
|
282
|
+
!!@using_custom_id
|
283
|
+
end
|
284
|
+
|
285
|
+
def inspect
|
286
|
+
attributes_as_nice_string = key_names.collect do |name|
|
287
|
+
"#{name}: #{read_attribute(name).inspect}"
|
288
|
+
end.join(", ")
|
289
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
290
|
+
end
|
291
|
+
|
292
|
+
def save
|
293
|
+
if _root_document
|
294
|
+
_root_document.save
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def save!
|
299
|
+
if _root_document
|
300
|
+
_root_document.save!
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def update_attributes(attrs={})
|
305
|
+
self.attributes = attrs
|
306
|
+
save
|
307
|
+
end
|
308
|
+
|
309
|
+
private
|
310
|
+
def _keys
|
311
|
+
self.class.keys
|
312
|
+
end
|
313
|
+
|
314
|
+
def key_names
|
315
|
+
_keys.keys
|
316
|
+
end
|
317
|
+
|
318
|
+
def non_embedded_keys
|
319
|
+
_keys.values.select { |key| !key.embeddable? }
|
320
|
+
end
|
321
|
+
|
322
|
+
def embedded_keys
|
323
|
+
_keys.values.select { |key| key.embeddable? }
|
324
|
+
end
|
325
|
+
|
326
|
+
def ensure_key_exists(name)
|
327
|
+
self.class.key(name) unless respond_to?("#{name}=")
|
328
|
+
end
|
329
|
+
|
330
|
+
def read_attribute(name)
|
331
|
+
value = _keys[name].get(instance_variable_get("@#{name}"))
|
332
|
+
instance_variable_set "@#{name}", value if !frozen?
|
333
|
+
value
|
334
|
+
end
|
335
|
+
|
336
|
+
def read_attribute_before_typecast(name)
|
337
|
+
instance_variable_get("@#{name}_before_typecast")
|
338
|
+
end
|
339
|
+
|
340
|
+
def write_attribute(name, value)
|
341
|
+
key = _keys[name]
|
342
|
+
instance_variable_set "@#{name}_before_typecast", value
|
343
|
+
instance_variable_set "@#{name}", key.set(value)
|
344
|
+
end
|
345
|
+
|
346
|
+
def embedded_associations
|
347
|
+
self.class.associations.select do |name, association|
|
348
|
+
association.embeddable?
|
349
|
+
end.map do |name, association|
|
350
|
+
association
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end # InstanceMethods
|
354
|
+
end # EmbeddedDocument
|
355
|
+
end # MongoMapper
|