mongo_mapper 0.5.6 → 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
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