djsun-mongomapper 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/.gitignore +7 -0
  2. data/History +51 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +71 -0
  6. data/VERSION +1 -0
  7. data/bin/mmconsole +56 -0
  8. data/lib/mongomapper.rb +96 -0
  9. data/lib/mongomapper/associations.rb +61 -0
  10. data/lib/mongomapper/associations/base.rb +71 -0
  11. data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +32 -0
  12. data/lib/mongomapper/associations/belongs_to_proxy.rb +22 -0
  13. data/lib/mongomapper/associations/many_documents_proxy.rb +85 -0
  14. data/lib/mongomapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongomapper/associations/many_embedded_proxy.rb +17 -0
  16. data/lib/mongomapper/associations/many_polymorphic_proxy.rb +11 -0
  17. data/lib/mongomapper/associations/many_proxy.rb +6 -0
  18. data/lib/mongomapper/associations/proxy.rb +67 -0
  19. data/lib/mongomapper/callbacks.rb +106 -0
  20. data/lib/mongomapper/document.rb +278 -0
  21. data/lib/mongomapper/embedded_document.rb +237 -0
  22. data/lib/mongomapper/finder_options.rb +96 -0
  23. data/lib/mongomapper/key.rb +80 -0
  24. data/lib/mongomapper/observing.rb +50 -0
  25. data/lib/mongomapper/pagination.rb +52 -0
  26. data/lib/mongomapper/rails_compatibility/document.rb +15 -0
  27. data/lib/mongomapper/rails_compatibility/embedded_document.rb +25 -0
  28. data/lib/mongomapper/save_with_validation.rb +19 -0
  29. data/lib/mongomapper/serialization.rb +55 -0
  30. data/lib/mongomapper/serializers/json_serializer.rb +79 -0
  31. data/lib/mongomapper/validations.rb +47 -0
  32. data/mongomapper.gemspec +139 -0
  33. data/test/NOTE_ON_TESTING +1 -0
  34. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +39 -0
  35. data/test/functional/associations/test_belongs_to_proxy.rb +35 -0
  36. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +131 -0
  37. data/test/functional/associations/test_many_embedded_proxy.rb +106 -0
  38. data/test/functional/associations/test_many_polymorphic_proxy.rb +267 -0
  39. data/test/functional/associations/test_many_proxy.rb +236 -0
  40. data/test/functional/test_associations.rb +40 -0
  41. data/test/functional/test_callbacks.rb +85 -0
  42. data/test/functional/test_document.rb +691 -0
  43. data/test/functional/test_pagination.rb +81 -0
  44. data/test/functional/test_rails_compatibility.rb +31 -0
  45. data/test/functional/test_validations.rb +172 -0
  46. data/test/models.rb +108 -0
  47. data/test/test_helper.rb +67 -0
  48. data/test/unit/serializers/test_json_serializer.rb +103 -0
  49. data/test/unit/test_association_base.rb +136 -0
  50. data/test/unit/test_document.rb +125 -0
  51. data/test/unit/test_embedded_document.rb +370 -0
  52. data/test/unit/test_finder_options.rb +214 -0
  53. data/test/unit/test_key.rb +217 -0
  54. data/test/unit/test_mongo_id.rb +35 -0
  55. data/test/unit/test_mongomapper.rb +28 -0
  56. data/test/unit/test_observing.rb +101 -0
  57. data/test/unit/test_pagination.rb +113 -0
  58. data/test/unit/test_rails_compatibility.rb +34 -0
  59. data/test/unit/test_serializations.rb +52 -0
  60. data/test/unit/test_validations.rb +259 -0
  61. metadata +189 -0
