mongo_mapper 0.5.6 → 0.5.7

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 (39) hide show
  1. data/.gitignore +3 -1
  2. data/README.rdoc +3 -0
  3. data/VERSION +1 -1
  4. data/lib/mongo_mapper.rb +14 -6
  5. data/lib/mongo_mapper/associations.rb +11 -5
  6. data/lib/mongo_mapper/associations/base.rb +17 -5
  7. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +0 -2
  8. data/lib/mongo_mapper/associations/many_documents_proxy.rb +15 -15
  9. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +2 -2
  10. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +1 -1
  11. data/lib/mongo_mapper/associations/proxy.rb +1 -0
  12. data/lib/mongo_mapper/callbacks.rb +18 -0
  13. data/lib/mongo_mapper/document.rb +206 -89
  14. data/lib/mongo_mapper/dynamic_finder.rb +1 -1
  15. data/lib/mongo_mapper/embedded_document.rb +7 -3
  16. data/lib/mongo_mapper/finder_options.rb +87 -66
  17. data/lib/mongo_mapper/pagination.rb +2 -0
  18. data/lib/mongo_mapper/serialization.rb +2 -3
  19. data/lib/mongo_mapper/serializers/json_serializer.rb +1 -1
  20. data/lib/mongo_mapper/support.rb +9 -0
  21. data/lib/mongo_mapper/validations.rb +3 -1
  22. data/mongo_mapper.gemspec +4 -4
  23. data/test/functional/associations/test_many_documents_as_proxy.rb +2 -2
  24. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +25 -1
  25. data/test/functional/associations/test_many_embedded_proxy.rb +25 -0
  26. data/test/functional/associations/test_many_polymorphic_proxy.rb +48 -6
  27. data/test/functional/associations/test_many_proxy.rb +27 -6
  28. data/test/functional/test_document.rb +49 -29
  29. data/test/functional/test_pagination.rb +17 -17
  30. data/test/functional/test_validations.rb +35 -14
  31. data/test/models.rb +85 -10
  32. data/test/support/{test_timing.rb → timing.rb} +1 -1
  33. data/test/test_helper.rb +8 -8
  34. data/test/unit/test_association_base.rb +17 -0
  35. data/test/unit/test_document.rb +12 -1
  36. data/test/unit/test_embedded_document.rb +13 -4
  37. data/test/unit/test_finder_options.rb +50 -48
  38. data/test/unit/test_pagination.rb +4 -0
  39. metadata +4 -4
@@ -14,7 +14,7 @@ module MongoMapper
14
14
  end
15
15
 
16
16
  protected
17
- def match
17
+ def match
18
18
  case method.to_s
19
19
  when /^find_(all_by|by)_([_a-zA-Z]\w*)$/
20
20
  @finder = :all if $1 == 'all_by'
@@ -332,9 +332,13 @@ module MongoMapper
332
332
  end
333
333
 
334
334
  def read_attribute(name)
335
- value = _keys[name].get(instance_variable_get("@#{name}"))
336
- instance_variable_set "@#{name}", value if !frozen?
337
- value
335
+ if key = _keys[name]
336
+ value = key.get(instance_variable_get("@#{name}"))
337
+ instance_variable_set "@#{name}", value if !frozen?
338
+ value
339
+ else
340
+ raise KeyNotFound, "Could not find key: #{name.inspect}"
341
+ end
338
342
  end
339
343
 
340
344
  def read_attribute_before_typecast(name)
@@ -1,66 +1,35 @@
1
1
  module MongoMapper
2
- class FinderOptions
3
- def self.to_mongo_criteria(model, conditions, parent_key=nil)
4
- criteria = {}
5
- add_sci_scope(model, criteria)
6
-
7
- conditions.each_pair do |field, value|
8
- field = field_normalized(field)
9
- case value
10
- when Array
11
- operator_present = field.to_s =~ /^\$/
12
- criteria[field] = if operator_present
13
- value
14
- else
15
- {'$in' => value}
16
- end
17
- when Hash
18
- criteria[field] = to_mongo_criteria(model, value, field)
19
- else
20
- criteria[field] = value
21
- end
22
- end
23
-
24
- criteria
2
+ # Controls the parsing and handling of options used by finders.
3
+ #
4
+ # == Important Note
5
+ #
6
+ # This class is private to MongoMapper and should not be considered part of
7
+ # MongoMapper's public API. Some documentation herein, however, may prove
8
+ # useful for understanding how MongoMapper handles the parsing of finder
9
+ # conditions and options.
10
+ #
11
+ # @private
12
+ class FinderOperator
13
+ def initialize(field, operator)
14
+ @field, @operator = field, operator
25
15
  end
