crnixon-mongomapper 0.2.0 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. data/.gitignore +1 -0
  2. data/History +48 -0
  3. data/README.rdoc +5 -3
  4. data/Rakefile +6 -4
  5. data/VERSION +1 -1
  6. data/bin/mmconsole +56 -0
  7. data/lib/mongomapper.rb +29 -18
  8. data/lib/mongomapper/associations.rb +53 -38
  9. data/lib/mongomapper/associations/base.rb +53 -20
  10. data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  11. data/lib/mongomapper/associations/belongs_to_proxy.rb +10 -14
  12. data/lib/mongomapper/associations/many_documents_as_proxy.rb +27 -0
  13. data/lib/mongomapper/associations/many_documents_proxy.rb +103 -0
  14. data/lib/mongomapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongomapper/associations/{has_many_embedded_proxy.rb → many_embedded_proxy.rb} +6 -8
  16. data/lib/mongomapper/associations/many_polymorphic_proxy.rb +11 -0
  17. data/lib/mongomapper/associations/{array_proxy.rb → many_proxy.rb} +1 -1
  18. data/lib/mongomapper/associations/proxy.rb +24 -21
  19. data/lib/mongomapper/callbacks.rb +1 -1
  20. data/lib/mongomapper/document.rb +160 -74
  21. data/lib/mongomapper/dynamic_finder.rb +38 -0
  22. data/lib/mongomapper/embedded_document.rb +154 -105
  23. data/lib/mongomapper/finder_options.rb +11 -7
  24. data/lib/mongomapper/key.rb +15 -21
  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/serialization.rb +1 -1
  29. data/lib/mongomapper/serializers/json_serializer.rb +15 -0
  30. data/lib/mongomapper/support.rb +30 -0
  31. data/mongomapper.gemspec +87 -46
  32. data/test/NOTE_ON_TESTING +1 -0
  33. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +53 -0
  34. data/test/functional/associations/test_belongs_to_proxy.rb +45 -0
  35. data/test/functional/associations/test_many_documents_as_proxy.rb +253 -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 +261 -0
  39. data/test/functional/associations/test_many_proxy.rb +295 -0
  40. data/test/functional/test_associations.rb +47 -0
  41. data/test/{test_callbacks.rb → functional/test_callbacks.rb} +2 -1
  42. data/test/functional/test_document.rb +952 -0
  43. data/test/functional/test_pagination.rb +81 -0
  44. data/test/functional/test_rails_compatibility.rb +30 -0
  45. data/test/functional/test_validations.rb +172 -0
  46. data/test/models.rb +169 -0
  47. data/test/test_helper.rb +7 -2
  48. data/test/unit/serializers/test_json_serializer.rb +189 -0
  49. data/test/unit/test_association_base.rb +144 -0
  50. data/test/unit/test_document.rb +123 -0
  51. data/test/unit/test_embedded_document.rb +526 -0
  52. data/test/{test_finder_options.rb → unit/test_finder_options.rb} +36 -1
  53. data/test/{test_key.rb → unit/test_key.rb} +59 -12
  54. data/test/{test_mongomapper.rb → unit/test_mongomapper.rb} +0 -0
  55. data/test/{test_observing.rb → unit/test_observing.rb} +0 -0
  56. data/test/unit/test_pagination.rb +113 -0
  57. data/test/unit/test_rails_compatibility.rb +34 -0
  58. data/test/{test_serializations.rb → unit/test_serializations.rb} +0 -2
  59. data/test/{test_validations.rb → unit/test_validations.rb} +0 -134
  60. metadata +81 -43
  61. data/lib/mongomapper/associations/has_many_proxy.rb +0 -28
  62. data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +0 -31
  63. data/lib/mongomapper/rails_compatibility.rb +0 -23
  64. data/test/serializers/test_json_serializer.rb +0 -104
  65. data/test/test_associations.rb +0 -174
  66. data/test/test_document.rb +0 -944
  67. data/test/test_embedded_document.rb +0 -253
  68. data/test/test_rails_compatibility.rb +0 -29