@@ -0,0 +1,32 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class BelongsToPolymorphicProxy < Proxy
4
+ def replace(doc)
5
+ if doc
6
+ doc.save if doc.new?
7
+ id, type = doc.id, doc.class.name
8
+ end
9
+
10
+ @owner.send("#{@association.belongs_to_key_name}=", id)
11
+ @owner.send("#{@association.type_key_name}=", type)
12
+ reset
13
+ end
14
+
15
+ protected
16
+ def find_target
17
+ proxy_class.find(proxy_id) if proxy_id && proxy_class
18
+ end
19
+
20
+ def proxy_id
21
+ @proxy_id ||= @owner.send(@association.belongs_to_key_name)
22
+ end
23
+
24
+ def proxy_class
25
+ @proxy_class ||= begin
26
+ klass = @owner.send(@association.type_key_name)
27
+ klass && klass.constantize
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class BelongsToProxy < Proxy
4
+ def replace(doc)
5
+ if doc
6
+ doc.save if doc.new?
7
+ id = doc.id
8
+ end
9
+
10
+ @owner.send("#{@association.belongs_to_key_name}=", id)
11
+ reset
12
+ end
13
+
14
+ protected
15
+ def find_target
16
+ if association_id = @owner.send(@association.belongs_to_key_name)
17
+ @association.klass.find(association_id)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,85 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyDocumentsProxy < Proxy
4
+ delegate :klass, :to => :@association
5
+
6
+ def find(*args)
7
+ options = args.extract_options!
8
+ klass.find(*args << scoped_options(options))
9
+ end
10
+
11
+ def paginate(options)
12
+ klass.paginate(scoped_options(options))
13
+ end
14
+
15
+ def all(options={})
16
+ find(:all, scoped_options(options))
17
+ end
18
+
19
+ def first(options={})
20
+ find(:first, scoped_options(options))
21
+ end
22
+
23
+ def last(options={})
24
+ find(:last, scoped_options(options))
25
+ end
26
+
27
+ def count(conditions={})
28
+ klass.count(conditions.deep_merge(scoped_conditions))
29
+ end
30
+
31
+ def replace(docs)
32
+ @target.map(&:destroy) if load_target
33
+ docs.each { |doc| apply_scope(doc).save }
34
+ reset
35
+ end
36
+
37
+ def <<(*docs)
38
+ ensure_owner_saved
39
+ flatten_deeper(docs).each { |doc| apply_scope(doc).save }
40
+ reset
41
+ end
42
+ alias_method :push, :<<
43
+ alias_method :concat, :<<
44
+
45
+ def build(attrs={})
46
+ doc = klass.new(attrs)
47
+ apply_scope(doc)
48
+ doc
49
+ end
50
+
51
+ def create(attrs={})
52
+ doc = klass.new(attrs)
53
+ apply_scope(doc).save
54
+ doc
55
+ end
56
+
57
+ protected
58
+ def scoped_conditions
59
+ {self.foreign_key => @owner.id}
60
+ end
61
+
62
+ def scoped_options(options)
63
+ options.deep_merge({:conditions => scoped_conditions})
64
+ end
65
+
66
+ def find_target
67
+ find(:all)
68
+ end
69
+
70
+ def ensure_owner_saved
71
+ @owner.save if @owner.new?
72
+ end
73
+
74
+ def apply_scope(doc)
75
+ ensure_owner_saved
76
+ doc.send("#{self.foreign_key}=", @owner.id)
77
+ doc
78
+ end
79
+
80
+ def foreign_key
81
+ @association.options[:foreign_key] || @owner.class.name.underscore.gsub("/", "_") + "_id"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,33 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyEmbeddedPolymorphicProxy < Proxy
4
+ def replace(v)
5
+ @_values = v.map do |doc_or_hash|
6
+ if doc_or_hash.kind_of?(EmbeddedDocument)
7
+ doc = doc_or_hash
8
+ {@association.type_key_name => doc.class.name}.merge(doc.attributes)
9
+ else
10
+ doc_or_hash
11
+ end
12
+ end
13
+
14
+ reset
15
+ end
16
+
17
+ protected
18
+ def find_target
19
+ (@_values || []).map do |hash|
20
+ polymorphic_class(hash).new(hash)
21
+ end
22
+ end
23
+
24
+ def polymorphic_class(doc)
25
+ if class_name = doc[@association.type_key_name]
26
+ class_name.constantize
27
+ else
28
+ @association.klass
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyEmbeddedProxy < Proxy
4
+ def replace(v)
5
+ @_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
6
+ reset
7
+ end
8
+
9
+ protected
10
+ def find_target
11
+ (@_values || []).map do |e|
12
+ @association.klass.new(e)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyPolymorphicProxy < ManyDocumentsProxy
4
+ private
5
+ def apply_scope(doc)
6
+ doc.send("#{@association.type_key_name}=", doc.class.name)
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyProxy < ManyDocumentsProxy
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,67 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class Proxy
4
+ attr_reader :owner, :association
5
+
6
+ instance_methods.each do |m|
7
+ undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/
8
+ end
9
+
10
+ def initialize(owner, association)
11
+ @owner = owner
12
+ @association = association
13
+ reset
14
+ end
15
+
16
+ def respond_to?(*methods)
17
+ (load_target && @target.respond_to?(*methods))
18
+ end
19
+
20
+ def reset
21
+ @target = nil
22
+ end
23
+
24
+ def reload_target
25
+ reset
26
+ load_target
27
+ self
28
+ end
29
+
30
+ def send(method, *args)
31
+ load_target
32
+ @target.send(method, *args)
33
+ end
34
+
35
+ def replace(v)
36
+ raise NotImplementedError
37
+ end
38
+
39
+ protected
40
+ def method_missing(method, *args)
41
+ if load_target
42
+ if block_given?
43
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
44
+ else
45
+ @target.send(method, *args)
46
+ end
47
+ end
48
+ end
49
+
50
+ def load_target
51
+ @target ||= find_target
52
+ end
53
+
54
+ def find_target
55
+ raise NotImplementedError
56
+ end
57
+
58
+ # Array#flatten has problems with recursive arrays. Going one level
59
+ # deeper solves the majority of the problems.
60
+ def flatten_deeper(array)
61
+ array.collect do |element|
62
+ (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element
63
+ end.flatten
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,106 @@
1
+ module MongoMapper
2
+ module Callbacks
3
+ def self.included(model) #:nodoc:
4
+ model.class_eval do
5
+ extend Observable
6
+ include ActiveSupport::Callbacks
7
+
8
+ define_callbacks *%w(
9
+ before_save after_save before_create after_create before_update after_update before_validation
10
+ after_validation before_validation_on_create after_validation_on_create before_validation_on_update
11
+ after_validation_on_update before_destroy after_destroy
12
+ )
13
+
14
+ [:create_or_update, :valid?, :create, :update, :destroy].each do |method|
15
+ alias_method_chain method, :callbacks
16
+ end
17
+ end
18
+ end
19
+
20
+ def before_save() end
21
+
22
+ def after_save() end
23
+ def create_or_update_with_callbacks #:nodoc:
24
+ return false if callback(:before_save) == false
25
+ if result = create_or_update_without_callbacks
26
+ callback(:after_save)
27
+ end
28
+ result
29
+ end
30
+ private :create_or_update_with_callbacks
31
+
32
+ def before_create() end
33
+
34
+ def after_create() end
35
+ def create_with_callbacks #:nodoc:
36
+ return false if callback(:before_create) == false
37
+ result = create_without_callbacks
38
+ callback(:after_create)
39
+ result
40
+ end
41
+ private :create_with_callbacks
42
+
43
+ def before_update() end
44
+
45
+ def after_update() end
46
+
47
+ def update_with_callbacks(*args) #:nodoc:
48
+ return false if callback(:before_update) == false
49
+ result = update_without_callbacks(*args)
50
+ callback(:after_update)
51
+ result
52
+ end
53
+ private :update_with_callbacks
54
+
55
+ def before_validation() end
56
+
57
+ def after_validation() end
58
+
59
+ def before_validation_on_create() end
60
+
61
+ def after_validation_on_create() end
62
+
63
+ def before_validation_on_update() end
64
+
65
+ def after_validation_on_update() end
66
+
67
+ def valid_with_callbacks? #:nodoc:
68
+ return false if callback(:before_validation) == false
69
+ result = new? ? callback(:before_validation_on_create) : callback(:before_validation_on_update)
70
+ return false if false == result
71
+
72
+ result = valid_without_callbacks?
73
+ callback(:after_validation)
74
+
75
+ new? ? callback(:after_validation_on_create) : callback(:after_validation_on_update)
76
+ return result
77
+ end
78
+
79
+ def before_destroy() end
80
+
81
+ def after_destroy() end
82
+ def destroy_with_callbacks #:nodoc:
83
+ return false if callback(:before_destroy) == false
84
+ result = destroy_without_callbacks
85
+ callback(:after_destroy)
86
+ result
87
+ end
88
+
89
+ private
90
+ def callback(method)
91
+ result = run_callbacks(method) { |result, object| false == result }
92
+
93
+ if result != false && respond_to?(method)
94
+ result = send(method)
95
+ end
96
+
97
+ notify(method)
98
+ return result
99
+ end
100
+
101
+ def notify(method) #:nodoc:
102
+ self.class.changed
103
+ self.class.notify_observers(method, self)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,278 @@
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 RailsCompatibility::Document
13
+ extend ClassMethods
14
+
15
+ key :created_at, Time
16
+ key :updated_at, Time
17
+ end
18
+
19
+ descendants << model
20
+ end
21
+
22
+ def self.descendants
23
+ @descendants ||= Set.new
24
+ end
25
+
26
+ module ClassMethods
27
+ def find(*args)
28
+ options = args.extract_options!
29
+
30
+ case args.first
31
+ when :first then find_first(options)
32
+ when :last then find_last(options)
33
+ when :all then find_every(options)
34
+ else find_from_ids(args, options)
35
+ end
36
+ end
37
+
38
+ def paginate(options)
39
+ per_page = options.delete(:per_page)
40
+ page = options.delete(:page)
41
+ total_entries = count(options[:conditions] || {})
42
+
43
+ collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
44
+
45
+ options[:limit] = collection.limit
46
+ options[:offset] = collection.offset
47
+
48
+ collection.subject = find_every(options)
49
+ collection
50
+ end
51
+
52
+ def first(options={})
53
+ find_first(options)
54
+ end
55
+
56
+ def last(options={})
57
+ find_last(options)
58
+ end
59
+
60
+ def all(options={})
61
+ find_every(options)
62
+ end
63
+
64
+ def find_by_id(id)
65
+ criteria = FinderOptions.to_mongo_criteria(:_id => id)
66
+ if doc = collection.find_first(criteria)
67
+ new(doc)
68
+ end
69
+ end
70
+
71
+ def count(conditions={})
72
+ collection.count(FinderOptions.to_mongo_criteria(conditions))
73
+ end
74
+
75
+ def create(*docs)
76
+ instances = []
77
+ docs = [{}] if docs.blank?
78
+ docs.flatten.each do |attrs|
79
+ doc = new(attrs); doc.save
80
+ instances << doc
81
+ end
82
+ instances.size == 1 ? instances[0] : instances
83
+ end
84
+
85
+ # For updating single document
86
+ # Person.update(1, {:foo => 'bar'})
87
+ #
88
+ # For updating multiple documents at once:
89
+ # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
90
+ def update(*args)
91
+ updating_multiple = args.length == 1
92
+ if updating_multiple
93
+ update_multiple(args[0])
94
+ else
95
+ id, attributes = args
96
+ update_single(id, attributes)
97
+ end
98
+ end
99
+
100
+ def delete(*ids)
101
+ criteria = FinderOptions.to_mongo_criteria(:_id => ids.flatten)
102
+ collection.remove(criteria)
103
+ end
104
+
105
+ def delete_all(conditions={})
106
+ criteria = FinderOptions.to_mongo_criteria(conditions)
107
+ collection.remove(criteria)
108
+ end
109
+
110
+ def destroy(*ids)
111
+ find_some(ids.flatten).each(&:destroy)
112
+ end
113
+
114
+ def destroy_all(conditions={})
115
+ find(:all, :conditions => conditions).each(&:destroy)
116
+ end
117
+
118
+ def connection(mongo_connection=nil)
119
+ if mongo_connection.nil?
120
+ @connection ||= MongoMapper.connection
121
+ else
122
+ @connection = mongo_connection
123
+ end
124
+ @connection
125
+ end
126
+
127
+ def database(name=nil)
128
+ if name.nil?
129
+ @database ||= MongoMapper.database
130
+ else
131
+ @database = connection.db(name)
132
+ end
133
+ @database
134
+ end
135
+
136
+ def collection(name=nil)
137
+ if name.nil?
138
+ @collection ||= database.collection(self.to_s.demodulize.tableize)
139
+ else
140
+ @collection = database.collection(name)
141
+ end
142
+ @collection
143
+ end
144
+
145
+ def validates_uniqueness_of(*args)
146
+ add_validations(args, MongoMapper::Validations::ValidatesUniquenessOf)
147
+ end
148
+
149
+ def validates_exclusion_of(*args)
150
+ add_validations(args, MongoMapper::Validations::ValidatesExclusionOf)
151
+ end
152
+
153
+ def validates_inclusion_of(*args)
154
+ add_validations(args, MongoMapper::Validations::ValidatesInclusionOf)
155
+ end
156
+
157
+ private
158
+ def find_every(options)
159
+ criteria, options = FinderOptions.new(options).to_a
160
+ collection.find(criteria, options).to_a.map { |doc| new(doc) }
161
+ end
162
+
163
+ def find_first(options)
164
+ options.merge!(:limit => 1)
165
+ find_every({:order => '$natural asc'}.merge(options))[0]
166
+ end
167
+
168
+ def find_last(options)
169
+ find_every(options.merge(:limit => 1, :order => '$natural desc'))[0]
170
+ end
171
+
172
+ def find_some(ids, options={})
173
+ documents = find_every(options.deep_merge(:conditions => {'_id' => ids}))
174
+ if ids.size == documents.size
175
+ documents
176
+ else
177
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
178
+ end
179
+ end
180
+
181
+ def find_one(id, options={})
182
+ if doc = find_every(options.deep_merge(:conditions => {:_id => id})).first
183
+ doc
184
+ else
185
+ raise DocumentNotFound, "Document with id of #{id} does not exist in collection named #{collection.name}"
186
+ end
187
+ end
188
+
189
+ def find_from_ids(ids, options={})
190
+ ids = ids.flatten.compact.uniq
191
+
192
+ case ids.size
193
+ when 0
194
+ raise(DocumentNotFound, "Couldn't find without an ID")
195
+ when 1
196
+ find_one(ids[0], options)
197
+ else
198
+ find_some(ids, options)
199
+ end
200
+ end
201
+
202
+ def update_single(id, attrs)
203
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
204
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
205
+ end
206
+
207
+ find(id).update_attributes(attrs)
208
+ end
209
+
210
+ def update_multiple(docs)
211
+ unless docs.is_a?(Hash)
212
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
213
+ end
214
+
215
+ instances = []
216
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
217
+ instances
218
+ end
219
+ end
220
+
221
+ module InstanceMethods
222
+ def collection
223
+ self.class.collection
224
+ end
225
+
226
+ def new?
227
+ read_attribute('_id').blank?
228
+ end
229
+
230
+ def save
231
+ create_or_update
232
+ end
233
+
234
+ def save!
235
+ create_or_update || raise(DocumentNotValid.new(self))
236
+ end
237
+
238
+ def update_attributes(attrs={})
239
+ self.attributes = attrs
240
+ save
241
+ self
242
+ end
243
+
244
+ def destroy
245
+ criteria = FinderOptions.to_mongo_criteria(:_id => id)
246
+ collection.remove(criteria) unless new?
247
+ freeze
248
+ end
249
+
250
+ private
251
+ def create_or_update
252
+ result = new? ? create : update
253
+ result != false
254
+ end
255
+
256
+ def create
257
+ update_timestamps
258
+ write_attribute :_id, save_to_collection
259
+ end
260
+
261
+ def update
262
+ update_timestamps
263
+ save_to_collection
264
+ end
265
+
266
+ # collection.save returns mongoid
267
+ def save_to_collection
268
+ collection.save(attributes)
269
+ end
270
+
271
+ def update_timestamps
272
+ t = Time.now.utc
273
+ write_attribute('created_at', t) if new?
274
+ write_attribute('updated_at', t)
275
+ end
276
+ end
277
+ end # Document
278
+ end # MongoMapper