26
16
 
27
- # adds _type single collection inheritance scope for models that need it
28
- def self.add_sci_scope(model, criteria)
29
- if model.single_collection_inherited?
30
- criteria[:_type] = model.to_s
31
- end
32
- end
33
-
34
- def self.to_mongo_options(model, options)
35
- options = options.dup
36
- {
37
- :fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
38
- :skip => (options.delete(:skip) || options.delete(:offset) || 0).to_i,
39
- :limit => (options.delete(:limit) || 0).to_i,
40
- :sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
41
- }
17
+ def to_criteria(value)
18
+ {@field => {@operator => value}}
42
19
  end
43
-
44
- def self.field_normalized(field)
45
- if field.to_s == 'id'
46
- :_id
47
- else
48
- field
49
- end
50
- end
51
-
20
+ end
21
+
22
+ class FinderOptions
52
23
  OptionKeys = [:fields, :select, :skip, :offset, :limit, :sort, :order]
53
-
54
- attr_reader :model, :options
55
-
24
+
56
25
  def initialize(model, options)
57
- raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
58
-
59
- @model = model
60
-
61
- options = options.symbolize_keys
62
- @options, @conditions = {}, options.delete(:conditions) || {}
26
+ raise ArgumentError, "Options must be a hash" unless options.is_a?(Hash)
27
+ options.symbolize_keys!
63
28
 
29
+ @model = model
30
+ @options = {}
31
+ @conditions = options.delete(:conditions) || {}
32
+
64
33
  options.each_pair do |key, value|
65
34
  if OptionKeys.include?(key)
66
35
  @options[key] = value
@@ -68,42 +37,94 @@ module MongoMapper
68
37
  @conditions[key] = value
69
38
  end
70
39
  end
40
+
41
+ add_sci_scope
71
42
  end
72
43
 
44
+ # @return [Hash] Mongo compatible criteria options
45
+ #
46
+ # @see FinderOptions#to_mongo_criteria
73
47
  def criteria
74
- self.class.to_mongo_criteria(model, @conditions)
48
+ to_mongo_criteria(@conditions)
75
49
  end
76
50
 
51
+ # @return [Hash] Mongo compatible options
77
52
  def options
78
- self.class.to_mongo_options(model, @options)
53
+ options = @options.dup
54
+
55
+ fields = options.delete(:fields) || options.delete(:select)
56
+ skip = options.delete(:skip) || options.delete(:offset) || 0
57
+ limit = options.delete(:limit) || 0
58
+ sort = options.delete(:sort) || convert_order_to_sort(options.delete(:order))
59
+
60
+ {:fields => to_mongo_fields(fields), :skip => skip.to_i, :limit => limit.to_i, :sort => sort}
79
61
  end
80
62
 
63
+ # @return [Array<Hash>] Mongo criteria and options enclosed in an Array
81
64
  def to_a
82
65
  [criteria, options]
83
66
  end
84
-
67
+
85
68
  private
86
- def self.to_mongo_fields(fields)
69
+ def to_mongo_criteria(conditions, parent_key=nil)
70
+ criteria = {}
71
+
72
+ conditions.each_pair do |field, value|
73
+ field = normalized_field(field)
74
+ if field.is_a?(FinderOperator)
75
+ criteria.merge!(field.to_criteria(value))
76
+ next
77
+ end
78
+ case value
79
+ when Array
80
+ operator_present = field.to_s =~ /^\$/
81
+ criteria[field] = operator?(field) ? value : {'$in' => value}
82
+ when Hash
83
+ criteria[field] = to_mongo_criteria(value, field)
84
+ else
85
+ criteria[field] = value
86
+ end
87
+ end
88
+
89
+ criteria
90
+ end
91
+
92
+ def operator?(field)
93
+ field.to_s =~ /^\$/
94
+ end
95
+
96
+ def normalized_field(field)
97
+ field.to_s == 'id' ? :_id : field
98
+ end
99
+
100
+ # adds _type single collection inheritance scope for models that need it
101
+ def add_sci_scope
102
+ if @model.single_collection_inherited?
103
+ @conditions[:_type] = @model.to_s
104
+ end
105
+ end
106
+
107
+ def to_mongo_fields(fields)
87
108
  return if fields.blank?
88
-
109
+
89
110
  if fields.is_a?(String)