@@ -0,0 +1,38 @@
1
+ module MongoMapper
2
+ class DynamicFinder
3
+ attr_reader :options
4
+
5
+ def initialize(model, method)
6
+ @model = model
7
+ @options = {}
8
+ @options[:method] = method
9
+ match
10
+ end
11
+
12
+ def valid?
13
+ @options[:finder].present?
14
+ end
15
+
16
+ protected
17
+ def match
18
+ @options[:finder] = :first
19
+
20
+ case @options[:method].to_s
21
+ when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
22
+ @options[:finder] = :last if $1 == 'last_by'
23
+ @options[:finder] = :all if $1 == 'all_by'
24
+ names = $2
25
+ when /^find_by_([_a-zA-Z]\w*)\!$/
26
+ @options[:bang] = true
27
+ names = $1
28
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
29
+ @options[:instantiator] = $1 == 'initialize' ? :new : :create
30
+ names = $2
31
+ else
32
+ @options[:finder] = nil
33
+ end
34
+
35
+ @options[:attribute_names] = names && names.split('_and_')
36
+ end
37
+ end
38
+ end
@@ -2,8 +2,6 @@ require 'observer'
2
2
 
3
3
  module MongoMapper
4
4
  module EmbeddedDocument
5
- class NotImplemented < StandardError; end
6
-
7
5
  def self.included(model)
8
6
  model.class_eval do
9
7
  extend ClassMethods
@@ -12,22 +10,56 @@ module MongoMapper
12
10
  extend Associations::ClassMethods
13
11
  include Associations::InstanceMethods
14
12
 
13
+ include RailsCompatibility::EmbeddedDocument
15
14
  include Validatable
16
15
  include Serialization
16
+
17
+ key :_id, String
17
18
  end
18
19
  end
19
20
 
20
21
  module ClassMethods
22
+ def inherited(subclass)
23
+ unless subclass.embeddable?
24
+ subclass.collection(self.collection.name)
25
+ end
26
+
27
+ (@subclasses ||= []) << subclass
28
+ end
29
+
30
+ def subclasses
31
+ @subclasses
32
+ end
33
+
21
34
  def keys
22
- @keys ||= HashWithIndifferentAccess.new
35
+ @keys ||= if parent = parent_model
36
+ parent.keys.dup
37
+ else
38
+ HashWithIndifferentAccess.new
39
+ end
23
40
  end
24
41
 
25
- def key(name, type, options={})
26
- key = Key.new(name, type, options)
27
- keys[key.name] = key
28
- apply_validations_for(key)
29
- create_indexes_for(key)
30
- key
42
+ def key(*args)
43
+ key = Key.new(*args)
44
+
45
+ if keys[key.name].blank?
46
+ keys[key.name] = key
47
+
48
+ create_accessors_for(key)
49
+ add_to_subclasses(*args)
50
+ apply_validations_for(key)
51
+ create_indexes_for(key)
52
+
53
+ key
54
+ end
55
+ end
56
+
57
+ def add_to_subclasses(*args)
58
+ return if subclasses.blank?
59
+
60
+ subclasses.each do |subclass|
61
+ subclass.key(*args)
62
+ end
31
63
  end
32
64
 
33
65
  def ensure_index(name_or_array, options={})
@@ -44,7 +76,42 @@ module MongoMapper
44
76
  !self.ancestors.include?(Document)
45
77
  end
46
78
 
79
+ def parent_model
80
+ (ancestors - [self,EmbeddedDocument]).find do |parent_class|
81
+ parent_class.ancestors.include?(EmbeddedDocument)
82
+ end
83
+ end
84
+
47
85
  private
