djsun-mongomapper 0.3.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.
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