90
111
  fields.split(',').map { |field| field.strip }
91
112
  else
92
113
  fields.flatten.compact
93
114
  end
94
115
  end
95
-
96
- def self.to_mongo_sort(sort)
116
+
117
+ def convert_order_to_sort(sort)
97
118
  return if sort.blank?
98
119
  pieces = sort.split(',')
99
120
  pieces.map { |s| to_mongo_sort_piece(s) }
100
121
  end
101
-
102
- def self.to_mongo_sort_piece(str)
122
+
123
+ def to_mongo_sort_piece(str)
103
124
  field, direction = str.strip.split(' ')
104
125
  direction ||= 'ASC'
105
126
  direction = direction.upcase == 'ASC' ? 1 : -1
106
127
  [field, direction]
107
128
  end
108
129
  end
109
- end
130
+ end
@@ -30,6 +30,8 @@ module MongoMapper
30
30
  def skip
31
31
  (current_page - 1) * per_page
32
32
  end
33
+ alias offset skip # for will paginate support
34
+
33
35
 
34
36
  def method_missing(name, *args, &block)
35
37
  @subject.send(name, *args, &block)
@@ -5,7 +5,7 @@ module MongoMapper #:nodoc:
5
5
  class Serializer #:nodoc:
6
6
  attr_reader :options
7
7
 
8
- def initialize(record, options = {})
8
+ def initialize(record, options={})
9
9
  @record, @options = record, options.dup
10
10
  end
11
11
 
@@ -51,5 +51,4 @@ module MongoMapper #:nodoc:
51
51
  end
52
52
  end
53
53
 
54
- dir = Pathname(__FILE__).dirname.expand_path + 'serializers'
55
- require dir + 'json_serializer'
54
+ require 'mongo_mapper/serializers/json_serializer'
@@ -49,7 +49,7 @@ module MongoMapper #:nodoc:
49
49
  # # => {"id": 1, "name": "Konata Izumi", "age": 16,
50
50
  # "created_at": "2006/08/01", "awesome": true,
51
51
  # "permalink": "1-konata-izumi"}
52
- def to_json(options = {})
52
+ def to_json(options={})
53
53
  apply_to_json_defaults(options)
54
54
 
55
55
  if include_root_in_json
@@ -1,4 +1,5 @@
1
1
  class BasicObject #:nodoc:
2
+ alias_method :proxy_extend, :extend
2
3
  instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|^methods$|instance_eval|proxy_|^object_id$)/ }
3
4
  end unless defined?(BasicObject)
4
5
 
@@ -129,6 +130,14 @@ class String
129
130
  end
130
131
  end
131
132
 
133
+ class Symbol
134
+ %w{gt lt gte lte ne in nin mod size where exists}.each do |operator|
135
+ define_method operator do
136
+ MongoMapper::FinderOperator.new(self, "$#{operator}")
137
+ end
138
+ end
139
+ end
140
+
132
141
  class Time
133
142
  def self.to_mongo(value)
134
143
  if value.nil? || value == ''
@@ -18,7 +18,9 @@ module MongoMapper
18
18
  option :scope
19
19
 
20
20
  def valid?(instance)
21
- doc = instance.class.find(:first, :conditions => {self.attribute => instance[attribute]}.merge(scope_conditions(instance)), :limit => 1)
21
+ value = instance[attribute]
22
+ return true if allow_blank && value.blank?
23
+ doc = instance.class.first({self.attribute => value}.merge(scope_conditions(instance)))
22
24
  doc.nil? || instance.id == doc.id
23
25
  end
24
26
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{mongo_mapper}
8
- s.version = "0.5.6"
8
+ s.version = "0.5.7"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["John Nunemaker"]
12
- s.date = %q{2009-10-22}
12
+ s.date = %q{2009-10-28}
13
13
  s.default_executable = %q{mmconsole}
14
14
  s.description = %q{Awesome gem for modeling your domain and storing it in mongo}
15
15
  s.email = %q{nunemaker@gmail.com}
@@ -74,7 +74,7 @@ Gem::Specification.new do |s|
74
74
  "test/functional/test_validations.rb",
75
75
  "test/models.rb",
76
76
  "test/support/custom_matchers.rb",
77
- "test/support/test_timing.rb",
77
+ "test/support/timing.rb",
78
78
  "test/test_helper.rb",
79
79
  "test/unit/serializers/test_json_serializer.rb",
80
80
  "test/unit/test_association_base.rb",