86
+ def accessors_module
87
+ if const_defined?('MongoMapperKeys') && constants.include?( 'MongoMapperKeys' )
88
+ const_get 'MongoMapperKeys'
89
+ else
90
+ const_set 'MongoMapperKeys', Module.new
91
+ end
92
+ end
93
+
94
+ def create_accessors_for(key)
95
+ accessors_module.module_eval <<-end_eval
96
+ def #{key.name}
97
+ read_attribute( :'#{key.name}' )
98
+ end
99
+
100
+ def #{key.name}_before_typecast
101
+ read_attribute_before_typecast(:'#{key.name}')
102
+ end
103
+
104
+ def #{key.name}=(value)
105
+ write_attribute(:'#{key.name}', value)
106
+ end
107
+
108
+ def #{key.name}?
109
+ read_attribute(:#{key.name}).present?
110
+ end
111
+ end_eval
112
+ include accessors_module
113
+ end
114
+
48
115
  def create_indexes_for(key)
49
116
  ensure_index key.name if key.options[:index]
50
117
  end
@@ -55,7 +122,7 @@ module MongoMapper
55
122
  if key.options[:required]
56
123
  validates_presence_of(attribute)
57
124
  end
58
-
125
+
59
126
  if key.options[:unique]
60
127
  validates_uniqueness_of(attribute)
61
128
  end
@@ -81,146 +148,128 @@ module MongoMapper
81
148
  validates_length_of(attribute, length_options)
82
149
  end
83
150
  end
84
-
85
151
  end
86
152
 
87
153
  module InstanceMethods
88
154
  def initialize(attrs={})
89
155
  unless attrs.nil?
90
- initialize_associations(attrs)
156
+ self.class.associations.each_pair do |name, association|
157
+ if collection = attrs.delete(name)
158
+ send("#{association.name}=", collection)
159
+ end
160
+ end
161
+
91
162
  self.attributes = attrs
92
163
  end
164
+
165
+ if self.class.embeddable? && read_attribute(:_id).blank?
166
+ write_attribute :_id, XGen::Mongo::Driver::ObjectID.new.to_s
167
+ end
93
168
  end
94
169
 
95
170
  def attributes=(attrs)
96
171
  return if attrs.blank?
97
- attrs.each_pair do |key_name, value|
98
- if writer?(key_name)
99
- write_attribute(key_name, value)
172
+ attrs.each_pair do |name, value|
173
+ writer_method = "#{name}="
174
+
175
+ if respond_to?(writer_method)
176
+ self.send(writer_method, value)
100
177
  else
101
- writer_method ="#{key_name}="
102
- self.send(writer_method, value) if respond_to?(writer_method)
178
+ self[name.to_s] = value
103
179
  end
104
180
  end
105
181
  end
106
182
 
107
183
  def attributes
108
- self.class.keys.inject(HashWithIndifferentAccess.new) do |attributes, key_hash|
109
- name, key = key_hash
110
- value = value_for_key(key)
111
- attributes[name] = value unless value.nil?
112
- attributes
184
+ attrs = HashWithIndifferentAccess.new
185
+ self.class.keys.each_pair do |name, key|
186
+ value =
187
+ if key.native?
188
+ read_attribute(key.name)
189
+ else
190
+ if embedded_document = read_attribute(key.name)
191
+ embedded_document.attributes
192
+ end
193
+ end
194
+
195
+ attrs[name] = value unless value.nil?
113
196
  end
197
+ attrs.merge!(embedded_association_attributes)
114
198
  end
115
-
116
- def reader?(name)
117
- defined_key_names.include?(name.to_s)
118
- end
119
-
120
- def writer?(name)
121
- name = name.to_s
122
- name = name.chop if name.ends_with?('=')
123
- reader?(name)
124
- end
125
-
126
- def before_typecast_reader?(name)
127
- name.to_s.match(/^(.*)_before_typecast$/) && reader?($1)
199
+
200
+ def attributes_with_associations
201
+ self.class.associations.inject(attributes) do |associations, key_hash|
202
+ name, association = key_hash
203
+ value = self.send(name).map(&:attributes_with_associations)
204
+ associations[name] = value unless value.nil?
205
+ associations
206
+ end
128
207
  end
208
+
129
209
 
130
210
  def [](name)
131
211
  read_attribute(name)
132
212
  end
133
213
 
134
214
  def []=(name, value)
215
+ ensure_key_exists(name)
135
216
  write_attribute(name, value)
136
217
  end
137
218
 
138
- def method_missing(method, *args, &block)
139
- attribute = method.to_s
219
+ def ==(other)
220
+ other.is_a?(self.class) && id == other.id
221
+ end
140
222
 
141
- if reader?(attribute)
142
- read_attribute(attribute)
143
- elsif writer?(attribute)
144
- write_attribute(attribute.chop, args[0])
145
- elsif before_typecast_reader?(attribute)
146
- read_attribute_before_typecast(attribute.gsub(/_before_typecast$/, ''))
147
- else
148
- super
149
- end
223
+ def id
224
+ read_attribute(:_id)
150
225
  end
151
226
 
152
- def ==(other)
153
- other.is_a?(self.class) && attributes == other.attributes
227
+ def id=(value)
228
+ @using_custom_id = true
229
+ write_attribute :_id, value
230
+ end
231
+
232
+ def using_custom_id?
233
+ !!@using_custom_id
154
234
  end
155
235
 
156
236
  def inspect
157
- attributes_as_nice_string = defined_key_names.collect do |name|
237
+ attributes_as_nice_string = self.class.keys.keys.collect do |name|
158
238
  "#{name}: #{read_attribute(name)}"
159
239
  end.join(", ")
160
240
  "#<#{self.class} #{attributes_as_nice_string}>"
161
241
  end
162
242
 
163
- alias :respond_to_without_attributes? :respond_to?
164
-
165
- def respond_to?(method, include_private=false)
166
- return true if reader?(method) || writer?(method) || before_typecast_reader?(method)
167
- super
168
- end
169
-
170
- private
171
- def value_for_key(key)
172
- if key.native?
173
- read_attribute(key.name)
174
- else
175
- embedded_document = read_attribute(key.name)
176
- embedded_document && embedded_document.attributes
243
+ private
244
+ def ensure_key_exists(name)
245
+ self.class.key(name) unless respond_to?("#{name}=")
177
246
  end
178
- end
179
-
180
- def read_attribute(name)
181
- defined_key(name).get(instance_variable_get("@#{name}"))
182
- end
183
-
184
- def read_attribute_before_typecast(name)
185
- instance_variable_get("@#{name}_before_typecast")
186
- end
187
-
188
- def write_attribute(name, value)
189
- instance_variable_set "@#{name}_before_typecast", value
190
- instance_variable_set "@#{name}", defined_key(name).set(value)
191
- end
192
247
 
193
- def defined_key(name)
194
- self.class.keys[name]
195
- end
248
+ def read_attribute(name)
249
+ value = self.class.keys[name].get(instance_variable_get("@#{name}"))
250
+ instance_variable_set "@#{name}", value if !frozen?
251
+ value
252
+ end
196
253
 
197
- def defined_key_names
198
- self.class.keys.keys
199
- end
254
+ def read_attribute_before_typecast(name)
255
+ instance_variable_get("@#{name}_before_typecast")
256
+ end
200
257
 
201
- def only_defined_keys(hash={})
202
- defined_key_names = defined_key_names()
203
- hash.delete_if { |k, v| !defined_key_names.include?(k.to_s) }
204
- end
205
-
206
- def embedded_association_attributes
207
- attributes = HashWithIndifferentAccess.new
208
- self.class.associations.each_pair do |name, association|
209
- if association.type == :many && association.klass.embeddable?
210
- vals = instance_variable_get(association.ivar)
211
- attributes[name] = vals.collect { |item| item.attributes } if vals
212
- end
258
+ def write_attribute(name, value)
259
+ instance_variable_set "@#{name}_before_typecast", value
260
+ instance_variable_set "@#{name}", self.class.keys[name].set(value)
213
261
  end
214
- attributes
215
- end
216
262
 
217
- def initialize_associations(attrs={})
218
- self.class.associations.each_pair do |name, association|
219
- if collection = attrs.delete(name)
220
- __send__("#{association.name}=", collection)
263
+ def embedded_association_attributes
264
+ returning HashWithIndifferentAccess.new do |attrs|
265
+ self.class.associations.each_pair do |name, association|
266
+ next unless association.embeddable?
267
+ next unless documents = instance_variable_get(association.ivar)
268
+
269
+ attrs[name] = documents.collect { |doc| doc.attributes }
270
+ end
221
271
  end
222
272
  end
223
- end
224
- end
225
- end
226
- end
273
+ end # InstanceMethods
274
+ end # EmbeddedDocument
275
+ end # MongoMapper
@@ -2,30 +2,34 @@ module MongoMapper
2
2
  class FinderOptions
3
3
  attr_reader :options
4
4
 
5
- def self.to_mongo_criteria(conditions)
6
- conditions = conditions.dup
5
+ def self.to_mongo_criteria(conditions, parent_key=nil)
7
6
  criteria = {}
8
7
  conditions.each_pair do |field, value|
9
8
  case value
10
9
  when Array
11
- criteria[field] = {'$in' => value}
10
+ operator_present = field.to_s =~ /^\$/
11
+ criteria[field] = if operator_present
12
+ value
13
+ else
14
+ {'$in' => value}
15
+ end
12
16
  when Hash
13
- criteria[field] = to_mongo_criteria(value)
14
- else
17
+ criteria[field] = to_mongo_criteria(value, field)
18
+ else
15
19
  criteria[field] = value
16
20
  end
17
21
  end
18
22
 
19
23
  criteria
20
24
  end
21
-
25
+
22
26
  def self.to_mongo_options(options)
23
27
  options = options.dup
24
28
  {
25
29
  :fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
26
30
  :offset => (options.delete(:offset) || 0).to_i,
27
31
  :limit => (options.delete(:limit) || 0).to_i,
28
- :sort => to_mongo_sort(options.delete(:order))
32
+ :sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
29
33
  }
30
34
  end
31
35
 
@@ -1,17 +1,15 @@
1
- class Boolean; end
2
- class Ref; end
3
-
4
1
  module MongoMapper
5
2
  class Key
6
3
  # DateTime and Date are currently not supported by mongo's bson so just use Time
7
- NativeTypes = [String, Float, Time, Integer, Boolean, Array, Hash, Ref]
4
+ NativeTypes = [String, Float, Time, Integer, Boolean, Array, Hash]
8
5
 
9
6
  attr_accessor :name, :type, :options, :default_value
10
-
11
- def initialize(name, type, options={})
12
- @name, @type = name.to_s, type
13
- self.options = options.symbolize_keys
14
- self.default_value = options.delete(:default)
7
+
8
+ def initialize(*args)
9
+ options = args.extract_options!
10
+ @name, @type = args.shift.to_s, args.shift
11
+ self.options = (options || {}).symbolize_keys
12
+ self.default_value = self.options.delete(:default)
15
13
  end
16
14
 
17
15
  def ==(other)
@@ -23,11 +21,11 @@ module MongoMapper
23
21
  end
24
22
 
25
23
  def native?
26
- @native ||= NativeTypes.include?(type)
24
+ @native ||= NativeTypes.include?(type) || type.nil?
27
25
  end
28
26
 
29
27
  def embedded_document?
30
- type.ancestors.include?(EmbeddedDocument) && !type.ancestors.include?(Document)
28
+ type.respond_to?(:embeddable?) && type.embeddable?
31
29
  end
32
30
 
33
31
  def get(value)
@@ -43,25 +41,21 @@ module MongoMapper
43
41
 
44
42
  private
45
43
  def typecast(value)
44
+ return value if type.nil?
46
45
  return HashWithIndifferentAccess.new(value) if value.is_a?(Hash) && type == Hash
46
+ return value.utc if type == Time && value.kind_of?(type)
47
47
  return value if value.kind_of?(type) || value.nil?
48
48
  begin
49
49
  if type == String then value.to_s
50
50
  elsif type == Float then value.to_f
51
51
  elsif type == Array then value.to_a
52
- elsif type == Time then Time.parse(value.to_s)
53
- #elsif type == Date then Date.parse(value.to_s)
54
- elsif type == Boolean then ['true', 't', '1'].include?(value.to_s.downcase)
52
+ elsif type == Time then Time.parse(value.to_s).utc
53
+ elsif type == Boolean then Boolean.mm_typecast(value)
55
54
  elsif type == Integer
56
55
  # ganked from datamapper
57
56
  value_to_i = value.to_i
58
- if value_to_i == 0 && value != '0'
59
- value_to_s = value.to_s
60
- begin
61
- Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
62
- rescue ArgumentError
63
- nil
64
- end
57
+ if value_to_i == 0
58
+ value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
65
59
  else
66
60
  value_to_i
67
61
  end