@@ -117,7 +117,7 @@ Gem::Specification.new do |s|
117
117
  "test/functional/test_validations.rb",
118
118
  "test/models.rb",
119
119
  "test/support/custom_matchers.rb",
120
- "test/support/test_timing.rb",
120
+ "test/support/timing.rb",
121
121
  "test/test_helper.rb",
122
122
  "test/unit/serializers/test_json_serializer.rb",
123
123
  "test/unit/test_association_base.rb",
@@ -137,7 +137,7 @@ class ManyDocumentsAsProxyTest < Test::Unit::TestCase
137
137
  end
138
138
 
139
139
  should "work with conditions" do
140
- comments = @post.comments.find(:all, :conditions => {:body => 'comment1'})
140
+ comments = @post.comments.find(:all, :body => 'comment1')
141
141
  comments.should == [@comment1]
142
142
  end
143
143
 
@@ -154,7 +154,7 @@ class ManyDocumentsAsProxyTest < Test::Unit::TestCase
154
154
  end
155
155
 
156
156
  should "work with conditions" do
157
- comments = @post.comments.all(:conditions => {:body => 'comment1'})
157
+ comments = @post.comments.all(:body => 'comment1')
158
158
  comments.should == [@comment1]
159
159
  end
160
160
 
@@ -129,4 +129,28 @@ class ManyEmbeddedPolymorphicProxyTest < Test::Unit::TestCase
129
129
  from_db.transports[2].icu.should == true
130
130
  end
131
131
  end
132
- end
132
+
133
+ context "extending the association" do
134
+ should "work using a block passed to many" do
135
+ catalog = Catalog.new
136
+ medias = catalog.medias = [
137
+ Video.new("file" => "video.mpg", "length" => 3600, :visible => true),
138
+ Music.new("file" => "music.mp3", "bitrate" => "128kbps", :visible => true),
139
+ Image.new("file" => "image.png", "width" => 800, "height" => 600, :visible => false)
140
+ ]
141
+ catalog.save
142
+ catalog.medias.visible.should == [medias[0], medias[1]]
143
+ end
144
+
145
+ should "work using many's :extend option" do
146
+ fleet = TrModels::Fleet.new
147
+ transports = fleet.transports = [
148
+ TrModels::Car.new("license_plate" => "ABC1223", "model" => "Honda Civic", "year" => 2003, :purchased_on => 2.years.ago.to_date),
149
+ TrModels::Bus.new("license_plate" => "XYZ9090", "max_passengers" => 51, :purchased_on => 3.years.ago.to_date),
150
+ TrModels::Ambulance.new("license_plate" => "HDD3030", "icu" => true, :purchased_on => 1.year.ago.to_date)
151
+ ]
152
+ fleet.save
153
+ fleet.transports.to_be_replaced.should == [transports[1]]
154
+ end
155
+ end
156
+ end
@@ -171,4 +171,29 @@ class ManyEmbeddedProxyTest < Test::Unit::TestCase
171
171
 
172
172
  meg.pets.find(sparky.id).should == sparky
173
173
  end
174
+
175
+ context "extending the association" do
176
+ should "work using a block passed to many" do
177
+ project = Project.new(:name => "Some Project")
178
+ addr1 = Address.new(:address => "Gate-3 Lankershim Blvd.", :city => "Universal City", :state => "CA", :zip => "91608")
179
+ addr2 = Address.new(:address => "3000 W. Alameda Ave.", :city => "Burbank", :state => "CA", :zip => "91523")
180
+ addr3 = Address.new(:address => "111 Some Ln", :city => "Nashville", :state => "TN", :zip => "37211")
181
+ project.addresses = [addr1, addr2, addr3]
182
+ project.save
183
+ project.addresses.find_all_by_state("CA").should == [addr1, addr2]
184
+ end
185
+
186
+ should "work using many's :extend option" do
187
+ project = Project.new(:name => "Some Project")
188
+ person1 = Person.new(:name => "Steve")
189
+ person2 = Person.new(:name => "Betty")
190
+ person3 = Person.new(:name => "Cynthia")
191
+
192
+ project.people << person1
193
+ project.people << person2
194
+ project.people << person3
195
+ project.save
196
+ project.people.find_by_name("Steve").should == person1
197
+ end
198
+ end
174
199
  end
@@ -4,6 +4,7 @@ require 'models'
4
4
  class ManyPolymorphicProxyTest < Test::Unit::TestCase
5
5
  def setup
6
6
  Room.collection.clear
7
+ Message.collection.clear
7
8
  end
8
9
 
9
10
  should "default reader to empty array" do
@@ -181,7 +182,7 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
181
182
  end
182
183
 
183
184
  should "work with conditions" do
184
- messages = @lounge.messages.find(:all, :conditions => {:body => 'Loungin!'}, :order => "position")
185
+ messages = @lounge.messages.find(:all, :body => 'Loungin!', :order => "position")
185
186
  messages.should == [@lm1]
186
187
  end
187
188
 
@@ -197,7 +198,7 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
197
198
  end
198
199
 
199
200
  should "work with conditions" do
200
- messages = @lounge.messages.all(:conditions => {:body => 'Loungin!'}, :order => "position")
201
+ messages = @lounge.messages.all(:body => 'Loungin!', :order => "position")
201
202
  messages.should == [@lm1]
202
203
  end
203
204
 
@@ -213,7 +214,7 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
213
214
  end
214
215
 
215
216
  should "work with conditions" do
216
- message = @lounge.messages.find(:first, :conditions => {:body => 'I love loungin!'}, :order => "position asc")
217
+ message = @lounge.messages.find(:first, :body => 'I love loungin!', :order => "position asc")
217
218
  message.should == @lm2
218
219
  end
219
220
  end
@@ -224,7 +225,7 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
224
225
  end
225
226
 
226
227
  should "work with conditions" do
227
- message = @lounge.messages.first(:conditions => {:body => 'I love loungin!'}, :order => "position asc")
228
+ message = @lounge.messages.first(:body => 'I love loungin!', :order => "position asc")
228
229
  message.should == @lm2
229
230
  end
230
231
  end
@@ -235,7 +236,7 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
235
236
  end
236
237
 
237
238
  should "work with conditions" do
238
- message = @lounge.messages.find(:last, :conditions => {:body => 'Loungin!'}, :order => "position asc")
239
+ message = @lounge.messages.find(:last, :body => 'Loungin!', :order => "position asc")
239
240
  message.should == @lm1
240
241
  end
241
242
  end
@@ -246,7 +247,7 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
246
247
  end
247
248
 
248
249
  should "work with conditions" do
249
- message = @lounge.messages.last(:conditions => {:body => 'Loungin!'}, :order => "position asc")
250
+ message = @lounge.messages.last(:body => 'Loungin!', :order => "position asc")
250
251
  message.should == @lm1
251
252
  end
252
253
  end
@@ -263,6 +264,20 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
263
264
  end
264
265
  end
265
266
 
267
+ context "with query options/criteria" do
268
+ should "work with order on association" do
269
+ @lounge.messages.should == [@lm1, @lm2]
270
+ end
271
+
272
+ should "allow overriding the order provided to the association" do
273
+ @lounge.messages.all(:order => 'position desc').should == [@lm2, @lm1]
274
+ end
275
+
276
+ should "allow using conditions on association" do
277
+ @hall.latest_messages.should == [@hm3, @hm2]
278
+ end
279
+ end
280
+
266
281
  context "with multiple ids" do
267
282
  should "work for ids in association" do
268
283
  messages = @lounge.messages.find(@lm1.id, @lm2.id)
@@ -294,4 +309,31 @@ class ManyPolymorphicProxyTest < Test::Unit::TestCase
294
309
  end
295
310
  end
296
311
  end
312
+
313
+ context "extending the association" do
314
+ should "work using a block passed to many" do
315
+ room = Room.new(:name => "Amazing Room")
316
+ messages = room.messages = [
317
+ Enter.new(:body => 'John entered room', :position => 3),
318
+ Chat.new(:body => 'Heyyyoooo!', :position => 4),
319
+ Exit.new(:body => 'John exited room', :position => 5),
320
+ Enter.new(:body => 'Steve entered room', :position => 6),
321
+ Chat.new(:body => 'Anyone there?', :position => 7),
322
+ Exit.new(:body => 'Steve exited room', :position => 8)
323
+ ]
324
+ room.save
325
+ room.messages.older.should == messages[3..5]
326
+ end
327
+
328
+ should "work using many's :extend option" do
329
+ room = Room.new(:name => "Amazing Room")
330
+ accounts = room.accounts = [
331
+ Bot.new(:last_logged_in => 3.weeks.ago),
332
+ User.new(:last_logged_in => nil),
333
+ Bot.new(:last_logged_in => 1.week.ago)
334
+ ]
335
+ room.save
336
+ room.accounts.inactive.should == [accounts[1]]
337
+ end
338
+ end
297
339
  end